diff options
Diffstat (limited to 'libs')
461 files changed, 16843 insertions, 3489 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 88fd461debbe..98935e95deaf 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -16,16 +16,17 @@ package androidx.window.common; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE; import static androidx.window.common.CommonFoldingFeature.parseListFromString; import android.annotation.NonNull; import android.content.Context; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; +import android.hardware.devicestate.DeviceStateUtil; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; @@ -54,29 +55,27 @@ public final class DeviceStateManagerFoldingFeatureProducer private static final boolean DEBUG = false; /** - * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to + * Emulated device state + * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to * {@link CommonFoldingFeature.State} map. */ private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); /** - * Emulated device state received via - * {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}. - * "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with - * physical device states. They represent the state of the device when various software - * features and APIs are applied. The emulated states generally consist of all "base" states, - * but may have additional states such as "concurrent" or "rear display". Concurrent mode for - * example is activated via public API and can be active in both the "open" and "half folded" - * device states. + * Device state received via + * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}. + * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1 + * with the physical state of the device. This could correspond to the system state of the + * device when various software features or overrides are applied. The emulated states generally + * consist of all "base" states, but may have additional states such as "concurrent" or + * "rear display". Concurrent mode for example is activated via public API and can be active in + * both the "open" and "half folded" device states. */ - private int mCurrentDeviceState = INVALID_DEVICE_STATE; + private DeviceState mCurrentDeviceState = new DeviceState( + new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, + "INVALID").build()); - /** - * Base device state received via - * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}. - * "Base" in this context means the "physical" state of the device. - */ - private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE; + private List<DeviceState> mSupportedStates; @NonNull private final RawFoldingFeatureProducer mRawFoldSupplier; @@ -85,22 +84,11 @@ public final class DeviceStateManagerFoldingFeatureProducer private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { @Override - public void onStateChanged(int state) { + public void onDeviceStateChanged(@NonNull DeviceState state) { mCurrentDeviceState = state; mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer .this::notifyFoldingFeatureChange); } - - @Override - public void onBaseStateChanged(int state) { - mCurrentBaseDeviceState = state; - - if (mDeviceStateToPostureMap.get(mCurrentDeviceState) - == COMMON_STATE_USE_BASE_STATE) { - mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer - .this::notifyFoldingFeatureChange); - } - } }; public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, @@ -109,6 +97,7 @@ public final class DeviceStateManagerFoldingFeatureProducer mRawFoldSupplier = rawFoldSupplier; String[] deviceStatePosturePairs = context.getResources() .getStringArray(R.array.config_device_state_postures); + mSupportedStates = deviceStateManager.getSupportedDeviceStates(); boolean isHalfOpenedSupported = false; for (String deviceStatePosturePair : deviceStatePosturePairs) { String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); @@ -168,7 +157,7 @@ public final class DeviceStateManagerFoldingFeatureProducer */ private boolean isCurrentStateValid() { // If the device state is not found in the map, indexOfKey returns a negative number. - return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0; + return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0; } @Override @@ -177,7 +166,9 @@ public final class DeviceStateManagerFoldingFeatureProducer if (hasListeners()) { mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); } else { - mCurrentDeviceState = INVALID_DEVICE_STATE; + mCurrentDeviceState = new DeviceState( + new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, + "INVALID").build()); mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); } } @@ -251,10 +242,13 @@ public final class DeviceStateManagerFoldingFeatureProducer @CommonFoldingFeature.State private int currentHingeState() { @CommonFoldingFeature.State - int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); + int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(), + COMMON_STATE_UNKNOWN); if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { - posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN); + posture = mDeviceStateToPostureMap.get( + DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, + mSupportedStates), COMMON_STATE_UNKNOWN); } return posture; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 6714263ad952..16c77d0c3c81 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -16,15 +16,19 @@ package androidx.window.extensions; -import android.app.ActivityTaskManager; +import static android.view.WindowManager.ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15; +import static android.view.WindowManager.ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15; + import android.app.ActivityThread; import android.app.Application; +import android.app.compat.CompatChanges; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.extensions.area.WindowAreaComponent; @@ -38,25 +42,38 @@ import java.util.Objects; /** - * The reference implementation of {@link WindowExtensions} that implements the initial API version. + * The reference implementation of {@link WindowExtensions} that implements the latest WindowManager + * Extensions APIs. */ -public class WindowExtensionsImpl implements WindowExtensions { +class WindowExtensionsImpl implements WindowExtensions { private static final String TAG = "WindowExtensionsImpl"; + + /** + * The min version of the WM Extensions that must be supported in the current platform version. + */ + @VisibleForTesting + static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6; + private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; private volatile WindowLayoutComponentImpl mWindowLayoutComponent; private volatile SplitController mSplitController; private volatile WindowAreaComponent mWindowAreaComponent; - public WindowExtensionsImpl() { - Log.i(TAG, "Initializing Window Extensions."); + private final int mVersion = EXTENSIONS_VERSION_CURRENT_PLATFORM; + private final boolean mIsActivityEmbeddingEnabled; + + WindowExtensionsImpl() { + mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled(); + Log.i(TAG, "Initializing Window Extensions, vendor API level=" + mVersion + + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled); } // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 5; + return mVersion; } @NonNull @@ -74,8 +91,8 @@ public class WindowExtensionsImpl implements WindowExtensions { if (mFoldingFeatureProducer == null) { synchronized (mLock) { if (mFoldingFeatureProducer == null) { - Context context = getApplication(); - RawFoldingFeatureProducer foldingFeatureProducer = + final Context context = getApplication(); + final RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, @@ -91,8 +108,8 @@ public class WindowExtensionsImpl implements WindowExtensions { if (mWindowLayoutComponent == null) { synchronized (mLock) { if (mWindowLayoutComponent == null) { - Context context = getApplication(); - DeviceStateManagerFoldingFeatureProducer producer = + final Context context = getApplication(); + final DeviceStateManagerFoldingFeatureProducer producer = getFoldingFeatureProducer(); mWindowLayoutComponent = new WindowLayoutComponentImpl(context, producer); } @@ -102,29 +119,35 @@ public class WindowExtensionsImpl implements WindowExtensions { } /** - * Returns a reference implementation of {@link WindowLayoutComponent} if available, - * {@code null} otherwise. The implementation must match the API level reported in - * {@link WindowExtensions#getWindowLayoutComponent()}. + * Returns a reference implementation of the latest {@link WindowLayoutComponent}. + * + * The implementation must match the API level reported in + * {@link WindowExtensions#getVendorApiLevel()}. + * * @return {@link WindowLayoutComponent} OEM implementation */ + @NonNull @Override public WindowLayoutComponent getWindowLayoutComponent() { return getWindowLayoutComponentImpl(); } /** - * Returns a reference implementation of {@link ActivityEmbeddingComponent} if available, - * {@code null} otherwise. The implementation must match the API level reported in - * {@link WindowExtensions#getWindowLayoutComponent()}. + * Returns a reference implementation of the latest {@link ActivityEmbeddingComponent} if the + * device supports this feature, {@code null} otherwise. + * + * The implementation must match the API level reported in + * {@link WindowExtensions#getVendorApiLevel()}. + * * @return {@link ActivityEmbeddingComponent} OEM implementation. */ @Nullable + @Override public ActivityEmbeddingComponent getActivityEmbeddingComponent() { + if (!mIsActivityEmbeddingEnabled) { + return null; + } if (mSplitController == null) { - if (!ActivityTaskManager.supportsMultiWindow(getApplication())) { - // Disable AE for device that doesn't support multi window. - return null; - } synchronized (mLock) { if (mSplitController == null) { mSplitController = new SplitController( @@ -138,21 +161,35 @@ public class WindowExtensionsImpl implements WindowExtensions { } /** - * Returns a reference implementation of {@link WindowAreaComponent} if available, - * {@code null} otherwise. The implementation must match the API level reported in - * {@link WindowExtensions#getWindowAreaComponent()}. + * Returns a reference implementation of the latest {@link WindowAreaComponent} + * + * The implementation must match the API level reported in + * {@link WindowExtensions#getVendorApiLevel()}. + * * @return {@link WindowAreaComponent} OEM implementation. */ + @Nullable + @Override public WindowAreaComponent getWindowAreaComponent() { if (mWindowAreaComponent == null) { synchronized (mLock) { if (mWindowAreaComponent == null) { - Context context = ActivityThread.currentApplication(); - mWindowAreaComponent = - new WindowAreaComponentImpl(context); + final Context context = getApplication(); + mWindowAreaComponent = new WindowAreaComponentImpl(context); } } } return mWindowAreaComponent; } + + @VisibleForTesting + static boolean isActivityEmbeddingEnabled() { + if (!ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) { + // Device enables it for all apps without targetSDK check. + // This must be true for all large screen devices. + return true; + } + // Use compat framework to guard the feature with targetSDK 15. + return CompatChanges.isChangeEnabled(ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java index f9e1f077cffc..5d4c7cbe60e4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java @@ -16,14 +16,20 @@ package androidx.window.extensions; -import android.annotation.NonNull; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.window.extensions.area.WindowAreaComponent; +import androidx.window.extensions.embedding.ActivityEmbeddingComponent; +import androidx.window.extensions.layout.WindowLayoutComponent; /** * Provides the OEM implementation of {@link WindowExtensions}. */ public class WindowExtensionsProvider { - private static final WindowExtensions sWindowExtensions = new WindowExtensionsImpl(); + private static volatile WindowExtensions sWindowExtensions; /** * Returns the OEM implementation of {@link WindowExtensions}. This method is implemented in @@ -33,6 +39,44 @@ public class WindowExtensionsProvider { */ @NonNull public static WindowExtensions getWindowExtensions() { + if (sWindowExtensions == null) { + synchronized (WindowExtensionsProvider.class) { + if (sWindowExtensions == null) { + sWindowExtensions = WindowManager.hasWindowExtensionsEnabled() + ? new WindowExtensionsImpl() + : new DisabledWindowExtensions(); + } + } + } return sWindowExtensions; } + + /** + * The stub version to return when the WindowManager Extensions is disabled + * @see WindowManager#hasWindowExtensionsEnabled + */ + private static class DisabledWindowExtensions implements WindowExtensions { + @Override + public int getVendorApiLevel() { + return 0; + } + + @Nullable + @Override + public WindowLayoutComponent getWindowLayoutComponent() { + return null; + } + + @Nullable + @Override + public ActivityEmbeddingComponent getActivityEmbeddingComponent() { + return null; + } + + @Nullable + @Override + public WindowAreaComponent getWindowAreaComponent() { + return null; + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index b315f94b5d00..a3d2d7f4dcdf 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -16,10 +16,11 @@ package androidx.window.extensions.area; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; import android.app.Activity; import android.content.Context; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateRequest; import android.hardware.display.DisplayManager; @@ -40,6 +41,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -79,7 +81,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; @GuardedBy("mLock") - private int mCurrentDeviceState = INVALID_DEVICE_STATE; + private int mCurrentDeviceState = INVALID_DEVICE_STATE_IDENTIFIER; @GuardedBy("mLock") private int[] mCurrentSupportedDeviceStates; @@ -101,7 +103,9 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mDisplayManager = context.getSystemService(DisplayManager.class); mExecutor = context.getMainExecutor(); - mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates(); + // TODO(b/329436166): Update the usage of device state manager API's + mCurrentSupportedDeviceStates = getSupportedStateIdentifiers( + mDeviceStateManager.getSupportedDeviceStates()); mFoldedDeviceStates = context.getResources().getIntArray( R.array.config_foldedDeviceStates); @@ -143,7 +147,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mRearDisplayStatusListeners.add(consumer); // If current device state is still invalid, the initial value has not been provided. - if (mCurrentDeviceState == INVALID_DEVICE_STATE) { + if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) { return; } consumer.accept(getCurrentRearDisplayModeStatus()); @@ -308,7 +312,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mRearDisplayPresentationStatusListeners.add(consumer); // If current device state is still invalid, the initial value has not been provided - if (mCurrentDeviceState == INVALID_DEVICE_STATE) { + if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) { return; } @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus(); @@ -446,9 +450,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, } @Override - public void onSupportedStatesChanged(int[] supportedStates) { + public void onSupportedStatesChanged(@NonNull List<DeviceState> supportedStates) { synchronized (mLock) { - mCurrentSupportedDeviceStates = supportedStates; + // TODO(b/329436166): Update the usage of device state manager API's + mCurrentSupportedDeviceStates = getSupportedStateIdentifiers(supportedStates); updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); updateRearDisplayPresentationStatusListeners( getCurrentRearDisplayPresentationModeStatus()); @@ -456,9 +461,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, } @Override - public void onStateChanged(int state) { + public void onDeviceStateChanged(@NonNull DeviceState state) { synchronized (mLock) { - mCurrentDeviceState = state; + // TODO(b/329436166): Update the usage of device state manager API's + mCurrentDeviceState = state.getIdentifier(); updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); updateRearDisplayPresentationStatusListeners( getCurrentRearDisplayPresentationModeStatus()); @@ -467,7 +473,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private int getCurrentRearDisplayModeStatus() { - if (mRearDisplayState == INVALID_DEVICE_STATE) { + if (mRearDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) { return WindowAreaComponent.STATUS_UNSUPPORTED; } @@ -482,6 +488,15 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, return WindowAreaComponent.STATUS_AVAILABLE; } + // TODO(b/329436166): Remove and update the usage of device state manager API's + private int[] getSupportedStateIdentifiers(@NonNull List<DeviceState> states) { + int[] identifiers = new int[states.size()]; + for (int i = 0; i < states.size(); i++) { + identifiers[i] = states.get(i).getIdentifier(); + } + return identifiers; + } + /** * Helper method to determine if a rear display session is currently active by checking * if the current device state is that which corresponds to {@code mRearDisplayState}. @@ -495,7 +510,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) { - if (mRearDisplayState == INVALID_DEVICE_STATE) { + if (mRearDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) { return; } synchronized (mLock) { @@ -507,7 +522,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private int getCurrentRearDisplayPresentationModeStatus() { - if (mConcurrentDisplayState == INVALID_DEVICE_STATE) { + if (mConcurrentDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) { return WindowAreaComponent.STATUS_UNSUPPORTED; } @@ -530,7 +545,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private void updateRearDisplayPresentationStatusListeners( @WindowAreaStatus int windowAreaStatus) { - if (mConcurrentDisplayState == INVALID_DEVICE_STATE) { + if (mConcurrentDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) { return; } RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus( diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java new file mode 100644 index 000000000000..0a5a81b4fd8f --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -0,0 +1,1132 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; + +import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT; +import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT; +import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; + +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RotateDrawable; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.window.InputTransferToken; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.GuardedBy; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.window.extensions.core.util.function.Consumer; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Manages the rendering and interaction of the divider. + */ +class DividerPresenter implements View.OnTouchListener { + static final float RATIO_EXPANDED_PRIMARY = 1.0f; + static final float RATIO_EXPANDED_SECONDARY = 0.0f; + private static final String WINDOW_NAME = "AE Divider"; + private static final int VEIL_LAYER = 0; + private static final int DIVIDER_LAYER = 1; + + // TODO(b/327067596) Update based on UX guidance. + private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK); + private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY); + @VisibleForTesting + static final float DEFAULT_MIN_RATIO = 0.35f; + @VisibleForTesting + static final float DEFAULT_MAX_RATIO = 0.65f; + @VisibleForTesting + static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + + private final int mTaskId; + + @NonNull + private final Object mLock = new Object(); + + @NonNull + private final DragEventCallback mDragEventCallback; + + @NonNull + private final Executor mCallbackExecutor; + + /** + * The {@link Properties} of the divider. This field is {@code null} when no divider should be + * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface + * is not available. + */ + @GuardedBy("mLock") + @Nullable + @VisibleForTesting + Properties mProperties; + + /** + * The {@link Renderer} of the divider. This field is {@code null} when no divider should be + * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or + * updated when {@link #mProperties} is changed. + */ + @GuardedBy("mLock") + @Nullable + @VisibleForTesting + Renderer mRenderer; + + /** + * The owner TaskFragment token of the decor surface. The decor surface is placed right above + * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. + */ + @GuardedBy("mLock") + @Nullable + @VisibleForTesting + IBinder mDecorSurfaceOwner; + + /** + * The current divider position relative to the Task bounds. For vertical split (left-to-right + * or right-to-left), it is the x coordinate in the task window, and for horizontal split + * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window. + */ + @GuardedBy("mLock") + private int mDividerPosition; + + DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, + @NonNull Executor callbackExecutor) { + mTaskId = taskId; + mDragEventCallback = dragEventCallback; + mCallbackExecutor = callbackExecutor; + } + + /** Updates the divider when external conditions are changed. */ + void updateDivider( + @NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentParentInfo parentInfo, + @Nullable SplitContainer topSplitContainer) { + if (!Flags.activityEmbeddingInteractiveDividerFlag()) { + return; + } + + synchronized (mLock) { + // Clean up the decor surface if top SplitContainer is null. + if (topSplitContainer == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + final SplitAttributes splitAttributes = topSplitContainer.getCurrentSplitAttributes(); + final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); + + // Clean up the decor surface if DividerAttributes is null. + if (dividerAttributes == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + // At this point, a divider is required. + + // Create the decor surface if one is not available yet. + final SurfaceControl decorSurface = parentInfo.getDecorSurface(); + if (decorSurface == null) { + // Clean up when the decor surface is currently unavailable. + removeDivider(); + // Request to create the decor surface + createOrMoveDecorSurfaceLocked(wct, topSplitContainer.getPrimaryContainer()); + return; + } + + // Update the decor surface owner if needed. + boolean isDraggableExpandType = + SplitAttributesHelper.isDraggableExpandType(splitAttributes); + final TaskFragmentContainer decorSurfaceOwnerContainer = isDraggableExpandType + ? topSplitContainer.getSecondaryContainer() + : topSplitContainer.getPrimaryContainer(); + + if (!Objects.equals( + mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) { + createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer); + } + final boolean isVerticalSplit = isVerticalSplit(topSplitContainer); + final boolean isReversedLayout = isReversedLayout( + topSplitContainer.getCurrentSplitAttributes(), + parentInfo.getConfiguration()); + + updateProperties( + new Properties( + parentInfo.getConfiguration(), + dividerAttributes, + decorSurface, + getInitialDividerPosition( + topSplitContainer, isVerticalSplit, isReversedLayout), + isVerticalSplit, + isReversedLayout, + parentInfo.getDisplayId(), + isDraggableExpandType + )); + } + } + + @GuardedBy("mLock") + private void updateProperties(@NonNull Properties properties) { + if (Properties.equalsForDivider(mProperties, properties)) { + return; + } + final Properties previousProperties = mProperties; + mProperties = properties; + + if (mRenderer == null) { + // Create a new renderer when a renderer doesn't exist yet. + mRenderer = new Renderer(mProperties, this); + } else if (!Properties.areSameSurfaces( + previousProperties.mDecorSurface, mProperties.mDecorSurface) + || previousProperties.mDisplayId != mProperties.mDisplayId) { + // Release and recreate the renderer if the decor surface or the display has changed. + mRenderer.release(); + mRenderer = new Renderer(mProperties, this); + } else { + // Otherwise, update the renderer for the new properties. + mRenderer.update(mProperties); + } + } + + /** + * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner + * of the existing decor surface to be the specified TaskFragment. + * + * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. + */ + void createOrMoveDecorSurface( + @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + synchronized (mLock) { + createOrMoveDecorSurfaceLocked(wct, container); + } + } + + @GuardedBy("mLock") + private void createOrMoveDecorSurfaceLocked( + @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + mDecorSurfaceOwner = container.getTaskFragmentToken(); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); + } + + @GuardedBy("mLock") + private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { + if (mDecorSurfaceOwner != null) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); + mDecorSurfaceOwner = null; + } + removeDivider(); + } + + @GuardedBy("mLock") + private void removeDivider() { + if (mRenderer != null) { + mRenderer.release(); + } + mProperties = null; + mRenderer = null; + } + + @VisibleForTesting + static int getInitialDividerPosition( + @NonNull SplitContainer splitContainer, + boolean isVerticalSplit, + boolean isReversedLayout) { + final Rect primaryBounds = + splitContainer.getPrimaryContainer().getLastRequestedBounds(); + final Rect secondaryBounds = + splitContainer.getSecondaryContainer().getLastRequestedBounds(); + final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); + + if (SplitAttributesHelper.isDraggableExpandType(splitAttributes)) { + // If the container is fully expanded by dragging the divider, we display the divider + // on the edge. + final int dividerWidth = getDividerWidthPx(splitAttributes.getDividerAttributes()); + final int fullyExpandedPosition = isVerticalSplit + ? primaryBounds.right - dividerWidth + : primaryBounds.bottom - dividerWidth; + return isReversedLayout ? fullyExpandedPosition : 0; + } else { + return isVerticalSplit + ? Math.min(primaryBounds.right, secondaryBounds.right) + : Math.min(primaryBounds.bottom, secondaryBounds.bottom); + } + } + + private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) { + final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection(); + switch (layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.LOCALE: + return true; + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return false; + default: + throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection); + } + } + + private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { + int dividerWidthDp = dividerAttributes.getWidthDp(); + return convertDpToPixel(dividerWidthDp); + } + + private static int convertDpToPixel(int dp) { + // TODO(b/329193115) support divider on secondary display + final Context applicationContext = ActivityThread.currentActivityThread().getApplication(); + + return (int) TypedValue.applyDimension( + COMPLEX_UNIT_DIP, + dp, + applicationContext.getResources().getDisplayMetrics()); + } + + private static int getDimensionDp(@IdRes int resId) { + final Context context = ActivityThread.currentActivityThread().getApplication(); + final int px = context.getResources().getDimensionPixelSize(resId); + return (int) TypedValue.convertPixelsToDimension( + COMPLEX_UNIT_DIP, + px, + context.getResources().getDisplayMetrics()); + } + + /** + * Returns the container bound offset that is a result of the presence of a divider. + * + * The offset is the relative position change for the container edge that is next to the divider + * due to the presence of the divider. The value could be negative or positive depending on the + * container position. Positive values indicate that the edge is shifting towards the right + * (or bottom) and negative values indicate that the edge is shifting towards the left (or top). + * + * @param splitAttributes the {@link SplitAttributes} of the split container that we want to + * compute bounds offset. + * @param position the position of the container in the split that we want to compute + * bounds offset for. + * @return the bounds offset in pixels. + */ + static int getBoundsOffsetForDivider( + @NonNull SplitAttributes splitAttributes, + @SplitPresenter.ContainerPosition int position) { + if (!Flags.activityEmbeddingInteractiveDividerFlag()) { + return 0; + } + final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); + if (dividerAttributes == null) { + return 0; + } + final int dividerWidthPx = getDividerWidthPx(dividerAttributes); + return getBoundsOffsetForDivider( + dividerWidthPx, + splitAttributes.getSplitType(), + position); + } + + @VisibleForTesting + static int getBoundsOffsetForDivider( + int dividerWidthPx, + @NonNull SplitType splitType, + @SplitPresenter.ContainerPosition int position) { + if (splitType instanceof ExpandContainersSplitType) { + // No divider offset is needed for the ExpandContainersSplitType. + return 0; + } + int primaryOffset; + if (splitType instanceof final RatioSplitType splitRatio) { + // When a divider is present, both containers shrink by an amount proportional to their + // split ratio and sum to the width of the divider, so that the ending sizing of the + // containers still maintain the same ratio. + primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio()); + } else { + // Hinge split type (and other future split types) will have the divider width equally + // distributed to both containers. + primaryOffset = dividerWidthPx / 2; + } + final int secondaryOffset = dividerWidthPx - primaryOffset; + switch (position) { + case CONTAINER_POSITION_LEFT: + case CONTAINER_POSITION_TOP: + return -primaryOffset; + case CONTAINER_POSITION_RIGHT: + case CONTAINER_POSITION_BOTTOM: + return secondaryOffset; + default: + throw new IllegalArgumentException("Unknown position:" + position); + } + } + + /** + * Sanitizes and sets default values in the {@link DividerAttributes}. + * + * Unset values will be set with system default values. See + * {@link DividerAttributes#WIDTH_SYSTEM_DEFAULT} and + * {@link DividerAttributes#RATIO_SYSTEM_DEFAULT}. + * + * @param dividerAttributes input {@link DividerAttributes} + * @return a {@link DividerAttributes} that has all values properly set. + */ + @Nullable + static DividerAttributes sanitizeDividerAttributes( + @Nullable DividerAttributes dividerAttributes) { + if (dividerAttributes == null) { + return null; + } + int widthDp = dividerAttributes.getWidthDp(); + if (widthDp == WIDTH_SYSTEM_DEFAULT) { + widthDp = DEFAULT_DIVIDER_WIDTH_DP; + } + + if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // Draggable divider width must be larger than the drag handle size. + widthDp = Math.max(widthDp, + getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width)); + } + + float minRatio = dividerAttributes.getPrimaryMinRatio(); + if (minRatio == RATIO_SYSTEM_DEFAULT) { + minRatio = DEFAULT_MIN_RATIO; + } + + float maxRatio = dividerAttributes.getPrimaryMaxRatio(); + if (maxRatio == RATIO_SYSTEM_DEFAULT) { + maxRatio = DEFAULT_MAX_RATIO; + } + + return new DividerAttributes.Builder(dividerAttributes) + .setWidthDp(widthDp) + .setPrimaryMinRatio(minRatio) + .setPrimaryMaxRatio(maxRatio) + .build(); + } + + @Override + public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { + synchronized (mLock) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + mDividerPosition = calculateDividerPosition( + event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mIsVerticalSplit, calculateMinPosition(), calculateMaxPosition()); + mRenderer.setDividerPosition(mDividerPosition); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartDragging(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFinishDragging(); + break; + case MotionEvent.ACTION_MOVE: + onDrag(); + break; + default: + break; + } + } + + // Returns true to prevent the default button click callback. The button pressed state is + // set/unset when starting/finishing dragging. + return true; + } + + @GuardedBy("mLock") + private void onStartDragging() { + mRenderer.mIsDragging = true; + mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + mRenderer.showVeils(t); + + // Callbacks must be executed on the executor to release mLock and prevent deadlocks. + mCallbackExecutor.execute(() -> { + mDragEventCallback.onStartDragging( + wct -> { + synchronized (mLock) { + setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, true /* boosted */, t); + } + }); + }); + } + + @GuardedBy("mLock") + private void onDrag() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + t.apply(); + } + + @GuardedBy("mLock") + private void onFinishDragging() { + mDividerPosition = adjustDividerPositionForSnapPoints(mDividerPosition); + mRenderer.setDividerPosition(mDividerPosition); + + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + mRenderer.hideVeils(t); + + // Callbacks must be executed on the executor to release mLock and prevent deadlocks. + // mDecorSurfaceOwner may change between here and when the callback is executed, + // e.g. when the decor surface owner becomes the secondary container when it is expanded to + // fullscreen. + mCallbackExecutor.execute(() -> { + mDragEventCallback.onFinishDragging( + mTaskId, + wct -> { + synchronized (mLock) { + setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, false /* boosted */, t); + } + }); + }); + mRenderer.mIsDragging = false; + mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); + } + + /** + * Returns the divider position adjusted for the min max ratio and fullscreen expansion. + * + * If the dragging position is above the {@link DividerAttributes#getPrimaryMaxRatio()} or below + * {@link DividerAttributes#getPrimaryMinRatio()} and + * {@link DividerAttributes#isDraggingToFullscreenAllowed} is {@code true}, the system will + * choose a snap algorithm to adjust the ending position to either fully expand one container or + * move the divider back to the specified min/max ratio. + * + * TODO(b/327067596) implement snap algorithm + * + * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0 + * for expanded right (bottom) container, or task width (height) minus the divider width for + * expanded left (top) container. + */ + @GuardedBy("mLock") + private int adjustDividerPositionForSnapPoints(int dividerPosition) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + final int minPosition = calculateMinPosition(); + final int maxPosition = calculateMaxPosition(); + final int fullyExpandedPosition = mProperties.mIsVerticalSplit + ? taskBounds.right - mRenderer.mDividerWidthPx + : taskBounds.bottom - mRenderer.mDividerWidthPx; + if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) { + if (dividerPosition < minPosition) { + return 0; + } + if (dividerPosition > maxPosition) { + return fullyExpandedPosition; + } + } + return Math.clamp(dividerPosition, minPosition, maxPosition); + } + + private static void setDecorSurfaceBoosted( + @NonNull WindowContainerTransaction wct, + @Nullable IBinder decorSurfaceOwner, + boolean boosted, + @NonNull SurfaceControl.Transaction clientTransaction) { + if (decorSurfaceOwner == null) { + return; + } + wct.addTaskFragmentOperation( + decorSurfaceOwner, + new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED) + .setBooleanValue(boosted) + .setSurfaceTransaction(clientTransaction) + .build() + ); + } + + /** Calculates the new divider position based on the touch event and divider attributes. */ + @VisibleForTesting + static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds, + int dividerWidthPx, @NonNull DividerAttributes dividerAttributes, + boolean isVerticalSplit, int minPosition, int maxPosition) { + // The touch event is in display space. Converting it into the task window space. + final int touchPositionInTaskSpace = isVerticalSplit + ? (int) (event.getRawX()) - taskBounds.left + : (int) (event.getRawY()) - taskBounds.top; + + // Assuming that the touch position is at the center of the divider bar, so the divider + // position is offset by half of the divider width. + int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2; + + // If dragging to fullscreen is not allowed, limit the divider position to the min and max + // ratios set in DividerAttributes. Otherwise, dragging beyond the min and max ratios is + // temporarily allowed and the final ratio will be adjusted in onFinishDragging. + if (!isDraggingToFullscreenAllowed(dividerAttributes)) { + dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); + } + return dividerPosition; + } + + @GuardedBy("mLock") + private int calculateMinPosition() { + return calculateMinPosition( + mProperties.mConfiguration.windowConfiguration.getBounds(), + mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); + } + + @GuardedBy("mLock") + private int calculateMaxPosition() { + return calculateMaxPosition( + mProperties.mConfiguration.windowConfiguration.getBounds(), + mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); + } + + /** Calculates the min position of the divider that the user is allowed to drag to. */ + @VisibleForTesting + static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx, + @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, + boolean isReversedLayout) { + // The usable size is the task window size minus the divider bar width. This is shared + // between the primary and secondary containers based on the split ratio. + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + return (int) (isReversedLayout + ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio() + : usableSize * dividerAttributes.getPrimaryMinRatio()); + } + + /** Calculates the max position of the divider that the user is allowed to drag to. */ + @VisibleForTesting + static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx, + @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, + boolean isReversedLayout) { + // The usable size is the task window size minus the divider bar width. This is shared + // between the primary and secondary containers based on the split ratio. + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + return (int) (isReversedLayout + ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio() + : usableSize * dividerAttributes.getPrimaryMaxRatio()); + } + + /** + * Returns the new split ratio of the {@link SplitContainer} based on the current divider + * position. + */ + float calculateNewSplitRatio(@NonNull SplitContainer topSplitContainer) { + synchronized (mLock) { + return calculateNewSplitRatio( + topSplitContainer, + mDividerPosition, + mProperties.mConfiguration.windowConfiguration.getBounds(), + mRenderer.mDividerWidthPx, + mProperties.mIsVerticalSplit, + mProperties.mIsReversedLayout, + calculateMinPosition(), + calculateMaxPosition(), + isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)); + } + } + + private static boolean isDraggingToFullscreenAllowed( + @NonNull DividerAttributes dividerAttributes) { + // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is + // updated. + return true; + } + + /** + * Returns the new split ratio of the {@link SplitContainer} based on the current divider + * position. + * + * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio. + * @param dividerPosition the divider position. See {@link #mDividerPosition}. + * @param taskBounds the task bounds + * @param dividerWidthPx the width of the divider in pixels. + * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the + * split is a horizontal split. See + * {@link #isVerticalSplit(SplitContainer)}. + * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or + * bottom-to-top. If {@code false}, the split is not reversed, i.e. + * left-to-right or top-to-bottom. See + * {@link SplitAttributesHelper#isReversedLayout} + * @return the computed split ratio of the primary container. If the primary container is fully + * expanded, {@link #RATIO_EXPANDED_PRIMARY} is returned. If the secondary container is fully + * expanded, {@link #RATIO_EXPANDED_SECONDARY} is returned. + */ + @VisibleForTesting + static float calculateNewSplitRatio( + @NonNull SplitContainer topSplitContainer, + int dividerPosition, + @NonNull Rect taskBounds, + int dividerWidthPx, + boolean isVerticalSplit, + boolean isReversedLayout, + int minPosition, + int maxPosition, + boolean isDraggingToFullscreenAllowed) { + + // Handle the fully expanded cases. + if (isDraggingToFullscreenAllowed) { + // The divider position is already adjusted by the snap algorithm in onFinishDragging. + // If the divider position is not in the range [minPosition, maxPosition], then one of + // the containers is fully expanded. + if (dividerPosition < minPosition) { + return isReversedLayout ? RATIO_EXPANDED_PRIMARY : RATIO_EXPANDED_SECONDARY; + } + if (dividerPosition > maxPosition) { + return isReversedLayout ? RATIO_EXPANDED_SECONDARY : RATIO_EXPANDED_PRIMARY; + } + } else { + dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); + } + + final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); + final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds(); + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + + final float newRatio; + if (isVerticalSplit) { + final int newPrimaryWidth = isReversedLayout + ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx)) + : (dividerPosition - origPrimaryBounds.left); + newRatio = 1.0f * newPrimaryWidth / usableSize; + } else { + final int newPrimaryHeight = isReversedLayout + ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx)) + : (dividerPosition - origPrimaryBounds.top); + newRatio = 1.0f * newPrimaryHeight / usableSize; + } + return newRatio; + } + + /** Callbacks for drag events */ + interface DragEventCallback { + /** + * Called when the user starts dragging the divider. Callbacks are executed on + * {@link #mCallbackExecutor}. + * + * @param action additional action that should be applied to the + * {@link WindowContainerTransaction} + */ + void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action); + + /** + * Called when the user finishes dragging the divider. Callbacks are executed on + * {@link #mCallbackExecutor}. + * + * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to. + * @param action additional action that should be applied to the + * {@link WindowContainerTransaction} + */ + void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action); + } + + /** + * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on + * these properties. When any value is updated, the divider is re-rendered. The Properties + * instance is created only when all the pre-conditions of drawing a divider are met. + */ + @VisibleForTesting + static class Properties { + private static final int CONFIGURATION_MASK_FOR_DIVIDER = + ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + @NonNull + private final Configuration mConfiguration; + @NonNull + private final DividerAttributes mDividerAttributes; + @NonNull + private final SurfaceControl mDecorSurface; + + /** The initial position of the divider calculated based on container bounds. */ + private final int mInitialDividerPosition; + + /** Whether the split is vertical, such as left-to-right or right-to-left split. */ + private final boolean mIsVerticalSplit; + + private final int mDisplayId; + private final boolean mIsReversedLayout; + private final boolean mIsDraggableExpandType; + + @VisibleForTesting + Properties( + @NonNull Configuration configuration, + @NonNull DividerAttributes dividerAttributes, + @NonNull SurfaceControl decorSurface, + int initialDividerPosition, + boolean isVerticalSplit, + boolean isReversedLayout, + int displayId, + boolean isDraggableExpandType) { + mConfiguration = configuration; + mDividerAttributes = dividerAttributes; + mDecorSurface = decorSurface; + mInitialDividerPosition = initialDividerPosition; + mIsVerticalSplit = isVerticalSplit; + mIsReversedLayout = isReversedLayout; + mDisplayId = displayId; + mIsDraggableExpandType = isDraggableExpandType; + } + + /** + * Compares whether two Properties objects are equal for rendering the divider. The + * Configuration is checked for rendering related fields, and other fields are checked for + * regular equality. + */ + private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return areSameSurfaces(a.mDecorSurface, b.mDecorSurface) + && Objects.equals(a.mDividerAttributes, b.mDividerAttributes) + && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) + && a.mInitialDividerPosition == b.mInitialDividerPosition + && a.mIsVerticalSplit == b.mIsVerticalSplit + && a.mDisplayId == b.mDisplayId + && a.mIsReversedLayout == b.mIsReversedLayout + && a.mIsDraggableExpandType == b.mIsDraggableExpandType; + } + + private static boolean areSameSurfaces( + @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) { + if (sc1 == sc2) { + // If both are null or both refer to the same object. + return true; + } + if (sc1 == null || sc2 == null) { + return false; + } + return sc1.isSameSurface(sc2); + } + + private static boolean areConfigurationsEqualForDivider( + @NonNull Configuration a, @NonNull Configuration b) { + final int diff = a.diff(b); + return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0; + } + } + + /** + * Handles the rendering of the divider. When the decor surface is updated, the renderer is + * recreated. When other fields in the Properties are changed, the renderer is updated. + */ + @VisibleForTesting + static class Renderer { + @NonNull + private final SurfaceControl mDividerSurface; + @NonNull + private final WindowlessWindowManager mWindowlessWindowManager; + @NonNull + private final SurfaceControlViewHost mViewHost; + @NonNull + private final FrameLayout mDividerLayout; + @NonNull + private final View.OnTouchListener mListener; + @NonNull + private Properties mProperties; + private int mDividerWidthPx; + @Nullable + private SurfaceControl mPrimaryVeil; + @Nullable + private SurfaceControl mSecondaryVeil; + private boolean mIsDragging; + private int mDividerPosition; + private View mDragHandle; + + private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) { + mProperties = properties; + mListener = listener; + + mDividerSurface = createChildSurface("DividerSurface", true /* visible */); + mWindowlessWindowManager = new WindowlessWindowManager( + mProperties.mConfiguration, + mDividerSurface, + new InputTransferToken()); + + final Context context = ActivityThread.currentActivityThread().getApplication(); + final DisplayManager displayManager = context.getSystemService(DisplayManager.class); + mViewHost = new SurfaceControlViewHost( + context, displayManager.getDisplay(mProperties.mDisplayId), + mWindowlessWindowManager, "DividerContainer"); + mDividerLayout = new FrameLayout(context); + + update(); + } + + /** Updates the divider when properties are changed */ + private void update(@NonNull Properties newProperties) { + mProperties = newProperties; + update(); + } + + /** Updates the divider when initializing or when properties are changed */ + @VisibleForTesting + void update() { + mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); + mDividerPosition = mProperties.mInitialDividerPosition; + mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); + // TODO handle synchronization between surface transactions and WCT. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + updateSurface(t); + updateLayout(); + updateDivider(t); + t.apply(); + } + + @VisibleForTesting + void release() { + mViewHost.release(); + // TODO handle synchronization between surface transactions and WCT. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.remove(mDividerSurface); + removeVeils(t); + t.apply(); + } + + private void setDividerPosition(int dividerPosition) { + mDividerPosition = dividerPosition; + } + + /** + * Updates the positions and crops of the divider surface and veil surfaces. This method + * should be called when {@link #mProperties} is changed or while dragging to update the + * position of the divider surface and the veil surfaces. + */ + private void updateSurface(@NonNull SurfaceControl.Transaction t) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + if (mProperties.mIsVerticalSplit) { + t.setPosition(mDividerSurface, mDividerPosition, 0.0f); + t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height()); + } else { + t.setPosition(mDividerSurface, 0.0f, mDividerPosition); + t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx); + } + if (mIsDragging) { + updateVeils(t); + } + } + + /** + * Updates the layout parameters of the layout used to host the divider. This method should + * be called only when {@link #mProperties} is changed. This should not be called while + * dragging, because the layout parameters are not changed during dragging. + */ + private void updateLayout() { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit + ? new WindowManager.LayoutParams( + mDividerWidthPx, + taskBounds.height(), + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT) + : new WindowManager.LayoutParams( + taskBounds.width(), + mDividerWidthPx, + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + lp.setTitle(WINDOW_NAME); + mViewHost.setView(mDividerLayout, lp); + mViewHost.relayout(lp); + } + + /** + * Updates the UI component of the divider, including the drag handle and the veils. This + * method should be called only when {@link #mProperties} is changed. This should not be + * called while dragging, because the UI components are not changed during dragging and + * only their surface positions are changed. + */ + private void updateDivider(@NonNull SurfaceControl.Transaction t) { + mDividerLayout.removeAllViews(); + if (mProperties.mIsDraggableExpandType) { + // If a container is fully expanded, the divider overlays on the expanded container. + mDividerLayout.setBackgroundColor(Color.TRANSPARENT); + } else { + mDividerLayout.setBackgroundColor(mProperties.mDividerAttributes.getDividerColor()); + } + if (mProperties.mDividerAttributes.getDividerType() + == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + createVeils(); + drawDragHandle(); + } else { + removeVeils(t); + } + mViewHost.getView().invalidate(); + } + + private void drawDragHandle() { + final Context context = mDividerLayout.getContext(); + final ImageButton button = new ImageButton(context); + final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit + ? new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height)) + : new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width)); + params.gravity = Gravity.CENTER; + button.setLayoutParams(params); + button.setBackgroundColor(R.color.transparent); + + final Drawable handle = context.getResources().getDrawable( + R.drawable.activity_embedding_divider_handle, context.getTheme()); + if (mProperties.mIsVerticalSplit) { + button.setImageDrawable(handle); + } else { + // Rotate the handle drawable + RotateDrawable rotatedHandle = new RotateDrawable(); + rotatedHandle.setFromDegrees(90f); + rotatedHandle.setToDegrees(90f); + rotatedHandle.setPivotXRelative(true); + rotatedHandle.setPivotYRelative(true); + rotatedHandle.setPivotX(0.5f); + rotatedHandle.setPivotY(0.5f); + rotatedHandle.setLevel(1); + rotatedHandle.setDrawable(handle); + + button.setImageDrawable(rotatedHandle); + } + + button.setOnTouchListener(mListener); + mDragHandle = button; + mDividerLayout.addView(button); + } + + @NonNull + private SurfaceControl createChildSurface(@NonNull String name, boolean visible) { + final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + return new SurfaceControl.Builder() + .setParent(mProperties.mDecorSurface) + .setName(name) + .setHidden(!visible) + .setCallsite("DividerManager.createChildSurface") + .setBufferSize(bounds.width(), bounds.height()) + .setEffectLayer() + .build(); + } + + private void createVeils() { + if (mPrimaryVeil == null) { + mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */); + } + if (mSecondaryVeil == null) { + mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */); + } + } + + private void removeVeils(@NonNull SurfaceControl.Transaction t) { + if (mPrimaryVeil != null) { + t.remove(mPrimaryVeil); + } + if (mSecondaryVeil != null) { + t.remove(mSecondaryVeil); + } + mPrimaryVeil = null; + mSecondaryVeil = null; + } + + private void showVeils(@NonNull SurfaceControl.Transaction t) { + t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR)) + .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR)) + .setLayer(mDividerSurface, DIVIDER_LAYER) + .setLayer(mPrimaryVeil, VEIL_LAYER) + .setLayer(mSecondaryVeil, VEIL_LAYER) + .setVisibility(mPrimaryVeil, true) + .setVisibility(mSecondaryVeil, true); + updateVeils(t); + } + + private void hideVeils(@NonNull SurfaceControl.Transaction t) { + t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false); + } + + private void updateVeils(@NonNull SurfaceControl.Transaction t) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + + // Relative bounds of the primary and secondary containers in the Task. + Rect primaryBounds; + Rect secondaryBounds; + if (mProperties.mIsVerticalSplit) { + final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height()); + final Rect boundsRight = new Rect(mDividerPosition + mDividerWidthPx, 0, + taskBounds.width(), taskBounds.height()); + primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft; + secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight; + } else { + final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition); + final Rect boundsBottom = new Rect(0, mDividerPosition + mDividerWidthPx, + taskBounds.width(), taskBounds.height()); + primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; + secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; + } + t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); + t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); + t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); + t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); + } + + private static float[] colorToFloatArray(@NonNull Color color) { + return new float[]{color.red(), color.green(), color.blue()}; + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 80afb16d5832..32f2d67888ae 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -165,10 +165,11 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { /** * Expands an existing TaskFragment to fill parent. * @param wct WindowContainerTransaction in which the task fragment should be resized. - * @param fragmentToken token of an existing TaskFragment. + * @param container the {@link TaskFragmentContainer} to be expanded. */ void expandTaskFragment(@NonNull WindowContainerTransaction wct, - @NonNull IBinder fragmentToken) { + @NonNull TaskFragmentContainer container) { + final IBinder fragmentToken = container.getTaskFragmentToken(); resizeTaskFragment(wct, fragmentToken, new Rect()); clearAdjacentTaskFragments(wct, fragmentToken); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java new file mode 100644 index 000000000000..4541a843f479 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import android.content.res.Configuration; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; + +/** Helper functions for {@link SplitAttributes} */ +class SplitAttributesHelper { + /** + * Returns whether the split layout direction is reversed. Right-to-left and bottom-to-top are + * considered reversed. + */ + static boolean isReversedLayout( + @NonNull SplitAttributes splitAttributes, @NonNull Configuration configuration) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return false; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return true; + case SplitAttributes.LayoutDirection.LOCALE: + return configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + default: + throw new IllegalArgumentException( + "Invalid layout direction:" + splitAttributes.getLayoutDirection()); + } + } + + /** + * Returns whether the {@link SplitAttributes} is an {@link ExpandContainersSplitType} and it + * should show a draggable handle that allows the user to drag and restore it into a split. + * This state is a result of user dragging the divider to fully expand the secondary container. + */ + static boolean isDraggableExpandType(@NonNull SplitAttributes splitAttributes) { + final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); + return splitAttributes.getSplitType() instanceof ExpandContainersSplitType + && dividerAttributes != null + && dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE; + + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 038d0081ead8..46a3e7f38bed 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -21,6 +21,7 @@ import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; @@ -56,6 +57,7 @@ import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; import android.app.Instrumentation; +import android.app.servertransaction.ClientTransactionListenerController; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -87,7 +89,7 @@ import androidx.annotation.Nullable; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; -import androidx.window.extensions.WindowExtensionsImpl; +import androidx.window.extensions.WindowExtensions; import androidx.window.extensions.core.util.function.Consumer; import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.core.util.function.Predicate; @@ -103,16 +105,22 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.BiConsumer; /** * Main controller class that manages split states and presentation. */ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, - ActivityEmbeddingComponent { + ActivityEmbeddingComponent, DividerPresenter.DragEventCallback { static final String TAG = "SplitController"; static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without + // association. It's not set in WM Extensions nor Wm Jetpack library currently. + private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY = + "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity"; + @VisibleForTesting @GuardedBy("mLock") final SplitPresenter mPresenter; @@ -161,6 +169,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); + /** Map from Task id to {@link DividerPresenter} which manages the divider in the Task. */ + @GuardedBy("mLock") + private final SparseArray<DividerPresenter> mDividerPresenters = new SparseArray<>(); + /** Callback to Jetpack to notify about changes to split states. */ @GuardedBy("mLock") @Nullable @@ -178,16 +190,31 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final List<ActivityStack> mLastReportedActivityStacks = new ArrayList<>(); + /** WM Jetpack set callback for {@link EmbeddedActivityWindowInfo}. */ + @GuardedBy("mLock") + @Nullable + private Pair<Executor, Consumer<EmbeddedActivityWindowInfo>> + mEmbeddedActivityWindowInfoCallback; + + /** Listener registered to {@link ClientTransactionListenerController}. */ + @GuardedBy("mLock") + @Nullable + private final BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener = + Flags.activityWindowInfoFlag() + ? this::onActivityWindowInfoChanged + : null; + private final Handler mHandler; + private final MainThreadExecutor mExecutor; final Object mLock = new Object(); private final ActivityStartMonitor mActivityStartMonitor; public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent, @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { Log.i(TAG, "Initializing Activity Embedding Controller."); - final MainThreadExecutor executor = new MainThreadExecutor(); - mHandler = executor.mHandler; - mPresenter = new SplitPresenter(executor, windowLayoutComponent, this); + mExecutor = new MainThreadExecutor(); + mHandler = mExecutor.mHandler; + mPresenter = new SplitPresenter(mExecutor, windowLayoutComponent, this); mTransactionManager = new TransactionManager(mPresenter); final ActivityThread activityThread = ActivityThread.currentActivityThread(); final Application application = activityThread.getApplication(); @@ -394,7 +421,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Registers the split organizer callback to notify about changes to active splits. * * @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with - * {@link WindowExtensionsImpl#getVendorApiLevel()} 2. + * {@link WindowExtensions#getVendorApiLevel()} 2. */ @Deprecated @Override @@ -407,7 +434,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Registers the split organizer callback to notify about changes to active splits. * - * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2 + * @since {@link WindowExtensions#getVendorApiLevel()} 2 */ @Override public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) { @@ -829,9 +856,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); - // If the last direct activity of the host task is dismissed and the overlay container is - // the only taskFragment, the overlay container should also be dismissed. - dismissOverlayContainerIfNeeded(wct, taskContainer); + // The divider need to be updated even if shouldUpdateContainer is false, because the decor + // surface may change in TaskFragmentParentInfo, which requires divider update but not + // container update. + updateDivider(wct, taskContainer); + + // If the last direct activity of the host task is dismissed and there's an always-on-top + // overlay container in the task, the overlay container should also be dismissed. + dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer); if (!shouldUpdateContainer) { return; @@ -990,6 +1022,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (taskContainer.isEmpty()) { // Cleanup the TaskContainer if it becomes empty. mTaskContainers.remove(taskContainer.getTaskId()); + mDividerPresenters.remove(taskContainer.getTaskId()); } return; } @@ -1208,7 +1241,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer container = getContainerWithActivity(activity); if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } else { // Put activity into a new expanded container. final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); @@ -1378,9 +1411,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */); } + @GuardedBy("mLock") + private void onActivityPaused(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { + // Checks if there's any finishing activity in paused state associate with an overlay + // container. #OnActivityPostDestroyed is a very late signal, which is called after activity + // is not visible and the next activity shows on screen. + if (!activity.isFinishing()) { + // onPaused is triggered without finishing. Early return. + return; + } + // Check if we should dismiss the overlay container with this finishing activity. + final IBinder activityToken = activity.getActivityToken(); + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + mTaskContainers.valueAt(i).onFinishingActivityPaused(wct, activityToken); + } + updateCallbackIfNecessary(); + } + @VisibleForTesting @GuardedBy("mLock") - void onActivityDestroyed(@NonNull Activity activity) { + void onActivityDestroyed(@NonNull WindowContainerTransaction wct, @NonNull Activity activity) { if (!activity.isFinishing()) { // onDestroyed is triggered without finishing. This happens when the activity is // relaunched. In this case, we don't want to cleanup the record. @@ -1390,7 +1441,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // organizer. final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - mTaskContainers.valueAt(i).onActivityDestroyed(activityToken); + mTaskContainers.valueAt(i).onActivityDestroyed(wct, activityToken); } // We didn't trigger the callback if there were any pending appeared activities, so check // again after the pending is removed. @@ -1570,7 +1621,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable Activity launchingActivity) { return createEmptyContainer(wct, intent, taskId, new ActivityStackAttributes.Builder().build(), launchingActivity, - null /* overlayTag */, null /* launchOptions */); + null /* overlayTag */, null /* launchOptions */, + false /* shouldAssociateWithLaunchingActivity */); } /** @@ -1585,7 +1637,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @NonNull ActivityStackAttributes activityStackAttributes, @Nullable Activity launchingActivity, @Nullable String overlayTag, - @Nullable Bundle launchOptions) { + @Nullable Bundle launchOptions, boolean associateLaunchingActivity) { // We need an activity in the organizer process in the same Task to use as the owner // activity, as well as to get the Task window info. final Activity activityInTask; @@ -1603,7 +1655,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */, intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag, - launchOptions); + launchOptions, associateLaunchingActivity); final IBinder taskFragmentToken = container.getTaskFragmentToken(); // Note that taskContainer will not exist before calling #newContainer if the container // is the first embedded TF in the task. @@ -1695,7 +1747,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, - null /* launchOptions */); + null /* launchOptions */, false /* associateLaunchingActivity */); } @GuardedBy("mLock") @@ -1703,7 +1755,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, - null /* launchOptions */); + null /* launchOptions */, false /* associateLaunchingActivity */); } @GuardedBy("mLock") @@ -1712,7 +1764,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer pairedPrimaryContainer) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, activityInTask, taskId, pairedPrimaryContainer, null /* tag */, - null /* launchOptions */); + null /* launchOptions */, false /* associateLaunchingActivity */); } /** @@ -1724,29 +1776,32 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param activityInTask activity in the same Task so that we can get the Task bounds * if needed. * @param taskId parent Task of the new TaskFragment. - * @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is + * @param pairedContainer the paired primary {@link TaskFragmentContainer}. When it is * set, the new container will be added right above it. * @param overlayTag The tag for the new created overlay container. It must be * needed if {@code isOverlay} is {@code true}. Otherwise, * it should be {@code null}. * @param launchOptions The launch options bundle to create a container. Must be * specified for overlay container. + * @param associateLaunchingActivity {@code true} to indicate this overlay container + * should associate with launching activity. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, - @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, - @Nullable Bundle launchOptions) { + @Nullable TaskFragmentContainer pairedContainer, @Nullable String overlayTag, + @Nullable Bundle launchOptions, boolean associateLaunchingActivity) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } if (!mTaskContainers.contains(taskId)) { mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); + mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag, - launchOptions); + pendingAppearedIntent, taskContainer, this, pairedContainer, overlayTag, + launchOptions, associateLaunchingActivity ? activityInTask : null); return container; } @@ -1912,7 +1967,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (shouldContainerBeExpanded(container)) { if (container.getInfo() != null) { - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } // If the info is not available yet the task fragment will be expanded when it's ready return; @@ -1935,7 +1990,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); - if (dismissOverlayContainerIfNeeded(wct, taskContainer)) { + if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) { return; } @@ -1959,22 +2014,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - /** Dismisses the overlay container in the {@code taskContainer} if needed. */ + /** + * Dismisses {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the {@code taskContainer} + * if needed. + */ @GuardedBy("mLock") - private boolean dismissOverlayContainerIfNeeded(@NonNull WindowContainerTransaction wct, - @NonNull TaskContainer taskContainer) { - final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer(); - if (overlayContainer == null) { + private boolean dismissAlwaysOnTopOverlayIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { + // Dismiss always-on-top overlay container if it's the only container in the task and + // there's no direct activity in the parent task. + final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers(); + if (containers.size() != 1 || taskContainer.hasDirectActivity()) { return false; } - // Dismiss the overlay container if it's the only container in the task and there's no - // direct activity in the parent task. - if (taskContainer.getTaskFragmentContainers().size() == 1 - && !taskContainer.hasDirectActivity()) { - mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); - return true; + + final TaskFragmentContainer container = containers.getLast(); + if (!container.isAlwaysOnTopOverlay()) { + return false; } - return false; + + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependant */); + return true; } /** @@ -2113,6 +2173,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + if (container != null && container.getTaskContainer().isPlaceholderRuleSuppressed()) { + return false; + } + final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity, placeholderRule.getPlaceholderIntent()); @@ -2208,6 +2272,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } + if (SplitPresenter.shouldShowPlaceholderWhenExpanded(splitAttributes)) { + return false; + } mTransactionManager.getCurrentTransactionRecord() .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); @@ -2456,6 +2523,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @VisibleForTesting + @Nullable + ActivityThread.ActivityClientRecord getActivityClientRecord(@NonNull Activity activity) { + return ActivityThread.currentActivityThread() + .getActivityClient(activity.getActivityToken()); + } + + @VisibleForTesting ActivityStartMonitor getActivityStartMonitor() { return mActivityStartMonitor; } @@ -2468,8 +2542,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @VisibleForTesting @Nullable IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) { - final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() - .getActivityClient(activity.getActivityToken()); + final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity); return record != null ? record.mTaskFragmentToken : null; } @@ -2552,25 +2625,42 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Gets all overlay containers from all tasks in this process, or an empty list if there's * no overlay container. - * <p> - * Note that we only support one overlay container for each task, but an app could have multiple - * tasks. */ @VisibleForTesting @GuardedBy("mLock") @NonNull - List<TaskFragmentContainer> getAllOverlayTaskFragmentContainers() { + List<TaskFragmentContainer> getAllNonFinishingOverlayContainers() { final List<TaskFragmentContainer> overlayContainers = new ArrayList<>(); for (int i = 0; i < mTaskContainers.size(); i++) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); - final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer(); - if (overlayContainer != null) { - overlayContainers.add(overlayContainer); - } + final List<TaskFragmentContainer> overlayContainersPerTask = taskContainer + .getTaskFragmentContainers() + .stream() + .filter(c -> c.isOverlay() && !c.isFinished()) + .toList(); + overlayContainers.addAll(overlayContainersPerTask); } return overlayContainers; } + /** + * Creates an overlay container or updates a visible overlay container if its + * {@link TaskFragmentContainer#getTaskId()}, {@link TaskFragmentContainer#getOverlayTag()} + * and {@link TaskFragmentContainer#getAssociatedActivityToken()} matches. + * <p> + * This method will also dismiss any existing overlay container if: + * <ul> + * <li>it's visible but not meet the criteria to update overlay</li> + * <li>{@link TaskFragmentContainer#getOverlayTag()} matches but not meet the criteria to + * update overlay</li> + * </ul> + * + * @param wct the {@link WindowContainerTransaction} + * @param options the {@link ActivityOptions} to launch the overlay + * @param intent the intent of activity to launch + * @param launchActivity the activity to launch the overlay container + * @return the overlay container + */ @VisibleForTesting // Suppress GuardedBy warning because lint ask to mark this method as // @GuardedBy(container.mController.mLock), which is mLock itself @@ -2581,8 +2671,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { final List<TaskFragmentContainer> overlayContainers = - getAllOverlayTaskFragmentContainers(); + getAllNonFinishingOverlayContainers(); final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); + final boolean associateLaunchingActivity = options + .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true); // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions // specified by Intent, expand the overlay container to fill the parent task instead. @@ -2603,35 +2695,91 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final int taskId = getTaskId(launchActivity); if (!overlayContainers.isEmpty()) { for (final TaskFragmentContainer overlayContainer : overlayContainers) { - if (!overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId == overlayContainer.getTaskId()) { - // If there's an overlay container with different tag shown in the same + final boolean isTopNonFinishingOverlay = overlayContainer.equals( + overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer( + true /* includePin */, true /* includeOverlay */)); + if (taskId != overlayContainer.getTaskId()) { + // If there's an overlay container with same tag in a different task, + // dismiss the overlay container since the tag must be unique per process. + if (overlayTag.equals(overlayContainer.getOverlayTag())) { + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's an existing overlay container with the same tag but" + + " different task ID:" + overlayContainer.getTaskId() + ". " + + "The new associated activity is " + launchActivity); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + continue; + } + if (!overlayTag.equals(overlayContainer.getOverlayTag())) { + // If there's an overlay container with different tag on top in the same // task, dismiss the existing overlay container. + if (isTopNonFinishingOverlay) { + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + continue; + } + // The overlay container has the same tag and task ID with the new launching + // overlay container. + if (!isTopNonFinishingOverlay) { + // Dismiss the invisible overlay container regardless of activity + // association if it collides the tag of new launched overlay container . + Log.w(TAG, "The invisible overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's a launching overlay container with the same tag." + + " The new associated activity is " + launchActivity); mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); + continue; } - if (overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId != overlayContainer.getTaskId()) { - // If there's an overlay container with same tag in a different task, - // dismiss the overlay container since the tag must be unique per process. + // Requesting an always-on-top overlay. + if (!associateLaunchingActivity) { + if (overlayContainer.isAssociatedWithActivity()) { + // Dismiss the overlay container since it has associated with an activity. + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's an existing overlay container with the same tag but" + + " different associated launching activity. The overlay container" + + " doesn't associate with any activity."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + continue; + } else { + // The existing overlay container doesn't associate an activity as well. + // Just update the overlay and return. + // Note that going to this condition means the tag, task ID matches a + // visible always-on-top overlay, and won't dismiss any overlay any more. + mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, + getMinDimensions(intent)); + return overlayContainer; + } + } + if (launchActivity.getActivityToken() + != overlayContainer.getAssociatedActivityToken()) { + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's an existing overlay container with the same tag but" + + " different associated launching activity. The new associated" + + " activity is " + launchActivity); + // The associated activity must be the same, or it will be dismissed. mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); + continue; } - if (overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId == overlayContainer.getTaskId()) { - mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, - getMinDimensions(intent)); - // We can just return the updated overlay container and don't need to - // check other condition since we only have one OverlayCreateParams, and - // if the tag and task are matched, it's impossible to match another task - // or tag since tags and tasks are all unique. - return overlayContainer; - } + // Reaching here means the launching activity launch an overlay container with the + // same task ID, tag, while there's a previously launching visible overlay + // container. We'll regard it as updating the existing overlay container. + mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, + getMinDimensions(intent)); + return overlayContainer; + } } // Launch the overlay container to the task with taskId. return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag, - options); + options, associateLaunchingActivity); } private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @@ -2720,6 +2868,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override + public void onActivityPostPaused(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } + synchronized (mLock) { + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_CLOSE); + SplitController.this.onActivityPaused( + transactionRecord.getTransaction(), activity); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } + + @Override public void onActivityPostDestroyed(@NonNull Activity activity) { if (activity.isChild()) { // Skip Activity that is child of another Activity (ActivityGroup) because it's @@ -2727,7 +2892,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } synchronized (mLock) { - SplitController.this.onActivityDestroyed(activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_CLOSE); + SplitController.this.onActivityDestroyed( + transactionRecord.getTransaction(), activity); + transactionRecord.apply(false /* shouldApplyIndependently */); } } } @@ -2876,17 +3046,102 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + @Override + public void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor, + @NonNull Consumer<EmbeddedActivityWindowInfo> callback) { + if (!Flags.activityWindowInfoFlag()) { + return; + } + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + synchronized (mLock) { + if (mEmbeddedActivityWindowInfoCallback == null) { + ClientTransactionListenerController.getInstance() + .registerActivityWindowInfoChangedListener(getActivityWindowInfoListener()); + } + mEmbeddedActivityWindowInfoCallback = new Pair<>(executor, callback); + } + } + + @Override + public void clearEmbeddedActivityWindowInfoCallback() { + if (!Flags.activityWindowInfoFlag()) { + return; + } + synchronized (mLock) { + if (mEmbeddedActivityWindowInfoCallback == null) { + return; + } + mEmbeddedActivityWindowInfoCallback = null; + ClientTransactionListenerController.getInstance() + .unregisterActivityWindowInfoChangedListener(getActivityWindowInfoListener()); + } + } + + @VisibleForTesting + @GuardedBy("mLock") @Nullable - private static ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { + BiConsumer<IBinder, ActivityWindowInfo> getActivityWindowInfoListener() { + return mActivityWindowInfoListener; + } + + @Nullable + @Override + public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { + if (!Flags.activityWindowInfoFlag()) { + return null; + } + synchronized (mLock) { + final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); + return activityWindowInfo != null + ? translateActivityWindowInfo(activity, activityWindowInfo) + : null; + } + } + + @VisibleForTesting + void onActivityWindowInfoChanged(@NonNull IBinder activityToken, + @NonNull ActivityWindowInfo activityWindowInfo) { + synchronized (mLock) { + if (mEmbeddedActivityWindowInfoCallback == null) { + return; + } + final Executor executor = mEmbeddedActivityWindowInfoCallback.first; + final Consumer<EmbeddedActivityWindowInfo> callback = + mEmbeddedActivityWindowInfoCallback.second; + + final Activity activity = getActivity(activityToken); + if (activity == null) { + return; + } + final EmbeddedActivityWindowInfo info = translateActivityWindowInfo( + activity, activityWindowInfo); + + executor.execute(() -> callback.accept(info)); + } + } + + @Nullable + private ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { if (activity.isFinishing()) { return null; } - final ActivityThread.ActivityClientRecord record = - ActivityThread.currentActivityThread() - .getActivityClient(activity.getActivityToken()); + final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity); return record != null ? record.getActivityWindowInfo() : null; } + @NonNull + private static EmbeddedActivityWindowInfo translateActivityWindowInfo( + @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) { + final boolean isEmbedded = activityWindowInfo.isEmbedded(); + final Rect activityBounds = new Rect(activity.getResources().getConfiguration() + .windowConfiguration.getBounds()); + final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds()); + final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds()); + return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds, + activityStackBounds); + } + /** * If the two rules have the same presentation, and the calculated {@link SplitAttributes} * matches the {@link SplitAttributes} of {@link SplitContainer}, we can reuse the same @@ -2957,4 +3212,51 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return configuration != null && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; } + + @GuardedBy("mLock") + void updateDivider( + @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) { + final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId()); + final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo(); + if (parentInfo != null) { + dividerPresenter.updateDivider( + wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer()); + } + } + + @Override + public void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action) { + synchronized (mLock) { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + action.accept(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } + + @Override + public void onFinishDragging( + int taskId, + @NonNull Consumer<WindowContainerTransaction> action) { + synchronized (mLock) { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + final TaskContainer taskContainer = mTaskContainers.get(taskId); + if (taskContainer != null) { + final DividerPresenter dividerPresenter = + mDividerPresenters.get(taskContainer.getTaskId()); + final List<TaskFragmentContainer> containersToFinish = new ArrayList<>(); + taskContainer.updateTopSplitContainerForDivider( + dividerPresenter, containersToFinish); + for (final TaskFragmentContainer container : containersToFinish) { + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } + updateContainersInTask(wct, taskContainer); + } + action.accept(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index b53b9c519cb6..6231ea09e5cc 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import static android.content.pm.PackageManager.MATCH_ALL; +import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; +import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import android.app.Activity; @@ -32,7 +34,6 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.util.LayoutDirection; import android.util.Pair; import android.util.Size; import android.view.View; @@ -85,10 +86,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} - private static final int CONTAINER_POSITION_LEFT = 0; - private static final int CONTAINER_POSITION_TOP = 1; - private static final int CONTAINER_POSITION_RIGHT = 2; - private static final int CONTAINER_POSITION_BOTTOM = 3; + static final int CONTAINER_POSITION_LEFT = 0; + static final int CONTAINER_POSITION_TOP = 1; + static final int CONTAINER_POSITION_RIGHT = 2; + static final int CONTAINER_POSITION_BOTTOM = 3; @IntDef(value = { CONTAINER_POSITION_LEFT, @@ -96,7 +97,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { CONTAINER_POSITION_RIGHT, CONTAINER_POSITION_BOTTOM, }) - private @interface ContainerPosition {} + @interface ContainerPosition {} /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, @@ -367,6 +368,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); + mController.updateDivider(wct, taskContainer); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -465,6 +467,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { reorderTaskFragmentToFront(wct, pinnedContainer.getSecondaryContainer().getTaskFragmentToken()); } + final TaskFragmentContainer alwaysOnTopOverlayContainer = container.getTaskContainer() + .getAlwaysOnTopOverlayContainer(); + if (alwaysOnTopOverlayContainer != null) { + reorderTaskFragmentToFront(wct, alwaysOnTopOverlayContainer.getTaskFragmentToken()); + } } @Override @@ -563,7 +570,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @Override void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, - @Nullable IBinder secondary) { + @Nullable IBinder secondary) { final TaskFragmentContainer container = mController.getContainer(primary); if (container == null) { throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is" @@ -588,7 +595,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, taskBounds); final boolean isFillParent = relativeBounds.isEmpty(); - final boolean isIsolatedNavigated = !isFillParent && container.isOverlay(); + // Note that we only set isolated navigation for overlay container without activity + // association. Activity will be launched to an expanded container on top of the overlay + // if the overlay is associated with an activity. Thus, an overlay with activity association + // will never be isolated navigated. + final boolean isIsolatedNavigated = container.isOverlay() + && !container.isAssociatedWithActivity() && !isFillParent; final boolean dimOnTask = !isFillParent && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK && Flags.fullscreenDimFlag(); @@ -685,8 +697,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { splitContainer.getPrimaryContainer().getTaskFragmentToken(); final IBinder secondaryToken = splitContainer.getSecondaryContainer().getTaskFragmentToken(); - expandTaskFragment(wct, primaryToken); - expandTaskFragment(wct, secondaryToken); + expandTaskFragment(wct, splitContainer.getPrimaryContainer()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer()); // Set the companion TaskFragment when the two containers stacked. setCompanionTaskFragment(wct, primaryToken, secondaryToken, splitContainer.getSplitRule(), true /* isStacked */); @@ -695,6 +707,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return RESULT_NOT_EXPANDED; } + /** + * Expands an existing TaskFragment to fill parent. + * @param wct WindowContainerTransaction in which the task fragment should be resized. + * @param container the {@link TaskFragmentContainer} to be expanded. + */ + void expandTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { + super.expandTaskFragment(wct, container); + mController.updateDivider(wct, container.getTaskContainer()); + } + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { return shouldShowSplit(splitContainer.getCurrentSplitAttributes()); } @@ -703,6 +726,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); } + static boolean shouldShowPlaceholderWhenExpanded(@NonNull SplitAttributes splitAttributes) { + // The placeholder should be kept if the expand split type is a result of user dragging + // the divider. + return SplitAttributesHelper.isDraggableExpandType(splitAttributes); + } + @NonNull SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes, @@ -738,6 +767,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, @NonNull SplitAttributes splitAttributes, @Nullable Pair<Size, Size> minDimensionsPair) { + // Sanitize the DividerAttributes and set default values. + if (splitAttributes.getDividerAttributes() != null) { + splitAttributes = new SplitAttributes.Builder(splitAttributes) + .setDividerAttributes( + DividerPresenter.sanitizeDividerAttributes( + splitAttributes.getDividerAttributes()) + ).build(); + } + if (minDimensionsPair == null) { return splitAttributes; } @@ -930,18 +968,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ private static SplitAttributes updateSplitAttributesType( @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) { - return new SplitAttributes.Builder() + return new SplitAttributes.Builder(splitAttributes) .setSplitType(splitTypeToUpdate) - .setLayoutDirection(splitAttributes.getLayoutDirection()) - .setAnimationBackground(splitAttributes.getAnimationBackground()) .build(); } @NonNull private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int dividerOffset = getBoundsOffsetForDivider( + splitAttributes, CONTAINER_POSITION_LEFT); final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, - CONTAINER_POSITION_LEFT, foldingFeature); + CONTAINER_POSITION_LEFT, foldingFeature) + dividerOffset; final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); } @@ -949,8 +987,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int dividerOffset = getBoundsOffsetForDivider( + splitAttributes, CONTAINER_POSITION_RIGHT); final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, - CONTAINER_POSITION_RIGHT, foldingFeature); + CONTAINER_POSITION_RIGHT, foldingFeature) + dividerOffset; final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); } @@ -958,8 +998,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int dividerOffset = getBoundsOffsetForDivider( + splitAttributes, CONTAINER_POSITION_TOP); final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, - CONTAINER_POSITION_TOP, foldingFeature); + CONTAINER_POSITION_TOP, foldingFeature) + dividerOffset; final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); } @@ -967,8 +1009,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int dividerOffset = getBoundsOffsetForDivider( + splitAttributes, CONTAINER_POSITION_BOTTOM); final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, - CONTAINER_POSITION_BOTTOM, foldingFeature); + CONTAINER_POSITION_BOTTOM, foldingFeature) + dividerOffset; final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); } @@ -1091,7 +1135,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { - final int layoutDirection = splitAttributes.getLayoutDirection(); final SplitType splitType = splitAttributes.getSplitType(); if (splitType instanceof ExpandContainersSplitType) { return splitType; @@ -1100,19 +1143,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary // computation have the same direction, which is from (top, left) to (bottom, right). final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); - switch (layoutDirection) { - case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: - case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: - return splitType; - case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: - case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: - return reversedSplitType; - case LayoutDirection.LOCALE: { - boolean isLtr = taskConfiguration.getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - return isLtr ? splitType : reversedSplitType; - } - } + return isReversedLayout(splitAttributes, taskConfiguration) + ? reversedSplitType + : splitType; } else if (splitType instanceof HingeSplitType) { final HingeSplitType hinge = (HingeSplitType) splitType; @WindowingMode diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 73109e266905..fdf0910519b5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -22,6 +22,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.inMultiWindowMode; +import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_PRIMARY; +import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_SECONDARY; + import android.app.Activity; import android.app.ActivityClient; import android.app.WindowConfiguration; @@ -40,6 +43,9 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; import java.util.ArrayList; import java.util.List; @@ -64,9 +70,11 @@ class TaskContainer { @Nullable private SplitPinContainer mSplitPinContainer; - /** The overlay container in this Task. */ + /** + * The {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the Task. + */ @Nullable - private TaskFragmentContainer mOverlayContainer; + private TaskFragmentContainer mAlwaysOnTopOverlayContainer; @NonNull private final Configuration mConfiguration; @@ -77,6 +85,9 @@ class TaskContainer { private boolean mHasDirectActivity; + @Nullable + private TaskFragmentParentInfo mTaskFragmentParentInfo; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when * the organizer receives @@ -86,13 +97,31 @@ class TaskContainer { final Set<IBinder> mFinishedContainer = new ArraySet<>(); /** + * The {@link RatioSplitType} that will be applied to newly added containers. This is to ensure + * the required UX that, after user dragging the divider, the split ratio is persistent after + * launching a new activity into a new TaskFragment in the same Task. + */ + private RatioSplitType mOverrideSplitType; + + /** + * If {@code true}, suppress the placeholder rules in the {@link TaskContainer}. + * <p> + * This is used in case the user drags the divider to fully expand the primary container and + * dismiss the secondary container while a {@link SplitPlaceholderRule} is used. Without this + * flag, after dismissing the secondary container, a placeholder will be launched again. + * <p> + * This flag is set true when the primary container is fully expanded and cleared when a new + * split is added to the {@link TaskContainer}. + */ + private boolean mPlaceholderRuleSuppressed; + + /** * The {@link TaskContainer} constructor * - * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with - * {@code activityInTask}. + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to * initialize the {@link TaskContainer} properties. - * */ TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { @@ -136,10 +165,17 @@ class TaskContainer { } void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + // TODO(b/293654166): cache the TaskFragmentParentInfo and remove these fields. mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.getDisplayId(); mIsVisible = info.isVisible(); mHasDirectActivity = info.hasDirectActivity(); + mTaskFragmentParentInfo = info; + } + + @Nullable + TaskFragmentParentInfo getTaskFragmentParentInfo() { + return mTaskFragmentParentInfo; } /** @@ -161,8 +197,8 @@ class TaskContainer { * Returns the windowing mode for the TaskFragments below this Task, which should be split with * other TaskFragments. * - * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when - * the pair of TaskFragments are stacked due to the limited space. + * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when + * the pair of TaskFragments are stacked due to the limited space. */ @WindowingMode int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) { @@ -202,10 +238,19 @@ class TaskContainer { return mContainers.isEmpty() && mFinishedContainer.isEmpty(); } + /** Called when the activity {@link Activity#isFinishing()} and paused. */ + void onFinishingActivityPaused(@NonNull WindowContainerTransaction wct, + @NonNull IBinder activityToken) { + for (TaskFragmentContainer container : mContainers) { + container.onFinishingActivityPaused(wct, activityToken); + } + } + /** Called when the activity is destroyed. */ - void onActivityDestroyed(@NonNull IBinder activityToken) { + void onActivityDestroyed(@NonNull WindowContainerTransaction wct, + @NonNull IBinder activityToken) { for (TaskFragmentContainer container : mContainers) { - container.onActivityDestroyed(activityToken); + container.onActivityDestroyed(wct, activityToken); } } @@ -228,7 +273,7 @@ class TaskContainer { @Nullable TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin, - boolean includeOverlay) { + boolean includeOverlay) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); if (!includePin && isTaskFragmentContainerPinned(container)) { @@ -273,17 +318,19 @@ class TaskContainer { return null; } - /** Returns the overlay container in the task, or {@code null} if it doesn't exist. */ + /** + * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. + */ @Nullable - TaskFragmentContainer getOverlayContainer() { - return mOverlayContainer; + TaskFragmentContainer getAlwaysOnTopOverlayContainer() { + return mAlwaysOnTopOverlayContainer; } int indexOf(@NonNull TaskFragmentContainer child) { return mContainers.indexOf(child); } - /** Whether the Task is in an intermediate state waiting for the server update.*/ + /** Whether the Task is in an intermediate state waiting for the server update. */ boolean isInIntermediateState() { for (TaskFragmentContainer container : mContainers) { if (container.isInIntermediateState()) { @@ -304,6 +351,11 @@ class TaskContainer { } void addSplitContainer(@NonNull SplitContainer splitContainer) { + // Reset the placeholder rule suppression when a new split container is added. + mPlaceholderRuleSuppressed = false; + + applyOverrideSplitTypeIfNeeded(splitContainer); + if (splitContainer instanceof SplitPinContainer) { mSplitPinContainer = (SplitPinContainer) splitContainer; mSplitContainers.add(splitContainer); @@ -318,6 +370,39 @@ class TaskContainer { } } + boolean isPlaceholderRuleSuppressed() { + return mPlaceholderRuleSuppressed; + } + + // If there is an override SplitType due to user dragging the divider, the split ratio should + // be applied to newly added SplitContainers. + private void applyOverrideSplitTypeIfNeeded(@NonNull SplitContainer splitContainer) { + if (mOverrideSplitType == null) { + return; + } + final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); + final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); + if (!(splitAttributes.getSplitType() instanceof RatioSplitType)) { + // Skip if the original split type is not a ratio type. + return; + } + if (dividerAttributes == null + || dividerAttributes.getDividerType() != DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // Skip if the split does not have a draggable divider. + return; + } + updateDefaultSplitAttributes(splitContainer, mOverrideSplitType); + } + + private static void updateDefaultSplitAttributes( + @NonNull SplitContainer splitContainer, @NonNull SplitType overrideSplitType) { + splitContainer.updateDefaultSplitAttributes( + new SplitAttributes.Builder(splitContainer.getDefaultSplitAttributes()) + .setSplitType(overrideSplitType) + .build() + ); + } + void removeSplitContainers(@NonNull List<SplitContainer> containers) { mSplitContainers.removeAll(containers); } @@ -389,13 +474,82 @@ class TaskContainer { return mContainers; } + void updateTopSplitContainerForDivider( + @NonNull DividerPresenter dividerPresenter, + @NonNull List<TaskFragmentContainer> outContainersToFinish) { + final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer(); + if (topSplitContainer == null) { + return; + } + final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); + final float newRatio = dividerPresenter.calculateNewSplitRatio(topSplitContainer); + + // If the primary container is fully expanded, we should finish all the associated + // secondary containers. + if (newRatio == RATIO_EXPANDED_PRIMARY) { + for (final SplitContainer splitContainer : mSplitContainers) { + if (primaryContainer == splitContainer.getPrimaryContainer()) { + outContainersToFinish.add(splitContainer.getSecondaryContainer()); + } + } + + // Temporarily suppress the placeholder rule in the TaskContainer. This will be restored + // if a new split is added into the TaskContainer. + mPlaceholderRuleSuppressed = true; + + mOverrideSplitType = null; + return; + } + + final SplitType newSplitType; + if (newRatio == RATIO_EXPANDED_SECONDARY) { + newSplitType = new ExpandContainersSplitType(); + // We do not want to apply ExpandContainersSplitType to new split containers. + mOverrideSplitType = null; + } else { + // We save the override RatioSplitType and apply to new split containers. + newSplitType = mOverrideSplitType = new RatioSplitType(newRatio); + } + for (final SplitContainer splitContainer : mSplitContainers) { + if (primaryContainer == splitContainer.getPrimaryContainer()) { + updateDefaultSplitAttributes(splitContainer, newSplitType); + } + } + } + + @Nullable + SplitContainer getTopNonFinishingSplitContainer() { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = mSplitContainers.get(i); + if (!splitContainer.getPrimaryContainer().isFinished() + && !splitContainer.getSecondaryContainer().isFinished()) { + return splitContainer; + } + } + return null; + } + private void onTaskFragmentContainerUpdated() { // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce // another special container that should also be on top in the future. updateSplitPinContainerIfNecessary(); // Update overlay container after split pin container since the overlay should be on top of // pin container. - updateOverlayContainerIfNecessary(); + updateAlwaysOnTopOverlayIfNecessary(); + } + + private void updateAlwaysOnTopOverlayIfNecessary() { + final List<TaskFragmentContainer> alwaysOnTopOverlays = mContainers + .stream().filter(TaskFragmentContainer::isAlwaysOnTopOverlay).toList(); + if (alwaysOnTopOverlays.size() > 1) { + throw new IllegalStateException("There must be at most one always-on-top overlay " + + "container per Task"); + } + mAlwaysOnTopOverlayContainer = alwaysOnTopOverlays.isEmpty() + ? null : alwaysOnTopOverlays.getFirst(); + if (mAlwaysOnTopOverlayContainer != null) { + moveContainerToLastIfNecessary(mAlwaysOnTopOverlayContainer); + } } private void updateSplitPinContainerIfNecessary() { @@ -423,18 +577,6 @@ class TaskContainer { } } - private void updateOverlayContainerIfNecessary() { - final List<TaskFragmentContainer> overlayContainers = mContainers.stream() - .filter(TaskFragmentContainer::isOverlay).toList(); - if (overlayContainers.size() > 1) { - throw new IllegalStateException("There must be at most one overlay container per Task"); - } - mOverlayContainer = overlayContainers.isEmpty() ? null : overlayContainers.get(0); - if (mOverlayContainer != null) { - moveContainerToLastIfNecessary(mOverlayContainer); - } - } - /** Moves the {@code container} to the last to align taskFragments' z-order. */ private void moveContainerToLastIfNecessary(@NonNull TaskFragmentContainer container) { final int index = mContainers.indexOf(container); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index a6bf99d4add5..094ebcb470f2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -113,6 +113,18 @@ class TaskFragmentContainer { @NonNull private final Bundle mLaunchOptions = new Bundle(); + /** + * The associated {@link Activity#getActivityToken()} of the overlay container. + * Must be {@code null} for non-overlay container. + * <p> + * If an overlay container is associated with an activity, this overlay container will be + * dismissed when the associated activity is destroyed. If the overlay container is visible, + * activity will be launched on top of the overlay container and expanded to fill the parent + * container. + */ + @Nullable + private final IBinder mAssociatedActivityToken; + /** Indicates whether the container was cleaned up after the last activity was removed. */ private boolean mIsFinished; @@ -178,7 +190,7 @@ class TaskFragmentContainer { /** * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController, - * TaskFragmentContainer, String, Bundle) + * TaskFragmentContainer, String, Bundle, Activity) */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @@ -187,7 +199,7 @@ class TaskFragmentContainer { @Nullable TaskFragmentContainer pairedPrimaryContainer) { this(pendingAppearedActivity, pendingAppearedIntent, taskContainer, controller, pairedPrimaryContainer, null /* overlayTag */, - null /* launchOptions */); + null /* launchOptions */, null /* associatedActivity */); } /** @@ -197,12 +209,14 @@ class TaskFragmentContainer { * @param overlayTag Sets to indicate this taskFragment is an overlay container * @param launchOptions The launch options to create this container. Must not be * {@code null} for an overlay container + * @param associatedActivity the associated activity of the overlay container. Must be + * {@code null} for a non-overlay container. */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, @NonNull SplitController controller, @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, - @Nullable Bundle launchOptions) { + @Nullable Bundle launchOptions, @Nullable Activity associatedActivity) { if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( @@ -214,7 +228,13 @@ class TaskFragmentContainer { mOverlayTag = overlayTag; if (overlayTag != null) { Objects.requireNonNull(launchOptions); + } else if (associatedActivity != null) { + throw new IllegalArgumentException("Associated activity must be null for " + + "non-overlay activity."); } + mAssociatedActivityToken = associatedActivity != null + ? associatedActivity.getActivityToken() : null; + if (launchOptions != null) { mLaunchOptions.putAll(launchOptions); } @@ -420,14 +440,38 @@ class TaskFragmentContainer { } } + /** Called when the activity {@link Activity#isFinishing()} and paused. */ + void onFinishingActivityPaused(@NonNull WindowContainerTransaction wct, + @NonNull IBinder activityToken) { + finishSelfWithActivityIfNeeded(wct, activityToken); + } + /** Called when the activity is destroyed. */ - void onActivityDestroyed(@NonNull IBinder activityToken) { + void onActivityDestroyed(@NonNull WindowContainerTransaction wct, + @NonNull IBinder activityToken) { removePendingAppearedActivity(activityToken); if (mInfo != null) { // Remove the activity now because there can be a delay before the server callback. mInfo.getActivities().remove(activityToken); } mActivitiesToFinishOnExit.remove(activityToken); + finishSelfWithActivityIfNeeded(wct, activityToken); + } + + @VisibleForTesting + void finishSelfWithActivityIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull IBinder activityToken) { + if (mIsFinished) { + return; + } + // Early return if this container is not an overlay with activity association. + if (!isOverlay() || !isAssociatedWithActivity()) { + return; + } + if (mAssociatedActivityToken == activityToken) { + // If the associated activity is destroyed, also finish this overlay container. + mController.mPresenter.cleanupContainer(wct, this, false /* shouldFinishDependent */); + } } @Nullable @@ -748,6 +792,10 @@ class TaskFragmentContainer { } } + @NonNull Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + /** * Checks if last requested windowing mode is equal to the provided value. * @see WindowContainerTransaction#setWindowingMode @@ -957,6 +1005,32 @@ class TaskFragmentContainer { return mLaunchOptions; } + /** + * Returns the associated Activity token of this overlay container. It must be {@code null} + * for non-overlay container. + * <p> + * If an overlay container is associated with an activity, this overlay container will be + * dismissed when the associated activity is destroyed. If the overlay container is visible, + * activity will be launched on top of the overlay container and expanded to fill the parent + * container. + */ + @Nullable + IBinder getAssociatedActivityToken() { + return mAssociatedActivityToken; + } + + boolean isAssociatedWithActivity() { + return mAssociatedActivityToken != null; + } + + /** + * Returns {@code true} if the overlay container should be always on top, which should be + * a non-fill-parent overlay without activity association. + */ + boolean isAlwaysOnTopOverlay() { + return isOverlay() && !isAssociatedWithActivity(); + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); @@ -976,6 +1050,7 @@ class TaskFragmentContainer { + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished + " overlayTag=" + mOverlayTag + + " associatedActivity" + mAssociatedActivityToken + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java index 62959b7b95e9..686a31b6be04 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java @@ -17,25 +17,48 @@ package androidx.window.sidecar; import android.content.Context; +import android.view.WindowManager; + +import androidx.annotation.Nullable; /** * Provider class that will instantiate the library implementation. It must be included in the * vendor library, and the vendor implementation must match the signature of this class. */ public class SidecarProvider { + + private static volatile Boolean sIsWindowExtensionsEnabled; + /** * Provide a simple implementation of {@link SidecarInterface} that can be replaced by * an OEM by overriding this method. */ + @Nullable public static SidecarInterface getSidecarImpl(Context context) { - return new SampleSidecarImpl(context.getApplicationContext()); + return isWindowExtensionsEnabled() + ? new SampleSidecarImpl(context.getApplicationContext()) + : null; } /** * The support library will use this method to check API version compatibility. * @return API version string in MAJOR.MINOR.PATCH-description format. */ + @Nullable public static String getApiVersion() { - return "1.0.0-reference"; + return isWindowExtensionsEnabled() + ? "1.0.0-reference" + : null; + } + + private static boolean isWindowExtensionsEnabled() { + if (sIsWindowExtensionsEnabled == null) { + synchronized (SidecarProvider.class) { + if (sIsWindowExtensionsEnabled == null) { + sIsWindowExtensionsEnabled = WindowManager.hasWindowExtensionsEnabled(); + } + } + } + return sIsWindowExtensionsEnabled; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java index b9c808a6569b..46c1f3ba4691 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java @@ -17,6 +17,7 @@ package androidx.window.sidecar; import android.os.IBinder; +import android.util.Log; import androidx.annotation.NonNull; @@ -29,6 +30,8 @@ import java.util.Set; */ abstract class StubSidecar implements SidecarInterface { + private static final String TAG = "WindowManagerSidecar"; + private SidecarCallback mSidecarCallback; final Set<IBinder> mWindowLayoutChangeListenerTokens = new HashSet<>(); private boolean mDeviceStateChangeListenerRegistered; @@ -61,14 +64,22 @@ abstract class StubSidecar implements SidecarInterface { void updateDeviceState(SidecarDeviceState newState) { if (this.mSidecarCallback != null) { - mSidecarCallback.onDeviceStateChanged(newState); + try { + mSidecarCallback.onDeviceStateChanged(newState); + } catch (AbstractMethodError e) { + Log.e(TAG, "App is using an outdated Window Jetpack library", e); + } } } void updateWindowLayout(@NonNull IBinder windowToken, @NonNull SidecarWindowLayoutInfo newLayout) { if (this.mSidecarCallback != null) { - mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout); + try { + mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout); + } catch (AbstractMethodError e) { + Log.e(TAG, "App is using an outdated Window Jetpack library", e); + } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index f471af052bf2..4267749dfa6b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -16,12 +16,15 @@ package androidx.window.extensions; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static androidx.window.extensions.WindowExtensionsImpl.EXTENSIONS_VERSION_CURRENT_PLATFORM; import static com.google.common.truth.Truth.assertThat; -import android.app.ActivityTaskManager; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + import android.platform.test.annotations.Presubmit; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -42,25 +45,61 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class WindowExtensionsTest { + private WindowExtensions mExtensions; + private int mVersion; @Before public void setUp() { mExtensions = WindowExtensionsProvider.getWindowExtensions(); + mVersion = mExtensions.getVendorApiLevel(); + } + + @Test + public void testGetVendorApiLevel_extensionsEnabled_matchesCurrentVersion() { + assumeTrue(WindowManager.hasWindowExtensionsEnabled()); + assertThat(mVersion).isEqualTo(EXTENSIONS_VERSION_CURRENT_PLATFORM); } @Test - public void testGetWindowLayoutComponent() { + public void testGetVendorApiLevel_extensionsDisabled_returnsZero() { + assumeFalse(WindowManager.hasWindowExtensionsEnabled()); + assertThat(mVersion).isEqualTo(0); + } + + @Test + public void testGetWindowLayoutComponent_extensionsEnabled_returnsImplementation() { + assumeTrue(WindowManager.hasWindowExtensionsEnabled()); assertThat(mExtensions.getWindowLayoutComponent()).isNotNull(); } @Test - public void testGetActivityEmbeddingComponent() { - if (ActivityTaskManager.supportsMultiWindow(getInstrumentation().getContext())) { - assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull(); - } else { - assertThat(mExtensions.getActivityEmbeddingComponent()).isNull(); - } + public void testGetWindowLayoutComponent_extensionsDisabled_returnsNull() { + assumeFalse(WindowManager.hasWindowExtensionsEnabled()); + assertThat(mExtensions.getWindowLayoutComponent()).isNull(); + } + @Test + public void testGetActivityEmbeddingComponent_featureDisabled_returnsNull() { + assumeFalse(WindowExtensionsImpl.isActivityEmbeddingEnabled()); + assertThat(mExtensions.getActivityEmbeddingComponent()).isNull(); + } + + @Test + public void testGetActivityEmbeddingComponent_featureEnabled_returnsImplementation() { + assumeTrue(WindowExtensionsImpl.isActivityEmbeddingEnabled()); + assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull(); + } + + @Test + public void testGetWindowAreaComponent_extensionsEnabled_returnsImplementation() { + assumeTrue(WindowManager.hasWindowExtensionsEnabled()); + assertThat(mExtensions.getWindowAreaComponent()).isNotNull(); + } + + @Test + public void testGetWindowAreaComponent_extensionsDisabled_returnsNull() { + assumeFalse(WindowManager.hasWindowExtensionsEnabled()); + assertThat(mExtensions.getWindowAreaComponent()).isNull(); } @Test diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java new file mode 100644 index 000000000000..de0171de4a37 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -0,0 +1,629 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; + +import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; +import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.window.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.Executor; + +/** + * Test class for {@link DividerPresenter}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:DividerPresenterTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DividerPresenterTest { + @Rule + public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + + private static final int MOCK_TASK_ID = 1234; + + @Mock + private DividerPresenter.Renderer mRenderer; + + @Mock + private WindowContainerTransaction mTransaction; + + @Mock + private TaskFragmentParentInfo mParentInfo; + + @Mock + private TaskContainer mTaskContainer; + + @Mock + private DividerPresenter.DragEventCallback mDragEventCallback; + + @Mock + private SplitContainer mSplitContainer; + + @Mock + private SurfaceControl mSurfaceControl; + + private DividerPresenter mDividerPresenter; + + private final IBinder mPrimaryContainerToken = new Binder(); + + private final IBinder mSecondaryContainerToken = new Binder(); + + private final IBinder mAnotherContainerToken = new Binder(); + + private DividerPresenter.Properties mProperties; + + private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build(); + + private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setWidthDp(10).build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG); + + when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID); + + when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY); + when(mParentInfo.getConfiguration()).thenReturn(new Configuration()); + when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl); + + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES) + .build()); + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mPrimaryContainerToken, new Rect(0, 0, 950, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + mProperties = new DividerPresenter.Properties( + new Configuration(), + DEFAULT_DIVIDER_ATTRIBUTES, + mSurfaceControl, + getInitialDividerPosition( + mSplitContainer, true /* isVerticalSplit */, false /* isReversedLayout */), + true /* isVerticalSplit */, + false /* isReversedLayout */, + Display.DEFAULT_DISPLAY, + false /* isDraggableExpandType */); + + mDividerPresenter = new DividerPresenter( + MOCK_TASK_ID, mDragEventCallback, mock(Executor.class)); + mDividerPresenter.mProperties = mProperties; + mDividerPresenter.mRenderer = mRenderer; + mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken; + } + + @Test + public void testUpdateDivider() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES) + .build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() { + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mAnotherContainerToken, new Rect(0, 0, 750, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(800, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner); + verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation); + } + + @Test + public void testUpdateDivider_noChangeIfPropertiesIdentical() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer, never()).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenDividerAttributesIsNull() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder().setDividerAttributes(null).build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testSanitizeDividerAttributes_setDefaultValues() { + DividerAttributes attributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build(); + DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes); + + assertEquals(DividerAttributes.DIVIDER_TYPE_DRAGGABLE, sanitized.getDividerType()); + assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp()); + assertEquals(DividerPresenter.DEFAULT_MIN_RATIO, sanitized.getPrimaryMinRatio(), + 0.0f /* delta */); + assertEquals(DividerPresenter.DEFAULT_MAX_RATIO, sanitized.getPrimaryMaxRatio(), + 0.0f /* delta */); + } + + @Test + public void testSanitizeDividerAttributes_notChangingValidValues() { + DividerAttributes attributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setWidthDp(24) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.7f) + .build(); + DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes); + + assertEquals(attributes, sanitized); + } + + @Test + public void testGetBoundsOffsetForDivider_ratioSplitType() { + final int dividerWidthPx = 100; + final float splitRatio = 0.25f; + final SplitAttributes.SplitType splitType = + new SplitAttributes.SplitType.RatioSplitType(splitRatio); + final int expectedTopLeftOffset = 25; + final int expectedBottomRightOffset = 75; + + assertDividerOffsetEquals( + dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); + } + + @Test + public void testGetBoundsOffsetForDivider_ratioSplitType_withRounding() { + final int dividerWidthPx = 101; + final float splitRatio = 0.25f; + final SplitAttributes.SplitType splitType = + new SplitAttributes.SplitType.RatioSplitType(splitRatio); + final int expectedTopLeftOffset = 25; + final int expectedBottomRightOffset = 76; + + assertDividerOffsetEquals( + dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); + } + + @Test + public void testGetBoundsOffsetForDivider_hingeSplitType() { + final int dividerWidthPx = 100; + final SplitAttributes.SplitType splitType = + new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.RatioSplitType(0.5f)); + + final int expectedTopLeftOffset = 50; + final int expectedBottomRightOffset = 50; + + assertDividerOffsetEquals( + dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); + } + + @Test + public void testGetBoundsOffsetForDivider_expandContainersSplitType() { + final int dividerWidthPx = 100; + final SplitAttributes.SplitType splitType = + new SplitAttributes.SplitType.ExpandContainersSplitType(); + // Always return 0 for ExpandContainersSplitType as divider is not needed. + final int expectedTopLeftOffset = 0; + final int expectedBottomRightOffset = 0; + + assertDividerOffsetEquals( + dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); + } + + @Test + public void testCalculateDividerPosition() { + final MotionEvent event = mock(MotionEvent.class); + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + when(event.getRawX()).thenReturn(500f); // Touch event is in display space + assertEquals( + // Touch position is in task space is 400, then minus half of divider width. + 375, + DividerPresenter.calculateDividerPosition( + event, + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + 0 /* minPosition */, + 900 /* maxPosition */)); + + // Top-to-bottom split + when(event.getRawY()).thenReturn(1000f); // Touch event is in display space + assertEquals( + // Touch position is in task space is 800, then minus half of divider width. + 775, + DividerPresenter.calculateDividerPosition( + event, + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + 0 /* minPosition */, + 900 /* maxPosition */)); + } + + @Test + public void testCalculateMinPosition() { + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + assertEquals( + 255, /* (1000 - 100 - 50) * 0.3 */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Top-to-bottom split + assertEquals( + 525, /* (2000 - 200 - 50) * 0.3 */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Right-to-left split + assertEquals( + 170, /* (1000 - 100 - 50) * (1 - 0.8) */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + true /* isReversedLayout */)); + } + + @Test + public void testCalculateMaxPosition() { + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + assertEquals( + 680, /* (1000 - 100 - 50) * 0.8 */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Top-to-bottom split + assertEquals( + 1400, /* (2000 - 200 - 50) * 0.8 */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Right-to-left split + assertEquals( + 595, /* (1000 - 100 - 50) * (1 - 0.3) */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + true /* isReversedLayout */)); + } + + @Test + public void testCalculateNewSplitRatio_leftToRight() { + // primary=500px; secondary=500px; divider=100px; total=1100px. + final Rect taskBounds = new Rect(0, 0, 1100, 2000); + final Rect primaryBounds = new Rect(0, 0, 500, 2000); + final Rect secondaryBounds = new Rect(600, 0, 1100, 2000); + final int dividerWidthPx = 100; + + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + // Test the normal case + int dividerPosition = 300; + assertEquals( + 0.3f, // Primary is 300px after dragging. + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + true /* isVerticalSplit */, + false /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + false /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + + // Test the case when dragging to fullscreen is allowed and divider is dragged to the edge + dividerPosition = 0; + assertEquals( + DividerPresenter.RATIO_EXPANDED_SECONDARY, + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + true /* isVerticalSplit */, + false /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + true /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + + // Test the case when dragging to fullscreen is not allowed and divider is dragged to the + // edge. + dividerPosition = 0; + assertEquals( + 0.2f, // Adjusted to the minPosition 200 + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + true /* isVerticalSplit */, + false /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + false /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + } + + @Test + public void testCalculateNewSplitRatio_bottomToTop() { + // Primary is at bottom. Secondary is at top. + // primary=500px; secondary=500px; divider=100px; total=1100px. + final Rect taskBounds = new Rect(0, 0, 2000, 1100); + final Rect primaryBounds = new Rect(0, 0, 2000, 1100); + final Rect secondaryBounds = new Rect(0, 0, 2000, 500); + final int dividerWidthPx = 100; + + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + // Test the normal case + int dividerPosition = 300; + assertEquals( + // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100]. + 0.7f, + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + false /* isVerticalSplit */, + true /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + true /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + + // Test the case when dragging to fullscreen is not allowed and divider is dragged to the + // edge. + dividerPosition = 0; + assertEquals( + // The primary (bottom) container is expanded + DividerPresenter.RATIO_EXPANDED_PRIMARY, + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + false /* isVerticalSplit */, + true /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + true /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + + // Test the case when dragging to fullscreen is not allowed and divider is dragged to the + // edge. + dividerPosition = 0; + assertEquals( + // Adjusted to minPosition 200, so the primary (bottom) container is 800. + 0.8f, + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + false /* isVerticalSplit */, + true /* isReversedLayout */, + 200 /* minPosition */, + 1000 /* maxPosition */, + false /* isDraggingToFullscreenAllowed */), + 0.0001 /* delta */); + } + + private TaskFragmentContainer createMockTaskFragmentContainer( + @NonNull IBinder token, @NonNull Rect bounds) { + final TaskFragmentContainer container = mock(TaskFragmentContainer.class); + when(container.getTaskFragmentToken()).thenReturn(token); + when(container.getLastRequestedBounds()).thenReturn(bounds); + return container; + } + + private void assertDividerOffsetEquals( + int dividerWidthPx, + @NonNull SplitAttributes.SplitType splitType, + int expectedTopLeftOffset, + int expectedBottomRightOffset) { + int offset = getBoundsOffsetForDivider( + dividerWidthPx, + splitType, + CONTAINER_POSITION_LEFT + ); + assertEquals(-expectedTopLeftOffset, offset); + + offset = getBoundsOffsetForDivider( + dividerWidthPx, + splitType, + CONTAINER_POSITION_RIGHT + ); + assertEquals(expectedBottomRightOffset, offset); + + offset = getBoundsOffsetForDivider( + dividerWidthPx, + splitType, + CONTAINER_POSITION_TOP + ); + assertEquals(-expectedTopLeftOffset, offset); + + offset = getBoundsOffsetForDivider( + dividerWidthPx, + splitType, + CONTAINER_POSITION_BOTTOM + ); + assertEquals(expectedBottomRightOffset, offset); + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index dd087e8eb7c9..6f37e9cb794d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -107,7 +107,7 @@ public class JetpackTaskFragmentOrganizerTest { mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); - mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + mOrganizer.expandTaskFragment(mTransaction, container); verify(mTransaction).setWindowingMode(container.getInfo().getToken(), WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 28fbadbebe7f..b1b1984e3a70 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -177,37 +178,59 @@ public class OverlayPresentationTest { } @Test - public void testGetOverlayContainers() { - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()).isEmpty(); + public void testGetAllNonFinishingOverlayContainers() { + assertThat(mSplitController.getAllNonFinishingOverlayContainers()).isEmpty(); final TaskFragmentContainer overlayContainer1 = createTestOverlayContainer(TASK_ID, "test1"); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer1); - assertThrows( - "The exception must throw if there are two overlay containers in the same task.", - IllegalStateException.class, - () -> createTestOverlayContainer(TASK_ID, "test2")); + final TaskFragmentContainer overlayContainer2 = + createTestOverlayContainer(TASK_ID, "test2"); + + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2); final TaskFragmentContainer overlayContainer3 = createTestOverlayContainer(TASK_ID + 1, "test3"); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) - .containsExactly(overlayContainer1, overlayContainer3); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3); + + final TaskFragmentContainer finishingOverlayContainer = + createTestOverlayContainer(TASK_ID, "test4"); + spyOn(finishingOverlayContainer); + doReturn(true).when(finishingOverlayContainer).isFinished(); + + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3); + } + + @Test + public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask() { + createExistingOverlayContainers(false /* visible */); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test3"); + + assertWithMessage("overlayContainer1 is still there since it's not visible.") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer); } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() { + public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() { createExistingOverlayContainers(); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded("test3"); - assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" - + " is launched to the same task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + assertWithMessage("overlayContainer1 must be dismissed since it's visible" + + " in the same task.") + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); } @@ -222,23 +245,24 @@ public class OverlayPresentationTest { assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is launched with the same tag as an existing overlay container in a different " + "task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAndTask_updateOverlay() { + public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() { createExistingOverlayContainers(); final Rect bounds = new Rect(0, 0, 100, 100); mSplitController.setActivityStackAttributesCalculator(params -> new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - "test1"); + mOverlayContainer1.getOverlayTag(), + mOverlayContainer1.getTopNonFinishingActivity()); assertWithMessage("overlayContainer1 must be updated since the new overlay container" + " is launched with the same tag and task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer1, mOverlayContainer2); assertThat(overlayContainer).isEqualTo(mOverlayContainer1); @@ -247,6 +271,37 @@ public class OverlayPresentationTest { } @Test + public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() { + createExistingOverlayContainers(); + + final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); + final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( + mOverlayContainer1.getOverlayTag(), mActivity); + + assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + + " is associated with different launching activity") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, overlayContainer); + } + + @Test + public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissOverlay() { + createExistingOverlayContainers(false /* visible */); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test2"); + + // OverlayContainer2 is dismissed since new container is launched with the + // same tag in different task. + assertWithMessage("overlayContainer1 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, overlayContainer); + } + + @Test public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() { createExistingOverlayContainers(); @@ -257,15 +312,19 @@ public class OverlayPresentationTest { // different tag. OverlayContainer2 is dismissed since new container is launched with the // same tag in different task. assertWithMessage("overlayContainer1 and overlayContainer2 must be dismissed") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer); } private void createExistingOverlayContainers() { - mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1"); - mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2"); + createExistingOverlayContainers(true /* visible */); + } + + private void createExistingOverlayContainers(boolean visible) { + mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible); + mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); List<TaskFragmentContainer> overlayContainers = mSplitController - .getAllOverlayTaskFragmentContainers(); + .getAllNonFinishingOverlayContainers(); assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2); } @@ -294,9 +353,9 @@ public class OverlayPresentationTest { new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded("test"); - setupTaskFragmentInfo(overlayContainer, mActivity); + setupTaskFragmentInfo(overlayContainer, mActivity, true /* isVisible */); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer); assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID); assertThat(overlayContainer.areLastRequestedBoundsEqual(bounds)).isTrue(); @@ -304,14 +363,16 @@ public class OverlayPresentationTest { } @Test - public void testGetTopNonFishingTaskFragmentContainerWithOverlay() { - final TaskFragmentContainer overlayContainer = - createTestOverlayContainer(TASK_ID, "test1"); - - // Add a SplitPinContainer, the overlay should be on top + public void testGetTopNonFishingTaskFragmentContainerWithoutAssociatedOverlay() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); - + final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */, + false /* shouldAssociateWithActivity */); + overlayContainer.setIsolatedNavigationEnabled(true); final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(primaryActivity); final TaskFragmentContainer secondaryContainer = @@ -353,10 +414,10 @@ public class OverlayPresentationTest { @Test public void testGetTopNonFinishingActivityWithOverlay() { - TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); - final Activity activity = createMockActivity(); final TaskFragmentContainer container = createMockTaskFragmentContainer(activity); + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, + "test1"); final TaskContainer task = container.getTaskContainer(); assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)) @@ -373,8 +434,9 @@ public class OverlayPresentationTest { } @Test - public void testUpdateOverlayContainer_dismissOverlayIfNeeded() { - TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + public void testUpdateOverlayContainer_dismissNonAssociatedOverlayIfNeeded() { + TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); mSplitController.updateOverlayContainer(mTransaction, overlayContainer); @@ -443,11 +505,10 @@ public class OverlayPresentationTest { @Test public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() { - final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); final TaskContainer taskContainer = overlayContainer.getTaskContainer(); - assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); - spyOn(taskContainer); final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( @@ -463,16 +524,15 @@ public class OverlayPresentationTest { assertWithMessage("The overlay container must still be dismissed even if " + "#updateContainer is not called") - .that(taskContainer.getOverlayContainer()).isNull(); + .that(taskContainer.getTaskFragmentContainers()).isEmpty(); } @Test - public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() { - final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissNonAssocOverlay() { + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); final TaskContainer taskContainer = overlayContainer.getTaskContainer(); - assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); - spyOn(taskContainer); final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( @@ -487,7 +547,7 @@ public class OverlayPresentationTest { assertWithMessage("The overlay container must still be dismissed even if " + "#updateContainer is not called") - .that(taskContainer.getOverlayContainer()).isNull(); + .that(taskContainer.getTaskFragmentContainers()).isEmpty(); } @Test @@ -510,7 +570,7 @@ public class OverlayPresentationTest { } @Test - public void testApplyActivityStackAttributesForOverlayContainer() { + public void testApplyActivityStackAttributesForOverlayContainerAssociatedWithActivity() { final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG); final IBinder token = container.getTaskFragmentToken(); final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() @@ -527,7 +587,35 @@ public class OverlayPresentationTest { WINDOWING_MODE_MULTI_WINDOW); verify(mSplitPresenter).updateAnimationParams(mTransaction, token, TaskFragmentAnimationParams.DEFAULT); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true); + // Set isolated navigation to false if the overlay container is associated with + // the launching activity. + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); + verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true); + } + + @Test + public void testApplyActivityStackAttributesForOverlayContainerWithoutAssociatedActivity() { + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG, + true, /* isVisible */ false /* associatedWithLaunchingActivity */); + final IBinder token = container.getTaskFragmentToken(); + final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() + .setRelativeBounds(new Rect(0, 0, 200, 200)) + .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK)) + .build(); + + mSplitPresenter.applyActivityStackAttributes(mTransaction, container, + attributes, null /* minDimensions */); + + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, + attributes.getRelativeBounds()); + verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, + container, WINDOWING_MODE_MULTI_WINDOW); + verify(mSplitPresenter).updateAnimationParams(mTransaction, token, + TaskFragmentAnimationParams.DEFAULT); + // Set isolated navigation to false if the overlay container is associated with + // the launching activity. + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, + container, true); verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true); } @@ -563,8 +651,7 @@ public class OverlayPresentationTest { mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, new Size(relativeBounds.width() + 1, relativeBounds.height())); - verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, - new Rect()); + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, new Rect()); verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, WINDOWING_MODE_UNDEFINED); verify(mSplitPresenter).updateAnimationParams(mTransaction, token, @@ -573,16 +660,65 @@ public class OverlayPresentationTest { verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); } + @Test + public void testFinishSelfWithActivityIfNeeded() { + TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + + container.finishSelfWithActivityIfNeeded(mTransaction, mActivity.getActivityToken()); + + verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean()); + + TaskFragmentContainer overlayWithoutAssociation = createTestOverlayContainer(TASK_ID, + "test", false /* associateLaunchingActivity */); + + overlayWithoutAssociation.finishSelfWithActivityIfNeeded(mTransaction, + mActivity.getActivityToken()); + + verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean()); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .contains(overlayWithoutAssociation); + + TaskFragmentContainer overlayWithAssociation = + createOrUpdateOverlayTaskFragmentIfNeeded("test"); + overlayWithAssociation.setInfo(mTransaction, createMockTaskFragmentInfo( + overlayWithAssociation, mActivity, true /* isVisible */)); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .contains(overlayWithAssociation); + clearInvocations(mSplitPresenter); + + overlayWithAssociation.finishSelfWithActivityIfNeeded(mTransaction, new Binder()); + + verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean()); + + overlayWithAssociation.finishSelfWithActivityIfNeeded(mTransaction, + mActivity.getActivityToken()); + + verify(mSplitPresenter).cleanupContainer(mTransaction, overlayWithAssociation, false); + + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .doesNotContain(overlayWithAssociation); + } + /** - * A simplified version of {@link SplitController.ActivityStartMonitor - * #createOrUpdateOverlayTaskFragmentIfNeeded} + * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} */ @Nullable private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(@NonNull String tag) { final Bundle launchOptions = new Bundle(); launchOptions.putString(KEY_OVERLAY_TAG, tag); + return createOrUpdateOverlayTaskFragmentIfNeeded(tag, mActivity); + } + + /** + * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} + */ + @Nullable + private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( + @NonNull String tag, @NonNull Activity activity) { + final Bundle launchOptions = new Bundle(); + launchOptions.putString(KEY_OVERLAY_TAG, tag); return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, - launchOptions, mIntent, mActivity); + launchOptions, mIntent, activity); } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @@ -590,23 +726,41 @@ public class OverlayPresentationTest { private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { final TaskFragmentContainer container = mSplitController.newContainer(activity, activity.getTaskId()); - setupTaskFragmentInfo(container, activity); + setupTaskFragmentInfo(container, activity, false /* isVisible */); return container; } @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) { + return createTestOverlayContainer(taskId, tag, false /* isVisible */, + true /* associateLaunchingActivity */); + } + + @NonNull + private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, + boolean isVisible) { + return createTestOverlayContainer(taskId, tag, isVisible, + true /* associateLaunchingActivity */); + } + + // TODO(b/243518738): add more test coverage on overlay container without activity association + // once we have use cases. + @NonNull + private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, + boolean isVisible, boolean associateLaunchingActivity) { Activity activity = createMockActivity(); TaskFragmentContainer overlayContainer = mSplitController.newContainer( null /* pendingAppearedActivity */, mIntent, activity, taskId, - null /* pairedPrimaryContainer */, tag, Bundle.EMPTY); - setupTaskFragmentInfo(overlayContainer, activity); + null /* pairedPrimaryContainer */, tag, Bundle.EMPTY, + associateLaunchingActivity); + setupTaskFragmentInfo(overlayContainer, activity, isVisible); return overlayContainer; } private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, - @NonNull Activity activity) { - final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); + @NonNull Activity activity, + boolean isVisible) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isVisible); container.setInfo(mTransaction, info); mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 00f8b5925d66..3441c2b26ea8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -72,6 +72,8 @@ import static org.mockito.Mockito.times; import android.annotation.NonNull; import android.app.Activity; import android.app.ActivityOptions; +import android.app.ActivityThread; +import android.app.servertransaction.ClientTransactionListenerController; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -83,9 +85,11 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import android.view.WindowInsets; import android.view.WindowMetrics; +import android.window.ActivityWindowInfo; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentParentInfo; @@ -99,7 +103,10 @@ import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; +import com.android.window.flags.Flags; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -110,6 +117,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -127,6 +136,9 @@ public class SplitControllerTest { private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( new ComponentName("test", "placeholder")); + @Rule + public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + private Activity mActivity; @Mock private Resources mActivityResources; @@ -138,6 +150,13 @@ public class SplitControllerTest { private Handler mHandler; @Mock private WindowLayoutComponentImpl mWindowLayoutComponent; + @Mock + private ActivityWindowInfo mActivityWindowInfo; + @Mock + private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener; + @Mock + private androidx.window.extensions.core.util.function.Consumer<EmbeddedActivityWindowInfo> + mEmbeddedActivityWindowInfoCallback; private SplitController mSplitController; private SplitPresenter mSplitPresenter; @@ -208,13 +227,13 @@ public class SplitControllerTest { // When the activity is not finishing, do not clear the record. doReturn(false).when(mActivity).isFinishing(); - mSplitController.onActivityDestroyed(mActivity); + mSplitController.onActivityDestroyed(mTransaction, mActivity); assertTrue(tf.hasActivity(mActivity.getActivityToken())); // Clear the record when the activity is finishing and destroyed. doReturn(true).when(mActivity).isFinishing(); - mSplitController.onActivityDestroyed(mActivity); + mSplitController.onActivityDestroyed(mTransaction, mActivity); assertFalse(tf.hasActivity(mActivity.getActivityToken())); } @@ -275,7 +294,10 @@ public class SplitControllerTest { doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); - doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); + final SplitRule splitRule = createSplitRule(mActivity, mActivity); + doReturn(splitRule).when(splitContainer).getSplitRule(); + doReturn(splitRule.getDefaultSplitAttributes()) + .when(splitContainer).getDefaultSplitAttributes(); taskContainer = mSplitController.getTaskContainer(TASK_ID); taskContainer.addSplitContainer(splitContainer); // Add a mock SplitContainer on top of splitContainer @@ -590,7 +612,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); + anyString(), any(), anyBoolean()); } @Test @@ -620,7 +642,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container); } @Test @@ -753,7 +775,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); + anyString(), any(), anyBoolean()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -796,7 +818,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); + anyString(), any(), anyBoolean()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -1529,6 +1551,73 @@ public class SplitControllerTest { .getTopNonFinishingActivity(), secondaryActivity); } + @Test + public void testIsActivityEmbedded() { + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); + + assertFalse(mSplitController.isActivityEmbedded(mActivity)); + + doReturn(true).when(mActivityWindowInfo).isEmbedded(); + + assertTrue(mSplitController.isActivityEmbedded(mActivity)); + } + + @Test + public void testGetEmbeddedActivityWindowInfo() { + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); + + final boolean isEmbedded = true; + final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration + .getBounds(); + final Rect taskBounds = new Rect(0, 0, 1000, 2000); + final Rect activityStackBounds = new Rect(0, 0, 500, 2000); + doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded(); + doReturn(taskBounds).when(mActivityWindowInfo).getTaskBounds(); + doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds(); + + final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity, + isEmbedded, activityBounds, taskBounds, activityStackBounds); + assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity)); + } + + @Test + public void testSetEmbeddedActivityWindowInfoCallback() { + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); + + final ClientTransactionListenerController controller = ClientTransactionListenerController + .getInstance(); + spyOn(controller); + doNothing().when(controller).registerActivityWindowInfoChangedListener(any()); + doReturn(mActivityWindowInfoListener).when(mSplitController) + .getActivityWindowInfoListener(); + final Executor executor = Runnable::run; + + // Register to ClientTransactionListenerController + mSplitController.setEmbeddedActivityWindowInfoCallback(executor, + mEmbeddedActivityWindowInfoCallback); + + verify(controller).registerActivityWindowInfoChangedListener(mActivityWindowInfoListener); + verify(mEmbeddedActivityWindowInfoCallback, never()).accept(any()); + + // Test onActivityWindowInfoChanged triggered. + mSplitController.onActivityWindowInfoChanged(mActivity.getActivityToken(), + mActivityWindowInfo); + + verify(mEmbeddedActivityWindowInfoCallback).accept(any()); + + // Unregister to ClientTransactionListenerController + mSplitController.clearEmbeddedActivityWindowInfoCallback(); + + verify(controller).unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener); + + // Test onActivityWindowInfoChanged triggered as no-op after clear callback. + clearInvocations(mEmbeddedActivityWindowInfoCallback); + mSplitController.onActivityWindowInfoChanged(mActivity.getActivityToken(), + mActivityWindowInfo); + + verify(mEmbeddedActivityWindowInfoCallback, never()).accept(any()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { return createMockActivity(TASK_ID); @@ -1537,13 +1626,17 @@ public class SplitControllerTest { /** Creates a mock activity in the organizer process. */ private Activity createMockActivity(int taskId) { final Activity activity = mock(Activity.class); + final ActivityThread.ActivityClientRecord activityClientRecord = + mock(ActivityThread.ActivityClientRecord.class); doReturn(mActivityResources).when(activity).getResources(); final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); + doReturn(activityClientRecord).when(mSplitController).getActivityClientRecord(activity); doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); + doReturn(mActivityWindowInfo).when(activityClientRecord).getActivityWindowInfo(); return activity; } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 941b4e1c3e41..62d8aa30a576 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -665,8 +665,8 @@ public class SplitPresenterTest { assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); @@ -675,8 +675,8 @@ public class SplitPresenterTest { splitContainer, mActivity, null /* secondaryActivity */, new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class))); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); } @Test diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index cc00a49604ee..0af41791cf4a 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -402,7 +402,7 @@ public class TaskFragmentContainerTest { assertTrue(container.hasActivity(mActivity.getActivityToken())); - taskContainer.onActivityDestroyed(mActivity.getActivityToken()); + taskContainer.onActivityDestroyed(mTransaction, mActivity.getActivityToken()); // It should not contain the destroyed Activity. assertFalse(container.hasActivity(mActivity.getActivityToken())); diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 8829d1b9e0e1..5c978e21b9bd 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -45,7 +45,6 @@ filegroup { name: "wm_shell_util-sources", srcs: [ "src/com/android/wm/shell/animation/Interpolators.java", - "src/com/android/wm/shell/animation/PhysicsAnimator.kt", "src/com/android/wm/shell/common/bubbles/*.kt", "src/com/android/wm/shell/common/bubbles/*.java", "src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt", @@ -166,10 +165,28 @@ java_library { }, } +filegroup { + name: "wm_shell-shared-aidls", + + srcs: [ + "shared/**/*.aidl", + ], + + path: "shared/src", +} + java_library { name: "WindowManager-Shell-shared", - srcs: ["shared/**/*.java"], + srcs: [ + "shared/**/*.java", + "shared/**/*.kt", + ":wm_shell-shared-aidls", + ], + static_libs: [ + "androidx.dynamicanimation_dynamicanimation", + "jsr330", + ], } android_library { @@ -193,7 +210,7 @@ android_library { "androidx.recyclerview_recyclerview", "kotlinx-coroutines-android", "kotlinx-coroutines-core", - "iconloader_base", + "//frameworks/libs/systemui:iconloader_base", "com_android_wm_shell_flags_lib", "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 36d3313a9f3b..7a986835359a 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -23,4 +23,12 @@ <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> + + <application> + <activity + android:name=".desktopmode.DesktopWallpaperActivity" + android:excludeFromRecents="true" + android:launchMode="singleInstance" + android:theme="@style/DesktopWallpaperTheme" /> + </application> </manifest> diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp index 1a98ffcea9e7..7f8f57b172ff 100644 --- a/libs/WindowManager/Shell/aconfig/Android.bp +++ b/libs/WindowManager/Shell/aconfig/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "com_android_wm_shell_flags", package: "com.android.wm.shell", + container: "system", srcs: [ "multitasking.aconfig", ], diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index b61dda4c4e53..7ff204c695f8 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -1,4 +1,5 @@ package: "com.android.wm.shell" +container: "system" flag { name: "enable_app_pairs" diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 1686d0d54dc4..1ad19c9f3033 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -46,6 +46,7 @@ android_robolectric_test { exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], static_libs: [ "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", @@ -64,6 +65,7 @@ android_test { static_libs: [ "WindowManager-Shell", "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 9cd14fca6a9d..8487e3792993 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles import android.content.Context import android.content.Intent import android.content.pm.ShortcutInfo +import android.content.res.Resources import android.graphics.Insets import android.graphics.PointF import android.graphics.Rect @@ -26,8 +27,10 @@ import android.view.WindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT +import com.android.wm.shell.common.bubbles.BubbleBarLocation import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.Before @@ -41,6 +44,9 @@ class BubblePositionerTest { private lateinit var positioner: BubblePositioner private val context = ApplicationProvider.getApplicationContext<Context>() + private val resources: Resources + get() = context.resources + private val defaultDeviceConfig = DeviceConfig( windowBounds = Rect(0, 0, 1000, 2000), @@ -53,6 +59,7 @@ class BubblePositionerTest { @Before fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false val windowManager = context.getSystemService(WindowManager::class.java) positioner = BubblePositioner(context, windowManager) } @@ -166,8 +173,9 @@ class BubblePositionerTest { @Test fun testGetRestingPosition_afterBoundsChange() { - positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, - windowBounds = Rect(0, 0, 2000, 1600))) + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) + ) // Set the resting position to the right side var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) @@ -175,8 +183,9 @@ class BubblePositionerTest { positioner.restingPosition = restingPosition // Now make the device smaller - positioner.update(defaultDeviceConfig.copy(isLargeScreen = false, - windowBounds = Rect(0, 0, 1000, 1600))) + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) + ) // Check the resting position is on the correct side allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) @@ -200,6 +209,58 @@ class BubblePositionerTest { } @Test + fun testBubbleBarExpandedViewHeightAndWidth() { + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + val bubbleBarBounds = Rect(1700, 2500, 1780, 2600) + + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = bubbleBarBounds + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 + val expandedViewVerticalSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing + val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width) + + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() { + val screenWidth = 300 + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, screenWidth, 2600) + ) + val bubbleBarBounds = Rect(100, 2500, 280, 2550) + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = bubbleBarBounds + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 + val expandedViewSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing + val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test fun testGetExpandedViewHeight_max() { val deviceConfig = defaultDeviceConfig.copy( @@ -235,7 +296,8 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, - directExecutor()) {} + directExecutor() + ) {} // Ensure the height is the same as the desired value assertThat(positioner.getExpandedViewHeight(bubble)) @@ -262,7 +324,8 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, - directExecutor()) {} + directExecutor() + ) {} // Ensure the height is the same as the desired value val minHeight = @@ -470,20 +533,106 @@ class BubblePositionerTest { fun testGetTaskViewContentWidth_onLeft() { positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) - val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */, - false /* isOverflow */) - assertThat(taskViewWidth).isEqualTo( - positioner.screenRect.width() - paddings[0] - paddings[2]) + val paddings = + positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) } @Test fun testGetTaskViewContentWidth_onRight() { positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) - val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */, - false /* isOverflow */) - assertThat(taskViewWidth).isEqualTo( - positioner.screenRect.width() - paddings[0] - paddings[2]) + val paddings = + positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testIsBubbleBarOnLeft_defaultsToRight() { + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) + } + + private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { + positioner.setShowingInBubbleBar(true) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 2000, 2600) + ) + positioner.update(deviceConfig) + + positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig) + + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + + val left: Int + val right: Int + if (onLeft) { + // Pin to the left, calculate right + left = deviceConfig.insets.left + expandedViewPadding + right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } else { + // Pin to the right, calculate left + right = + deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding + left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } + // Above the bubble bar + val bottom = positioner.bubbleBarBounds.top - expandedViewPadding + // Calculate right and top based on size + val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) + val expectedBounds = Rect(left, top, right, bottom) + + val bounds = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) + + assertThat(bounds).isEqualTo(expectedBounds) } private val defaultYPosition: Float @@ -517,4 +666,21 @@ class BubblePositionerTest { positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent } + + private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect { + val width = 200 + val height = 100 + val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom + val top = bottom - height + val left: Int + val right: Int + if (onLeft) { + left = deviceConfig.insets.left + right = left + width + } else { + right = deviceConfig.windowBounds.right - deviceConfig.insets.right + left = right - width + } + return Rect(left, top, right, bottom) + } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 8989fc543044..35a4a627c0d1 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -18,27 +18,32 @@ package com.android.wm.shell.bubbles import android.content.Context import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Icon import android.os.UserHandle import android.view.IWindowManager import android.view.WindowManager import android.view.WindowManagerGlobal -import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.common.ProtoLog import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.R import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.After import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -64,6 +69,7 @@ class BubbleStackViewTest { @Before fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false windowManager = WindowManagerGlobal.getWindowManagerService()!! @@ -104,34 +110,158 @@ class BubbleStackViewTest { { sysuiProxy }, shellExecutor ) + + context + .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit() + .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true) + .apply() + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() } - @UiThreadTest @Test fun addBubble() { val bubble = createAndInflateBubble() - bubbleStackView.addBubble(bubble) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleStackView.bubbleCount).isEqualTo(1) } - @UiThreadTest @Test fun tapBubbleToExpand() { val bubble = createAndInflateBubble() - bubbleStackView.addBubble(bubble) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble.iconView!!.performClick() + // we're checking the expanded state in BubbleData because that's the source of truth. + // This will eventually propagate an update back to the stack view, but setting the + // entire pipeline is outside the scope of a unit test. + assertThat(bubbleData.isExpanded).isTrue() + } - bubble.iconView!!.performClick() - // we're checking the expanded state in BubbleData because that's the source of truth. This - // will eventually propagate an update back to the stack view, but setting the entire - // pipeline is outside the scope of a unit test. - assertThat(bubbleData.isExpanded).isTrue() + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate).isNotNull() + assertThat(lastUpdate!!.expandedChanged).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + } + + @Test + fun tapDifferentBubble_shouldReorder() { + val bubble1 = createAndInflateChatBubble(key = "bubble1") + val bubble2 = createAndInflateChatBubble(key = "bubble2") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble1) + bubbleStackView.addBubble(bubble2) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(bubbleStackView.bubbleCount).isEqualTo(2) + assertThat(bubbleData.bubbles).hasSize(2) + assertThat(bubbleData.selectedBubble).isEqualTo(bubble2) + assertThat(bubble2.iconView).isNotNull() + + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble2.iconView!!.performClick() + assertThat(bubbleData.isExpanded).isTrue() + + bubbleStackView.setSelectedBubble(bubble2) + bubbleStackView.isExpanded = true + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble2", "bubble1") + .inOrder() + + // wait for idle to allow the animation to start + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + // wait for the expansion animation to complete before interacting with the bubbles + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y) + + // tap on bubble1 to select it + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble1.iconView!!.performClick() + } + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + + // tap on bubble1 again to collapse the stack + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // we have to set the selected bubble in the stack view manually because we don't have a + // listener wired up. + bubbleStackView.setSelectedBubble(bubble1) + bubble1.iconView!!.performClick() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + assertThat(bubbleData.isExpanded).isFalse() + assertThat(lastUpdate!!.orderChanged).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble1", "bubble2") + .inOrder() + } + + private fun createAndInflateChatBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 6, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor() + ) {} + inflateBubble(bubble) + return bubble } private fun createAndInflateBubble(): Bubble { val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + inflateBubble(bubble) + return bubble + } + + private fun inflateBubble(bubble: Bubble) { bubble.setInflateSynchronously(true) bubbleData.notificationEntryUpdated(bubble, true, false) @@ -152,7 +282,6 @@ class BubbleStackViewTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() assertThat(bubble.isInflated).isTrue() - return bubble } private class FakeBubbleStackViewManager : BubbleStackViewManager { @@ -176,7 +305,7 @@ class BubbleStackViewTest { r.run() } - override fun removeCallbacks(r: Runnable) {} + override fun removeCallbacks(r: Runnable?) {} override fun hasCallback(r: Runnable): Boolean = false } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt new file mode 100644 index 000000000000..e1bf40ca19dc --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.bar.BubbleExpandedViewPinController.Companion.DROP_TARGET_SCALE +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleExpandedViewPinController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleExpandedViewPinControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + + const val BUBBLE_BAR_WIDTH = 100 + const val BUBBLE_BAR_HEIGHT = 50 + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + private lateinit var controller: BubbleExpandedViewPinController + private lateinit var testListener: TestLocationChangeListener + + private val pointOnLeft = PointF(100f, 100f) + private val pointOnRight = PointF(1900f, 500f) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = + Rect( + SCREEN_WIDTH - deviceConfig.insets.right - BUBBLE_BAR_WIDTH, + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT, + SCREEN_WIDTH - deviceConfig.insets.right, + SCREEN_HEIGHT - deviceConfig.insets.bottom + ) + + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + @Test + fun onDragUpdate_stayOnSameSide() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + } + + @Test + fun onDragUpdate_toLeft() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = true) + assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width()) + assertThat(dropTargetView!!.layoutParams.height) + .isEqualTo(expectedDropTargetBounds.height()) + + assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT) + } + + @Test + fun onDragUpdate_toLeftAndBackToRight() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) } + // We have to wait for existing drop target to animate out and new to animate in + waitForAnimateOut() + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = false) + assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width()) + assertThat(dropTargetView!!.layoutParams.height) + .isEqualTo(expectedDropTargetBounds.height()) + + assertThat(testListener.locationChanges) + .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT) + } + + @Test + fun onDragUpdate_toLeftInExclusionRect() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + } + + @Test + fun toggleSetDropTargetHidden_dropTargetExists() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + runOnMainSync { controller.setDropTargetHidden(true) } + waitForAnimateOut() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + runOnMainSync { controller.setDropTargetHidden(false) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + } + + @Test + fun toggleSetDropTargetHidden_noDropTarget() { + runOnMainSync { controller.setDropTargetHidden(true) } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + + runOnMainSync { controller.setDropTargetHidden(false) } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + } + + @Test + fun onDragEnd_dropTargetExists() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + } + + @Test + fun onDragEnd_noDropTarget() { + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + } + + private val dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect { + val rect = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect) + // Scale the rect to expected size, but keep the center point the same + val centerX = rect.centerX() + val centerY = rect.centerY() + rect.scale(DROP_TARGET_SCALE) + rect.offset(centerX - rect.centerX(), centerY - rect.centerY()) + return rect + } + + private fun runOnMainSync(runnable: Runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) } + } + + internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { + val locationChanges = mutableListOf<BubbleBarLocation>() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + } +} diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml index 1f3e3a4c5b22..ab1ab984fd5f 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml +++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml @@ -1,6 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -14,7 +13,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@android:color/white" /> -</shape> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml index 65f5239737b2..640d184e641c 100644 --- a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml +++ b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml @@ -14,15 +14,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <item android:state_pressed="true" - android:color="@color/desktop_mode_maximize_menu_button_on_hover"/> - <item android:state_hovered="true" - android:color="@color/desktop_mode_maximize_menu_button_on_hover"/> + android:color="?androidprv:attr/colorAccentPrimary"/> <item android:state_focused="true" - android:color="@color/desktop_mode_maximize_menu_button_on_hover"/> + android:color="?androidprv:attr/colorAccentPrimary"/> <item android:state_selected="true" - android:color="@color/desktop_mode_maximize_menu_button_on_hover"/> - <item android:color="@color/desktop_mode_maximize_menu_button"/> + android:color="?androidprv:attr/colorAccentPrimary"/> + <item android:color="?androidprv:attr/materialColorOutlineVariant"/> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml deleted file mode 100644 index 86679af5428b..000000000000 --- a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 ---> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" - android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/> - <item android:state_hovered="true" - android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/> - <item android:state_focused="true" - android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/> - <item android:state_selected="true" - android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/> - <item android:color="@color/desktop_mode_maximize_menu_button_outline"/> -</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml new file mode 100644 index 000000000000..9dcde3b54421 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" /> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="?androidprv:attr/materialColorPrimaryContainer" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml index 948264579e1d..294b1f0e21fd 100644 --- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml +++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml @@ -25,7 +25,7 @@ <shape android:shape="ring" android:thickness="3dp" - android:innerRadius="17dp" + android:innerRadius="14dp" android:useLevel="true"> </shape> </rotate> diff --git a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml index 02b707568cd0..e5fe1b5431eb 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml @@ -15,12 +15,12 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="48dp" - android:height="48dp" + android:width="24dp" + android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960"> <path - android:fillColor="@android:color/white" - android:pathData="M180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L780,120Q804,120 822,138Q840,156 840,180L840,780Q840,804 822,822Q804,840 780,840L180,840ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,277L180,277L180,780Q180,780 180,780Q180,780 180,780Z" /> + android:fillColor="@android:color/black" + android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320L160,320L160,720Q160,720 160,720Q160,720 160,720Z"/> </vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml deleted file mode 100644 index 7c4f49979455..000000000000 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="20dp" - android:height="20dp" - android:viewportWidth="20" - android:viewportHeight="20"> - <path - android:pathData="M15.701,14.583L18.567,17.5L17.425,18.733L14.525,15.833L12.442,17.917V12.5H17.917L15.701,14.583ZM15.833,5.833H17.5V7.5H15.833V5.833ZM17.5,4.167H15.833V2.567C16.75,2.567 17.5,3.333 17.5,4.167ZM12.5,2.5H14.167V4.167H12.5V2.5ZM15.833,9.167H17.5V10.833H15.833V9.167ZM7.5,17.5H5.833V15.833H7.5V17.5ZM4.167,7.5H2.5V5.833H4.167V7.5ZM4.167,2.567V4.167H2.5C2.5,3.333 3.333,2.567 4.167,2.567ZM4.167,14.167H2.5V12.5H4.167V14.167ZM7.5,4.167H5.833V2.5H7.5V4.167ZM10.833,4.167H9.167V2.5H10.833V4.167ZM10.833,17.5H9.167V15.833H10.833V17.5ZM4.167,10.833H2.5V9.167H4.167V10.833ZM4.167,17.567C3.25,17.567 2.5,16.667 2.5,15.833H4.167V17.567Z" - android:fillColor="#1C1C14"/> -</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_maximize_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_button_background.xml index bfb0dd7f3100..ed51498dfe24 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_maximize_button_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_button_background.xml @@ -19,6 +19,5 @@ android:shape="rectangle"> <solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/> <corners - android:radius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"/> - <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/> + android:radius="@dimen/desktop_mode_maximize_menu_buttons_radius"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_right_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml index 7bd6e9981c12..04ad572e046f 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_right_button_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- ~ Copyright (C) 2023 The Android Open Source Project ~ @@ -17,12 +16,9 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle"> - <solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/> <corners - android:topLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius" - android:topRightRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius" - android:bottomLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius" - android:bottomRightRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"/> - <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/> + android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/> + <stroke android:width="1dp" android:color="?androidprv:attr/materialColorOutlineVariant"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_left_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml index 6630fcab4794..86da9feacc49 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_left_button_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- ~ Copyright (C) 2023 The Android Open Source Project ~ @@ -17,12 +16,9 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle"> - <solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/> <corners - android:topLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius" - android:topRightRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius" - android:bottomLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius" - android:bottomRightRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius"/> - <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/> + android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/> + <stroke android:width="1dp" android:color="?androidprv:attr/colorAccentPrimary"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_drop_target.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_drop_target.xml new file mode 100644 index 000000000000..9d29f7da8797 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_drop_target.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<View xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_bar_drop_target" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/bubble_drop_target_background" + android:elevation="@dimen/bubble_elevation" + android:importantForAccessibility="no" /> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml index a5605a7ff50a..fa18e2bc5add 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml @@ -80,11 +80,14 @@ <com.android.wm.shell.windowdecor.MaximizeButtonView android:id="@+id/maximize_button_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="44dp" + android:layout_height="40dp" android:layout_gravity="end" + android:layout_marginHorizontal="8dp" + android:paddingHorizontal="5dp" + android:paddingVertical="3dp" android:clickable="true" - android:focusable="true" /> + android:focusable="true"/> <ImageButton android:id="@+id/close_window" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml index ef7478c04dda..c0ff1922edc8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml @@ -22,14 +22,15 @@ android:layout_height="wrap_content" android:gravity="center_horizontal"> - <ImageButton + <com.android.wm.shell.windowdecor.HandleImageButton android:id="@+id/caption_handle" android:layout_width="@dimen/desktop_mode_fullscreen_decor_caption_width" android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height" android:paddingVertical="16dp" + android:paddingHorizontal="10dp" android:contentDescription="@string/handle_text" android:src="@drawable/decor_handle_dark" tools:tint="@color/desktop_mode_caption_handle_bar_dark" android:scaleType="fitXY" - android:background="?android:selectableItemBackground"/> + android:background="@android:color/transparent"/> </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml index a4bbd8998cc5..147f99144b1d 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml @@ -16,13 +16,12 @@ --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/desktop_mode_resize_veil_background"> + android:layout_height="match_parent"> <ImageView android:id="@+id/veil_application_icon" - android:layout_width="96dp" - android:layout_height="96dp" + android:layout_width="@dimen/desktop_mode_resize_veil_icon_size" + android:layout_height="@dimen/desktop_mode_resize_veil_icon_size" android:layout_gravity="center" android:contentDescription="@string/app_icon_text" /> </FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index c6f85a0b4ed4..d5724cc6a420 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -52,7 +52,7 @@ android:textStyle="normal" android:layout_weight="1"/> - <ImageButton + <com.android.wm.shell.windowdecor.HandleMenuImageButton android:id="@+id/collapse_menu_button" android:layout_width="32dp" android:layout_height="32dp" @@ -134,15 +134,6 @@ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot" android:drawableTint="?androidprv:attr/materialColorOnSurface" style="@style/DesktopModeHandleMenuActionButton"/> - - <Button - android:id="@+id/select_button" - android:contentDescription="@string/select_text" - android:text="@string/select_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_select" - android:drawableTint="?androidprv:attr/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton"/> - </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index dbfd6e5d8d94..9f0a425a82f8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -15,41 +15,87 @@ ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" style="?android:attr/buttonBarStyle" android:layout_width="@dimen/desktop_mode_maximize_menu_width" android:layout_height="@dimen/desktop_mode_maximize_menu_height" android:orientation="horizontal" android:gravity="center" + android:padding="16dp" android:background="@drawable/desktop_mode_maximize_menu_background"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> - <Button - android:id="@+id/maximize_menu_maximize_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="120dp" - android:layout_height="80dp" - android:layout_marginRight="15dp" - android:color="@color/desktop_mode_maximize_menu_button" - android:background="@drawable/desktop_mode_maximize_menu_maximize_button_background" - android:stateListAnimator="@null"/> + <FrameLayout + android:id="@+id/maximize_menu_maximize_button_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/desktop_mode_maximize_menu_layout_background" + android:padding="4dp" + android:layout_marginRight="8dp" + android:layout_marginBottom="4dp"> + <Button + android:id="@+id/maximize_menu_maximize_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="86dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + </FrameLayout> - <Button - android:id="@+id/maximize_menu_snap_left_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="58dp" - android:layout_height="80dp" - android:layout_marginRight="6dp" - android:color="@color/desktop_mode_maximize_menu_button" - android:background="@drawable/desktop_mode_maximize_menu_snap_left_button_background" - android:stateListAnimator="@null"/> + <TextView + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_maximize_text" + android:textColor="?androidprv:attr/materialColorOnSurface"/> + </LinearLayout> - <Button - android:id="@+id/maximize_menu_snap_right_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="58dp" - android:layout_height="80dp" - android:color="@color/desktop_mode_maximize_menu_button" - android:background="@drawable/desktop_mode_maximize_menu_snap_right_button_background" - android:stateListAnimator="@null"/> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/maximize_menu_snap_menu_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp" + android:background="@drawable/desktop_mode_maximize_menu_layout_background" + android:layout_marginBottom="4dp"> + <Button + android:id="@+id/maximize_menu_snap_left_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:layout_marginRight="4dp" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + + <Button + android:id="@+id/maximize_menu_snap_right_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + </LinearLayout> + <TextView + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:layout_gravity="center" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_snap_text" + android:textColor="?androidprv:attr/materialColorOnSurface"/> + </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml index e0057fe64fd2..296c89568386 100644 --- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml @@ -20,16 +20,16 @@ android:id="@+id/progress_bar" style="?android:attr/progressBarStyleHorizontal" android:progressDrawable="@drawable/circular_progress" - android:layout_width="40dp" - android:layout_height="40dp" + android:layout_width="34dp" + android:layout_height="34dp" android:indeterminate="false" android:visibility="invisible"/> <ImageButton android:id="@+id/maximize_window" - android:layout_width="40dp" - android:layout_height="40dp" - android:padding="9dp" + android:layout_width="34dp" + android:layout_height="34dp" + android:padding="5dp" android:contentDescription="@string/maximize_button_text" android:tint="?androidprv:attr/materialColorOnSurface" android:background="?android:selectableItemBackgroundBorderless" diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index dd6f8455f82a..b9ff5c682b42 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Maak toe"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys toe"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Maak kieslys oop"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimeer skerm"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gryp skerm vas"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 18a4ccf5c16d..81ab3ab15aad 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ዝጋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ምናሌን ክፈት"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"የማያ ገጹ መጠን አሳድግ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ማያ ገጹን አሳድግ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 7ca335e4a655..3974c39d4803 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"إغلاق"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</string> <string name="expand_menu_text" msgid="3847736164494181168">"فتح القائمة"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"التقاط صورة للشاشة"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 944c4f25bf2a..a1ce1b3b9513 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</string> <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খোলক"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্ৰীন মেক্সিমাইজ কৰক"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্ৰীন স্নেপ কৰক"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index c320e415d604..71dfe5ac6bed 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Bağlayın"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu bağlayın"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menyunu açın"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı maksimum böyüdün"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranı çəkin"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index 19ca4d300b7e..f48360991d49 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otvorite meni"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Povećaj ekran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Uklopi ekran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 74ae1d7637d0..81d066f82261 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Закрыць"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Адкрыць меню"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Разгарнуць на ўвесь экран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Размясціць на палавіне экрана"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 1b753f5359ba..8f828badcf47 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Затваряне"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Отваряне на менюто"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Увеличаване на екрана"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Прилепване на екрана"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 2ea22cc1455a..e0a2ea824be0 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</string> <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খুলুন"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্রিনে অ্যাপ মানানসই হিসেবে ছোট বড় করুন"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 13655b3c5c85..41c72c1d3a03 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje menija"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snimi ekran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index cb897c5b612d..679227248ea5 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Tanca"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Obre el menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximitza la pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajusta la pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index ded2707afd1d..aafb2e16b703 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zavřít"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otevřít nabídku"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 2bdb29d67447..8878910a4d2c 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Luk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Åbn menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimér skærm"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tilpas skærm"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index e99d9d0c269a..7b5c471e074f 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Schließen"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menü öffnen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Bildschirm maximieren"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Bildschirm teilen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index d8bb740535fc..14e5e2f87ab8 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Άνοιγμα μενού"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 2525b321d9d7..cb9ee4f6b6b3 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 0623bef925e2..8498807f9fdb 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 9fe77ddf7e28..406c1f37c455 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir el menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index b88f215eb54e..0583d79da127 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 529b6d10b3c6..70547f566ea6 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sule"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ava menüü"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Kuva täisekraanil"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Kuva poolel ekraanil"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 7438f4240eae..4be35eac6c1f 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Itxi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ireki menua"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zatitu pantaila"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index f7fcb2162603..32d5f5f34fb8 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"بستن"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string> <string name="expand_menu_text" msgid="3847736164494181168">"باز کردن منو"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"بزرگ کردن صفحه"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"بزرگ کردن صفحه"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 400107317637..6f03545e5542 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sulje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Avaa valikko"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Suurenna näyttö"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Jaa näyttö"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index b54f9cf2f15d..1fde4cfb76f8 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Agrandir l\'écran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aligner l\'écran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 357ff91df06d..e7233ae029b5 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mettre en plein écran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fractionner l\'écran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index a62190754129..89db32793b32 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Pechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 43c178f50d15..7e3d7a373be4 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string> <string name="expand_menu_text" msgid="3847736164494181168">"મેનૂ ખોલો"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"સ્ક્રીન કરો મોટી કરો"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"સ્ક્રીન સ્નૅપ કરો"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 9f6a57fa0d73..cd0f4e3618f7 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"बंद करें"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string> <string name="expand_menu_text" msgid="3847736164494181168">"मेन्यू खोलें"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 4378c5642f5c..fc3942095fdf 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje izbornika"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimalno povećaj zaslon"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Izradi snimku zaslona"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index e5f199fea647..a8cc5c120efc 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Bezárás"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menü megnyitása"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Képernyő méretének maximalizálása"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Igazodás a képernyő adott részéhez"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index e0a5afe13827..7f372774241a 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Փակել"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Բացել ընտրացանկը"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ծավալել էկրանը"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ծալել էկրանը"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 802583771852..3cf55fa0ede2 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Perbesar Layar"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gabungkan Layar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index cece56ec960f..6aa56f9858ad 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Loka"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Opna valmynd"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Stækka skjá"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Smelluskjár"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 731db8c12825..3c1d5e4dac02 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Chiudi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Apri menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Massimizza schermo"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aggancia schermo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index adf55f3696c3..a0c3b3a95ca8 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"סגירה"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string> <string name="expand_menu_text" msgid="3847736164494181168">"פתיחת התפריט"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"הגדלת המסך"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"כיווץ המסך"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 35432229dc7b..fb726c180997 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"閉じる"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</string> <string name="expand_menu_text" msgid="3847736164494181168">"メニューを開く"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"画面の最大化"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"画面のスナップ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 1e6e657b5cf8..e9f620a17203 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"დახურვა"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</string> <string name="expand_menu_text" msgid="3847736164494181168">"მენიუს გახსნა"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"აპლიკაციის გაშლა სრულ ეკრანზე"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"აპლიკაციის დაპატარავება ეკრანზე"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 6d9ff26132ea..34e41038f285 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Жабу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Мәзірді ашу"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды ұлғайту"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды бөлу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 586ef7327a70..362bbad4ec12 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"បិទ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"បិទម៉ឺនុយ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"បើកម៉ឺនុយ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ពង្រីកអេក្រង់"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ថតអេក្រង់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 78ca0c7979a9..77cc4a44f81a 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ಮೆನು ತೆರೆಯಿರಿ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ಸ್ಕ್ರೀನ್ ಅನ್ನು ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ಸ್ನ್ಯಾಪ್ ಸ್ಕ್ರೀನ್"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 70aa3767d7dd..e8b5522838b7 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"닫기"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string> <string name="expand_menu_text" msgid="3847736164494181168">"메뉴 열기"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"화면 최대화"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"화면 분할"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index b2a0a49b401a..7b7779d85d78 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Жабуу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Менюну ачуу"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды чоңойтуу"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды сүрөткө тартып алуу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 1cbdbd412631..a3519636b71f 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ປິດ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ເປີດເມນູ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ປັບຈໍໃຫຍ່ສຸດ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ສະແນັບໜ້າຈໍ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index d154c57704a1..e4dd7398f679 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Uždaryti"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Atidaryti meniu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index ce269503ceef..99aebf626322 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Aizvērt"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Atvērt izvēlni"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizēt ekrānu"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fiksēt ekrānu"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 9d69c50626be..c152c60fa631 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Затворете"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Отвори го менито"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Подели го екранот на половина"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index c0e83386d572..90275cdb517a 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string> <string name="expand_menu_text" msgid="3847736164494181168">"മെനു തുറക്കുക"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"സ്ക്രീൻ വലുതാക്കുക"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"സ്ക്രീൻ സ്നാപ്പ് ചെയ്യുക"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index ba5d283fb8a7..4a9fab92f11f 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Хаах"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Цэс нээх"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Дэлгэцийг томруулах"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Дэлгэцийг таллах"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 17601c18366a..5874bffc9199 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"बंद करा"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</string> <string name="expand_menu_text" msgid="3847736164494181168">"मेनू उघडा"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन मोठी करा"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रीन स्नॅप करा"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index d5547fa8a056..4de8a7b03547 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimumkan Skrin"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tangkap Skrin"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 07bfc990d62d..5b9e9cb7353e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</string> <string name="expand_menu_text" msgid="3847736164494181168">"မီနူး ဖွင့်ရန်"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"စခရင်ကို ချဲ့မည်"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"စခရင်ကို ချုံ့မည်"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index f609d019daae..9f03d8b5b178 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Lukk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Åpne menyen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimer skjermen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fest skjermen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 9a26b7e25187..e4830af1bad6 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string> <string name="expand_menu_text" msgid="3847736164494181168">"मेनु खोल्नुहोस्"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index a38cb7547385..0cd27c5c1457 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sluiten"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menu openen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Scherm maximaliseren"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Scherm halveren"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index e3097beb6166..bf751852a255 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ମେନୁ ଖୋଲନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ସ୍କ୍ରିନକୁ ସ୍ନାପ କରନ୍ତୁ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 3aea6f69749f..325c1e80c433 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ਸਕ੍ਰੀਨ ਦਾ ਆਕਾਰ ਵਧਾਓ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ਸਕ੍ਰੀਨ ਨੂੰ ਸਨੈਪ ਕਰੋ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index aec3722cefa5..a7648c8e323b 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zamknij"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otwórz menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index ba24d7b3eb07..e47d151337b2 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index f636da7997eb..ff77d3b2d150 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index ba24d7b3eb07..e47d151337b2 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index c20f350ae198..ae871f3dd42b 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Închide"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Deschide meniul"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizează fereastra"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Micșorează fereastra și fixeaz-o"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 49347d2d8086..e23c1ff19563 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Закрыть"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Открыть меню"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Развернуть на весь экран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Свернуть"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index e5a974683e49..ef1381cbe635 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"වසන්න"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string> <string name="expand_menu_text" msgid="3847736164494181168">"මෙනුව විවෘත කරන්න"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"තිරය උපරිම කරන්න"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ස්නැප් තිරය"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index c2d20ddb0d3b..55a03122483b 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zavrieť"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otvoriť ponuku"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovať obrazovku"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zobraziť polovicu obrazovky"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index cfe4480c6e1a..bb123dcdbfb6 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zapri"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Odpri meni"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index cba98c2fb61a..c74a8cd23338 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Mbyll"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Hap menynë"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizo ekranin"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Regjistro ekranin"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 5031f5bf5d64..0694a973dc1e 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Затворите"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Отворите мени"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Повећај екран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Уклопи екран"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 742be37b67ef..8e0bcfe91679 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Stäng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Öppna menyn"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximera skärmen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fäst skärmen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 68a7262d0eaf..41180abcf712 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Funga"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Fungua Menyu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Panua Dirisha kwenye Skrini"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Panga Madirisha kwenye Skrini"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index fe8fa057dc95..01ac78d984f3 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"மூடும்"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string> <string name="expand_menu_text" msgid="3847736164494181168">"மெனுவைத் திற"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 9be3f3354635..6224e72c19fe 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string> <string name="expand_menu_text" msgid="3847736164494181168">"మెనూను తెరవండి"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్ను పెంచండి"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్ను స్నాప్ చేయండి"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index cd3bf6a626c0..407fbbbb1707 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ปิด"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</string> <string name="expand_menu_text" msgid="3847736164494181168">"เปิดเมนู"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"สแนปหน้าจอ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index bf05e149ea57..786e99cfe8c8 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Isara"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Buksan ang Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"I-maximize ang Screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"I-snap ang Screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 2dfa38a76dfa..e953f5808aff 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Kapat"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü kapat"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menüyü Aç"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı Büyüt"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranın Yarısına Tuttur"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 57ca64f5b159..fbdf42e582d1 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Закрити"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Відкрити меню"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Розгорнути екран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Зафіксувати екран"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 077037373aa2..5562fa70bf09 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"بند کریں"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"مینیو بند کریں"</string> <string name="expand_menu_text" msgid="3847736164494181168">"مینو کھولیں"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"اسکرین کو بڑا کریں"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"اسکرین کا اسناپ شاٹ لیں"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index e2d1f47c210b..50e42329a1a0 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Yopish"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni yopish"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menyuni ochish"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranni yoyish"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranni biriktirish"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 4608b2b10c3f..6da85881210d 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Đóng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Mở Trình đơn"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mở rộng màn hình"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Điều chỉnh kích thước màn hình"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index cbb857c58611..4318caf26199 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"关闭"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</string> <string name="expand_menu_text" msgid="3847736164494181168">"打开菜单"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"屏幕快照"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index d89b2c29216e..72cd39d8e00a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> <string name="expand_menu_text" msgid="3847736164494181168">"打開選單"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index 4ce50a44e12a..c06d7b105694 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> <string name="expand_menu_text" msgid="3847736164494181168">"開啟選單"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index fa680f6eee89..755414e52762 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Vala"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Vula Imenyu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Khulisa Isikrini Sifike Ekugcineni"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Thwebula Isikrini"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 758dbfd5f3c5..cf18da6e7463 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -62,10 +62,6 @@ <color name="desktop_mode_caption_handle_bar_dark">#1C1C17</color> <color name="desktop_mode_resize_veil_light">#EFF1F2</color> <color name="desktop_mode_resize_veil_dark">#1C1C17</color> - <color name="desktop_mode_maximize_menu_button">#DDDACD</color> - <color name="desktop_mode_maximize_menu_button_outline">#797869</color> - <color name="desktop_mode_maximize_menu_button_outline_on_hover">#606219</color> - <color name="desktop_mode_maximize_menu_button_on_hover">#E7E790</color> <color name="desktop_mode_maximize_menu_progress_light">#33000000</color> <color name="desktop_mode_maximize_menu_progress_dark">#33FFFFFF</color> <color name="desktop_mode_caption_button_on_hover_light">#11000000</color> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 38ee6e2fb7de..c2ba064ac7b6 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -176,4 +176,7 @@ <!-- Whether CompatUIController is enabled --> <bool name="config_enableCompatUIController">true</bool> + + <!-- Whether pointer pilfer is required to start back animation. --> + <bool name="config_backAnimationRequiresPointerPilfer">true</bool> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 74967ef0d97c..c2c90c84278e 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -213,7 +213,7 @@ <dimen name="bubble_swap_animation_offset">15dp</dimen> <!-- How far offscreen the bubble stack rests. There's some padding around the bubble so add 3dp to the desired overhang. --> - <dimen name="bubble_stack_offscreen">3dp</dimen> + <dimen name="bubble_stack_offscreen">2.5dp</dimen> <!-- How far down the screen the stack starts. --> <dimen name="bubble_stack_starting_offset_y">120dp</dimen> <!-- Space between the pointer triangle and the bubble expanded view --> @@ -254,6 +254,8 @@ <dimen name="bubble_bar_expanded_view_caption_dot_size">4dp</dimen> <!-- The spacing between the dots for the caption menu in the bubble bar expanded view.. --> <dimen name="bubble_bar_expanded_view_caption_dot_spacing">4dp</dimen> + <!-- Width of the expanded bubble bar view shown when the bubble is expanded. --> + <dimen name="bubble_bar_expanded_view_width">412dp</dimen> <!-- Minimum width of the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_min_width">200dp</dimen> <!-- Size of the dismiss icon in the bubble bar manage menu. --> @@ -272,6 +274,10 @@ <dimen name="bubble_bar_expanded_view_corner_radius">16dp</dimen> <!-- Corner radius for expanded view while it is being dragged --> <dimen name="bubble_bar_expanded_view_corner_radius_dragged">28dp</dimen> + <!-- Width of the box around bottom center of the screen where drag only leads to dismiss --> + <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen> + <!-- Height of the box around bottom center of the screen where drag only leads to dismiss --> + <dimen name="bubble_bar_dismiss_zone_height">242dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> @@ -419,8 +425,9 @@ <!-- Height of desktop mode caption for fullscreen tasks. --> <dimen name="desktop_mode_fullscreen_decor_caption_height">36dp</dimen> - <!-- Width of desktop mode caption for fullscreen tasks. --> - <dimen name="desktop_mode_fullscreen_decor_caption_width">128dp</dimen> + <!-- Width of desktop mode caption for fullscreen tasks. + 80 dp for handle + 20 dp for room to grow on the sides when hovered. --> + <dimen name="desktop_mode_fullscreen_decor_caption_width">100dp</dimen> <!-- Required empty space to be visible for partially offscreen tasks. --> <dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen> @@ -452,16 +459,19 @@ <dimen name="desktop_mode_customizable_caption_margin_end">152dp</dimen> <!-- The width of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_width">287dp</dimen> + <dimen name="desktop_mode_maximize_menu_width">228dp</dimen> <!-- The height of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_height">112dp</dimen> + <dimen name="desktop_mode_maximize_menu_height">114dp</dimen> - <!-- The larger of the two corner radii of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_large_corner_radius">4dp</dimen> + <!-- The height of the buttons in the maximize menu. --> + <dimen name="desktop_mode_maximize_menu_button_height">52dp</dimen> - <!-- The smaller of the two corner radii of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_small_corner_radius">2dp</dimen> + <!-- The radius of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_radius">4dp</dimen> + + <!-- The radius of the layout outline around the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_outline_radius">6dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> @@ -502,18 +512,29 @@ <!-- The radius of the caption menu shadow. --> <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> + <!-- The size of the icon shown in the resize veil. --> + <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen> + + <!-- The with of the border around the app task for edge resizing, when + enable_windowing_edge_drag_resize is enabled. --> + <dimen name="desktop_mode_edge_handle">12dp</dimen> + + <!-- The original width of the border around the app task for edge resizing, when + enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_handle">15dp</dimen> + <!-- The size of the corner region for drag resizing with touch, when a larger touch region is + appropriate. Applied when enable_windowing_edge_drag_resize is enabled. --> + <dimen name="desktop_mode_corner_resize_large">48dp</dimen> + + <!-- The original size of the corner region for darg resizing, when + enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_corner">44dp</dimen> <!-- The width of the area at the sides of the screen where a freeform task will transition to split select if dragged until the touch input is within the range. --> <dimen name="desktop_mode_transition_area_width">32dp</dimen> - <!-- The height of the area at the top of the screen where a freeform task will transition to - fullscreen if dragged until the top bound of the task is within the area. --> - <dimen name="desktop_mode_transition_area_height">16dp</dimen> - <!-- The width of the area where a desktop task will transition to fullscreen. --> <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen> @@ -535,5 +556,7 @@ <!-- The vertical margin that needs to be preserved between the scaled window bounds and the original window bounds (once the surface is scaled enough to do so) --> <dimen name="cross_task_back_vertical_margin">8dp</dimen> + <!-- The offset from the left edge of the entering page for the cross-activity animation --> + <dimen name="cross_activity_back_entering_start_offset">96dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 812a81ba33d1..bf654d979856 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -280,4 +280,8 @@ <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] --> <string name="expand_menu_text">Open Menu</string> + <!-- Maximize menu maximize button string. --> + <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> + <!-- Maximize menu snap buttons string. --> + <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 08c2a02acf55..13c0e6646002 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -23,6 +23,14 @@ <item name="android:windowAnimationStyle">@style/Animation.ForcedResizable</item> </style> + <!-- Theme used for the activity that shows below the desktop mode windows to show wallpaper --> + <style name="DesktopWallpaperTheme" parent="@android:style/Theme.Wallpaper.NoTitleBar"> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="android:windowDrawsSystemBarBackgrounds">true</item> + <item name="android:windowAnimationStyle">@null</item> + </style> + <style name="Animation.ForcedResizable" parent="@android:style/Animation"> <item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl index 72fba3bb7de4..8481c446c6aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.transition; +package com.android.wm.shell.shared; import android.window.RemoteTransition; import android.window.TransitionFilter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl index 7f4a8f1d476a..526407e25d98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.wm.shell.transition; +package com.android.wm.shell.shared; import android.view.SurfaceControl; import android.window.RemoteTransition; import android.window.TransitionFilter; -import com.android.wm.shell.transition.IHomeTransitionListener; +import com.android.wm.shell.shared.IHomeTransitionListener; /** * Interface that is exposed to remote callers to manipulate the transitions feature. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java index da39017a0313..5e49f559ca64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.wm.shell.transition; +package com.android.wm.shell.shared; import android.annotation.NonNull; import android.window.RemoteTransition; import android.window.TransitionFilter; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; /** * Interface to manage remote transitions. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt index ee8c41417458..9d3b56d22a2f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.animation +package com.android.wm.shell.shared.animation import android.util.ArrayMap import android.util.Log @@ -25,7 +25,7 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce -import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance +import com.android.wm.shell.shared.animation.PhysicsAnimator.Companion.getInstance import java.lang.ref.WeakReference import java.util.WeakHashMap import kotlin.math.abs @@ -505,7 +505,6 @@ class PhysicsAnimator<T> private constructor (target: T) { // Check for a spring configuration. If one is present, we're either springing, or // flinging-then-springing. if (springConfig != null) { - // If there is no corresponding fling config, we're only springing. if (flingConfig == null) { // Apply the configuration and start the animation. @@ -679,7 +678,6 @@ class PhysicsAnimator<T> private constructor (target: T) { value: Float, velocity: Float ) { - // If this property animation isn't relevant to this listener, ignore it. if (!properties.contains(property)) { return @@ -702,7 +700,6 @@ class PhysicsAnimator<T> private constructor (target: T) { finalVelocity: Float, isFling: Boolean ): Boolean { - // If this property animation isn't relevant to this listener, ignore it. if (!properties.contains(property)) { return false @@ -877,7 +874,7 @@ class PhysicsAnimator<T> private constructor (target: T) { * * @param <T> The type of the object being animated. </T> */ - interface UpdateListener<T> { + fun interface UpdateListener<T> { /** * Called on each animation frame with the target object, and a map of FloatPropertyCompat @@ -907,7 +904,7 @@ class PhysicsAnimator<T> private constructor (target: T) { * * @param <T> The type of the object being animated. </T> */ - interface EndListener<T> { + fun interface EndListener<T> { /** * Called with the final animation values as each property animation ends. This can be used @@ -971,17 +968,18 @@ class PhysicsAnimator<T> private constructor (target: T) { companion object { /** - * Constructor to use to for new physics animator instances in [getInstance]. This is - * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that - * all code using the physics animator is given testable instances instead. + * Callback to notify that a new animator was created. Used in [PhysicsAnimatorTestUtils] + * to be able to keep track of animators and wait for them to finish. */ - internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator + internal var onAnimatorCreated: (PhysicsAnimator<*>, Any) -> Unit = { _, _ -> } @JvmStatic @Suppress("UNCHECKED_CAST") fun <T : Any> getInstance(target: T): PhysicsAnimator<T> { if (!animators.containsKey(target)) { - animators[target] = instanceConstructor(target) + val animator = PhysicsAnimator(target) + onAnimatorCreated(animator, target) + animators[target] = animator } return animators[target] as PhysicsAnimator<T> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt index 86eb8da952f1..235b9bf7b9fd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.animation +package com.android.wm.shell.shared.animation import android.os.Handler import android.os.Looper import android.util.ArrayMap import androidx.dynamicanimation.animation.FloatPropertyCompat -import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.prepareForTest +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.prepareForTest import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -62,12 +62,9 @@ object PhysicsAnimatorTestUtils { */ @JvmStatic fun prepareForTest() { - val defaultConstructor = PhysicsAnimator.instanceConstructor - PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> { - val animator = defaultConstructor(target) + PhysicsAnimator.onAnimatorCreated = { animator, target -> allAnimatedObjects.add(target) animatorTestHelpers[animator] = AnimatorTestHelper(animator) - return animator } timeoutMs = 2000 @@ -158,12 +155,12 @@ object PhysicsAnimatorTestUtils { @Throws(InterruptedException::class) @Suppress("UNCHECKED_CAST") fun <T : Any> blockUntilAnimationsEnd( - properties: FloatPropertyCompat<in T> + vararg properties: FloatPropertyCompat<in T> ) { for (target in allAnimatedObjects) { try { blockUntilAnimationsEnd( - PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties) + PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, *properties) } catch (e: ClassCastException) { // Keep checking the other objects for ones whose types match the provided // properties. @@ -267,10 +264,8 @@ object PhysicsAnimatorTestUtils { // Loop through the updates from the testable animator. for (update in framesForProperty) { - // Check whether this frame satisfies the current matcher. if (curMatcher(update)) { - // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining // frames and return without failing. if (matchers.size == 0) { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java new file mode 100644 index 000000000000..a1496ac1d33b --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync + * instead of the app vsync. + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ChoreographerSfVsync {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java index 9ac7a12bc509..52a717b3a60c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.annotations; +package com.android.wm.shell.shared.annotations; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java new file mode 100644 index 000000000000..ae5188cf8093 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or class that is called from an external thread to the Shell threads. */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ExternalThread {} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java new file mode 100644 index 000000000000..bd2887e39ef1 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellAnimationThread {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java index 4cd3c903f2f8..586ac8297e26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.annotations; +package com.android.wm.shell.shared.annotations; import java.lang.annotation.Documented; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java new file mode 100644 index 000000000000..6c879a491fe0 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell main-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellMainThread {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java index c2fd54fd96d7..4887dbe81b25 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.annotations; +package com.android.wm.shell.shared.annotations; import java.lang.annotation.Documented; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 539832e3cf3c..d44033c72302 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -523,8 +523,8 @@ class ActivityEmbeddingAnimationRunner { /** * Whether we should use jump cut for the change transition. * This normally happens when opening a new secondary with the existing primary using a - * different split layout. This can be complicated, like from horizontal to vertical split with - * new split pairs. + * different split layout (ratio or direction). This can be complicated, like from horizontal to + * vertical split with new split pairs. * Uses a jump cut animation to simplify. */ private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) { @@ -553,8 +553,8 @@ class ActivityEmbeddingAnimationRunner { } // Check if the transition contains both opening and closing windows. - boolean hasOpeningWindow = false; - boolean hasClosingWindow = false; + final List<TransitionInfo.Change> openChanges = new ArrayList<>(); + final List<TransitionInfo.Change> closeChanges = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { if (changingChanges.contains(change)) { continue; @@ -564,10 +564,30 @@ class ActivityEmbeddingAnimationRunner { // No-op if it will be covered by the changing parent window. continue; } - hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode()); - hasClosingWindow |= TransitionUtil.isClosingType(change.getMode()); + if (TransitionUtil.isOpeningType(change.getMode())) { + openChanges.add(change); + } else if (TransitionUtil.isClosingType(change.getMode())) { + closeChanges.add(change); + } + } + if (openChanges.isEmpty() || closeChanges.isEmpty()) { + // Only skip if the transition contains both open and close. + return false; + } + if (changingChanges.size() != 1 || openChanges.size() != 1 || closeChanges.size() != 1) { + // Skip when there are too many windows involved. + return true; + } + final TransitionInfo.Change changingChange = changingChanges.get(0); + final TransitionInfo.Change openChange = openChanges.get(0); + final TransitionInfo.Change closeChange = closeChanges.get(0); + if (changingChange.getStartAbsBounds().equals(openChange.getEndAbsBounds()) + && changingChange.getEndAbsBounds().equals(closeChange.getStartAbsBounds())) { + // Don't skip if the transition is a simple shifting without split direction or ratio + // change. For example, A|B -> B|C. + return false; } - return hasOpeningWindow && hasClosingWindow; + return true; } /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 19963675ff86..ce0bf8b29374 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -17,6 +17,7 @@ package com.android.wm.shell.animation; import android.graphics.Path; +import android.view.animation.BackGestureInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; @@ -95,6 +96,15 @@ public class Interpolators { public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); + /** + * Use this interpolator for animating progress values coming from the back callback to get + * the predictive-back-typical decelerate motion. + * + * This interpolator is similar to {@link Interpolators#STANDARD_DECELERATE} but has a slight + * acceleration phase at the start. + */ + public static final Interpolator BACK_GESTURE = new BackGestureInterpolator(); + // Create the default emphasized interpolator private static PathInterpolator createEmphasizedInterpolator() { Path path = new Path(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index 8d8dc10951a6..26432111efdc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -20,7 +20,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.window.BackEvent; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; /** * Interface for external process to get access to the Back animation related methods. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 2606fb661e80..d3fe4f82daf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -32,6 +32,7 @@ import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.content.ContentResolver; import android.content.Context; +import android.content.res.Configuration; import android.database.ContentObserver; import android.hardware.input.InputManager; import android.net.Uri; @@ -64,12 +65,14 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; +import com.android.wm.shell.R; import com.android.wm.shell.animation.FlingAnimationUtils; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ShellBackgroundThread; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -80,7 +83,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * Controls the window animation run when a user initiates a back gesture. */ -public class BackAnimationController implements RemoteCallable<BackAnimationController> { +public class BackAnimationController implements RemoteCallable<BackAnimationController>, + ConfigurationChangeListener { private static final String TAG = "ShellBackPreview"; private static final int SETTING_VALUE_OFF = 0; private static final int SETTING_VALUE_ON = 1; @@ -115,6 +119,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private boolean mShouldStartOnNextMoveEvent = false; private boolean mOnBackStartDispatched = false; private boolean mPointerPilfered = false; + private final boolean mRequirePointerPilfer; private final FlingAnimationUtils mFlingAnimationUtils; @@ -145,7 +150,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final Runnable mAnimationTimeoutRunnable = () -> { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...", MAX_ANIMATION_DURATION); - onBackAnimationFinished(); + finishBackAnimation(); }; private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; @@ -154,6 +159,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mActiveCallback; + @Nullable + private RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( @@ -169,6 +176,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone."); setTriggerBack(false); resetTouchTracker(); + // Don't wait for animation start + mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); }); } }); @@ -220,6 +229,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mActivityTaskManager = activityTaskManager; mContext = context; mContentResolver = contentResolver; + mRequirePointerPilfer = + context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer); mBgHandler = bgHandler; shellInit.addInitCallback(this::onInit, this); mAnimationBackground = backAnimationBackground; @@ -240,6 +251,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); + mShellController.addConfigurationChangeListener(this); } private void setupAnimationDeveloperSettingsObserver( @@ -289,6 +301,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final BackAnimationImpl mBackAnimation = new BackAnimationImpl(); @Override + public void onConfigurationChanged(Configuration newConfig) { + mShellBackAnimationRegistry.onConfigurationChanged(newConfig); + } + + @Override public Context getContext() { return mContext; } @@ -462,6 +479,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { + boolean interruptCancelPostCommitAnimation = mPostCommitAnimationInProgress + && mCurrentTracker.isFinished() && !mCurrentTracker.getTriggerBack() + && mQueuedTracker.isInInitialState(); + if (interruptCancelPostCommitAnimation) { + // If a system animation is currently in the post-commit phase animating an + // onBackCancelled event, let's interrupt it and start animating a new back gesture + resetTouchTracker(); + } TouchTracker touchTracker; if (mCurrentTracker.isInInitialState()) { touchTracker = mCurrentTracker; @@ -476,9 +501,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont touchTracker.setState(TouchTracker.TouchTrackerState.ACTIVE); mBackGestureStarted = true; - if (touchTracker == mCurrentTracker) { + if (interruptCancelPostCommitAnimation) { + // post-commit cancel is currently running. let's interrupt it and dispatch a new + // onBackStarted event. + mPostCommitAnimationInProgress = false; + mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); + startSystemAnimation(); + } else if (touchTracker == mCurrentTracker) { // Only start the back navigation if no other gesture is being processed. Otherwise, - // the back navigation will be started once the current gesture has finished. + // the back navigation will fall back to legacy back event injection. startBackNavigation(mCurrentTracker); } } @@ -560,7 +591,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void tryDispatchOnBackStarted( IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (mOnBackStartDispatched || callback == null || !mPointerPilfered) { + if (mOnBackStartDispatched + || callback == null + || (!mPointerPilfered && mRequirePointerPilfer)) { return; } dispatchOnBackStarted(callback, backEvent); @@ -664,7 +697,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void tryDispatchOnBackCancelled(IOnBackInvokedCallback callback) { + if (!mOnBackStartDispatched) { + Log.e(TAG, "Skipping dispatching onBackCancelled. Start was never dispatched."); + return; + } if (callback == null) { return; } @@ -723,7 +760,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (touchTracker.getTriggerBack()) { dispatchOrAnimateOnBackInvoked(callback, touchTracker); } else { - dispatchOnBackCancelled(callback); + tryDispatchOnBackCancelled(callback); } } finishBackNavigation(touchTracker.getTriggerBack()); @@ -800,9 +837,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // The next callback should be {@link #onBackAnimationFinished}. if (mCurrentTracker.getTriggerBack()) { + // notify gesture finished + mBackNavigationInfo.onBackGestureFinished(true); dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker); } else { - dispatchOnBackCancelled(mActiveCallback); + tryDispatchOnBackCancelled(mActiveCallback); } } @@ -812,6 +851,20 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ @VisibleForTesting void onBackAnimationFinished() { + if (!mPostCommitAnimationInProgress) { + // This can happen when a post-commit cancel animation was interrupted by a new back + // gesture but the timing of interruption was bad such that the back-callback + // implementation finished in between the time of the new gesture having started and + // the time of the back-callback receiving the new onBackStarted event. Due to the + // asynchronous APIs this isn't an unlikely case. To handle this, let's return early. + // The back-callback implementation will call onBackAnimationFinished again when it is + // done with animating the second gesture. + return; + } + finishBackAnimation(); + } + + private void finishBackAnimation() { // Stop timeout runner. mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); mPostCommitAnimationInProgress = false; @@ -840,7 +893,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mCurrentTracker.isInInitialState()) { if (mBackGestureStarted) { mBackGestureStarted = false; - dispatchOnBackCancelled(mActiveCallback); + tryDispatchOnBackCancelled(mActiveCallback); finishBackNavigation(false); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> reset an unfinished gesture"); @@ -872,6 +925,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont void finishBackNavigation(boolean triggerBack) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()"); mActiveCallback = null; + mApps = null; mShouldStartOnNextMoveEvent = false; mOnBackStartDispatched = false; mPointerPilfered = false; @@ -908,6 +962,57 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTrackingLatency = false; } + private void startSystemAnimation() { + if (mBackNavigationInfo == null) { + ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Lack of navigation info to start animation."); + return; + } + if (!validateAnimationTargets(mApps)) { + ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Not starting animation due to mApps being null."); + return; + } + + final BackAnimationRunner runner = + mShellBackAnimationRegistry.getAnimationRunnerAndInit(mBackNavigationInfo); + if (runner == null) { + if (mBackAnimationFinishedCallback != null) { + try { + mBackAnimationFinishedCallback.onAnimationFinished(false); + } catch (RemoteException e) { + Log.w(TAG, "Failed call IBackNaviAnimationController", e); + } + } + return; + } + mActiveCallback = runner.getCallback(); + + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()"); + + runner.startAnimation(mApps, /*wallpapers*/ null, /*nonApps*/ null, + () -> mShellExecutor.execute(this::onBackAnimationFinished)); + + if (mApps.length >= 1) { + mCurrentTracker.updateStartLocation(); + BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]); + dispatchOnBackStarted(mActiveCallback, startEvent); + } + } + + /** + * Validate animation targets. + */ + static boolean validateAnimationTargets(RemoteAnimationTarget[] apps) { + if (apps == null || apps.length == 0) { + return false; + } + for (int i = apps.length - 1; i >= 0; --i) { + if (!apps[i].leash.isValid()) { + return false; + } + } + return true; + } + private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @@ -920,48 +1025,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.execute( () -> { endLatencyTracking(); - if (mBackNavigationInfo == null) { - ProtoLog.e(WM_SHELL_BACK_PREVIEW, - "Lack of navigation info to start animation."); + if (!validateAnimationTargets(apps)) { + Log.e(TAG, "Invalid animation targets!"); return; } - final BackAnimationRunner runner = - mShellBackAnimationRegistry.getAnimationRunnerAndInit( - mBackNavigationInfo); - if (runner == null) { - if (finishedCallback != null) { - try { - finishedCallback.onAnimationFinished(false); - } catch (RemoteException e) { - Log.w( - TAG, - "Failed call IBackNaviAnimationController", - e); - } - } - return; - } - mActiveCallback = runner.getCallback(); mBackAnimationFinishedCallback = finishedCallback; - - ProtoLog.d( - WM_SHELL_BACK_PREVIEW, - "BackAnimationController: startAnimation()"); - runner.startAnimation( - apps, - wallpapers, - nonApps, - () -> - mShellExecutor.execute( - BackAnimationController.this - ::onBackAnimationFinished)); - - if (apps.length >= 1) { - mCurrentTracker.updateStartLocation(); - BackMotionEvent startEvent = - mCurrentTracker.createStartEvent(apps[0]); - dispatchOnBackStarted(mActiveCallback, startEvent); - } + mApps = apps; + startSystemAnimation(); // Dispatch the first progress after animation start for // smoothing the initial animation, instead of waiting for next @@ -1006,6 +1076,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted); pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress); pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent); + pw.println(prefix + " mPointerPilfered=" + mPointerPilfered); + pw.println(prefix + " mRequirePointerPilfer=" + mRequirePointerPilfer); pw.println(prefix + " mCurrentTracker state:"); mCurrentTracker.dump(pw, prefix + " "); pw.println(prefix + " mQueuedTracker state:"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index a32b435ff99e..4988a9481d21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -28,6 +28,7 @@ import android.view.RemoteAnimationTarget; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj.CujType; import com.android.wm.shell.common.InteractionJankMonitorUtils; @@ -108,7 +109,8 @@ public class BackAnimationRunner { } } - private boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { + @VisibleForTesting + boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { return apps.length > 0 && mCujType != NO_CUJ; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java deleted file mode 100644 index d6f7c367f772..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright (C) 2022 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.wm.shell.back; - -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static android.window.BackEvent.EDGE_RIGHT; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.os.RemoteException; -import android.util.FloatProperty; -import android.util.TypedValue; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.animation.Interpolator; -import android.window.BackEvent; -import android.window.BackMotionEvent; -import android.window.BackProgressAnimator; -import android.window.IOnBackInvokedCallback; - -import com.android.internal.dynamicanimation.animation.SpringAnimation; -import com.android.internal.dynamicanimation.animation.SpringForce; -import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.common.annotations.ShellMainThread; - -import javax.inject.Inject; - -/** Class that defines cross-activity animation. */ -@ShellMainThread -public class CrossActivityBackAnimation extends ShellBackAnimation { - /** - * Minimum scale of the entering/closing window. - */ - private static final float MIN_WINDOW_SCALE = 0.9f; - - /** Duration of post animation after gesture committed. */ - private static final int POST_ANIMATION_DURATION = 350; - private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE; - private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP = - new FloatProperty<>("enter-alpha") { - @Override - public void setValue(CrossActivityBackAnimation anim, float value) { - anim.setEnteringProgress(value); - } - - @Override - public Float get(CrossActivityBackAnimation object) { - return object.getEnteringProgress(); - } - }; - private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP = - new FloatProperty<>("leave-alpha") { - @Override - public void setValue(CrossActivityBackAnimation anim, float value) { - anim.setLeavingProgress(value); - } - - @Override - public Float get(CrossActivityBackAnimation object) { - return object.getLeavingProgress(); - } - }; - private static final float MIN_WINDOW_ALPHA = 0.01f; - private static final float WINDOW_X_SHIFT_DP = 48; - private static final int SCALE_FACTOR = 100; - // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists. - private static final float TARGET_COMMIT_PROGRESS = 0.5f; - private static final float ENTER_ALPHA_THRESHOLD = 0.22f; - - private final Rect mStartTaskRect = new Rect(); - private final float mCornerRadius; - - // The closing window properties. - private final RectF mClosingRect = new RectF(); - - // The entering window properties. - private final Rect mEnteringStartRect = new Rect(); - private final RectF mEnteringRect = new RectF(); - private final SpringAnimation mEnteringProgressSpring; - private final SpringAnimation mLeavingProgressSpring; - // Max window x-shift in pixels. - private final float mWindowXShift; - private final BackAnimationRunner mBackAnimationRunner; - - private float mEnteringProgress = 0f; - private float mLeavingProgress = 0f; - - private final PointF mInitialTouchPos = new PointF(); - - private final Matrix mTransformMatrix = new Matrix(); - - private final float[] mTmpFloat9 = new float[9]; - - private RemoteAnimationTarget mEnteringTarget; - private RemoteAnimationTarget mClosingTarget; - private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - - private boolean mBackInProgress = false; - private boolean mIsRightEdge; - private boolean mTriggerBack = false; - - private PointF mTouchPos = new PointF(); - private IRemoteAnimationFinishedCallback mFinishCallback; - - private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); - - private final BackAnimationBackground mBackground; - - @Inject - public CrossActivityBackAnimation(Context context, BackAnimationBackground background) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); - mBackAnimationRunner = new BackAnimationRunner( - new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY); - mBackground = background; - mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); - mEnteringProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP); - mLeavingProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP, - context.getResources().getDisplayMetrics()); - } - - /** - * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two. - * From https://en.wikipedia.org/wiki/Smoothstep - */ - private static float smoothstep(float edge0, float edge1, float x) { - if (x < edge0) return 0; - if (x >= edge1) return 1; - - x = (x - edge0) / (edge1 - edge0); - return x * x * (3 - 2 * x); - } - - /** - * Linearly map x from range (a1, a2) to range (b1, b2). - */ - private static float mapLinear(float x, float a1, float a2, float b1, float b2) { - return b1 + (x - a1) * (b2 - b1) / (a2 - a1); - } - - /** - * Linearly map a normalized value from (0, 1) to (min, max). - */ - private static float mapRange(float value, float min, float max) { - return min + (value * (max - min)); - } - - private void startBackAnimation() { - if (mEnteringTarget == null || mClosingTarget == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); - return; - } - mTransaction.setAnimationTransaction(); - - // Offset start rectangle to align task bounds. - mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds()); - mStartTaskRect.offsetTo(0, 0); - - // Draw background with task background color. - mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction); - setEnteringProgress(0); - setLeavingProgress(0); - } - - private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) { - if (leash == null || !leash.isValid()) { - return; - } - - final float scale = targetRect.width() / mStartTaskRect.width(); - mTransformMatrix.reset(); - mTransformMatrix.setScale(scale, scale); - mTransformMatrix.postTranslate(targetRect.left, targetRect.top); - mTransaction.setAlpha(leash, targetAlpha) - .setMatrix(leash, mTransformMatrix, mTmpFloat9) - .setWindowCrop(leash, mStartTaskRect) - .setCornerRadius(leash, mCornerRadius); - } - - private void finishAnimation() { - if (mEnteringTarget != null) { - if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) { - mTransaction.setCornerRadius(mEnteringTarget.leash, 0); - mEnteringTarget.leash.release(); - } - mEnteringTarget = null; - } - if (mClosingTarget != null) { - if (mClosingTarget.leash != null) { - mClosingTarget.leash.release(); - } - mClosingTarget = null; - } - if (mBackground != null) { - mBackground.removeBackground(mTransaction); - } - - mTransaction.apply(); - mBackInProgress = false; - mTransformMatrix.reset(); - mInitialTouchPos.set(0, 0); - - if (mFinishCallback != null) { - try { - mFinishCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.printStackTrace(); - } - mFinishCallback = null; - } - mEnteringProgressSpring.animateToFinalPosition(0); - mEnteringProgressSpring.skipToEnd(); - mLeavingProgressSpring.animateToFinalPosition(0); - mLeavingProgressSpring.skipToEnd(); - } - - private void onGestureProgress(@NonNull BackEvent backEvent) { - if (!mBackInProgress) { - mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; - mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); - mBackInProgress = true; - } - mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); - - float progress = backEvent.getProgress(); - float springProgress = (mTriggerBack - ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1) - : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; - mLeavingProgressSpring.animateToFinalPosition(springProgress); - mEnteringProgressSpring.animateToFinalPosition(springProgress); - mBackground.onBackProgressed(progress); - } - - private void onGestureCommitted() { - if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null - || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid() - || !mClosingTarget.leash.isValid()) { - finishAnimation(); - return; - } - // End the fade animations - mLeavingProgressSpring.cancel(); - mEnteringProgressSpring.cancel(); - - // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current - // coordinate of the gesture driven phase. - mEnteringRect.round(mEnteringStartRect); - mTransaction.hide(mClosingTarget.leash); - - ValueAnimator valueAnimator = - ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION); - valueAnimator.setInterpolator(INTERPOLATOR); - valueAnimator.addUpdateListener(animation -> { - float progress = animation.getAnimatedFraction(); - updatePostCommitEnteringAnimation(progress); - if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) { - mBackground.resetStatusBarCustomization(); - } - mTransaction.apply(); - }); - - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mBackground.resetStatusBarCustomization(); - finishAnimation(); - } - }); - valueAnimator.start(); - } - - private void updatePostCommitEnteringAnimation(float progress) { - float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left); - float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top); - float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width()); - float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height()); - float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f); - mEnteringRect.set(left, top, left + width, top + height); - applyTransform(mEnteringTarget.leash, mEnteringRect, alpha); - } - - private float getPreCommitEnteringAlpha() { - return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress), - MIN_WINDOW_ALPHA); - } - - private float getEnteringProgress() { - return mEnteringProgress * SCALE_FACTOR; - } - - private void setEnteringProgress(float value) { - mEnteringProgress = value / SCALE_FACTOR; - if (mEnteringTarget != null && mEnteringTarget.leash != null) { - transformWithProgress( - mEnteringProgress, - getPreCommitEnteringAlpha(), - mEnteringTarget.leash, - mEnteringRect, - -mWindowXShift, - 0 - ); - } - } - - private float getPreCommitLeavingAlpha() { - return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress), - MIN_WINDOW_ALPHA); - } - - private float getLeavingProgress() { - return mLeavingProgress * SCALE_FACTOR; - } - - private void setLeavingProgress(float value) { - mLeavingProgress = value / SCALE_FACTOR; - if (mClosingTarget != null && mClosingTarget.leash != null) { - transformWithProgress( - mLeavingProgress, - getPreCommitLeavingAlpha(), - mClosingTarget.leash, - mClosingRect, - 0, - mIsRightEdge ? 0 : mWindowXShift - ); - } - } - - private void transformWithProgress(float progress, float alpha, SurfaceControl surface, - RectF targetRect, float deltaXMin, float deltaXMax) { - - final int width = mStartTaskRect.width(); - final int height = mStartTaskRect.height(); - - final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress); - final float closingScale = MIN_WINDOW_SCALE - + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE); - final float closingWidth = closingScale * width; - final float closingHeight = (float) height / width * closingWidth; - - // Move the window along the X axis. - float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2; - closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax); - - // Move the window along the Y axis. - final float closingTop = (height - closingHeight) * 0.5f; - targetRect.set( - closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight); - - applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA)); - mTransaction.apply(); - } - - @Override - public BackAnimationRunner getRunner() { - return mBackAnimationRunner; - } - - private final class Callback extends IOnBackInvokedCallback.Default { - @Override - public void onBackStarted(BackMotionEvent backEvent) { - mTriggerBack = backEvent.getTriggerBack(); - mProgressAnimator.onBackStarted(backEvent, - CrossActivityBackAnimation.this::onGestureProgress); - } - - @Override - public void onBackProgressed(@NonNull BackMotionEvent backEvent) { - mTriggerBack = backEvent.getTriggerBack(); - mProgressAnimator.onBackProgressed(backEvent); - } - - @Override - public void onBackCancelled() { - mProgressAnimator.onBackCancelled(() -> { - // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring, - // and if we release all animation leash first, the leavingProgressSpring won't - // able to update the animation anymore, which cause flicker. - // Here should force update the closing animation target to the final stage before - // release it. - setLeavingProgress(0); - finishAnimation(); - }); - } - - @Override - public void onBackInvoked() { - mProgressAnimator.reset(); - onGestureCommitted(); - } - } - - private final class Runner extends IRemoteAnimationRunner.Default { - @Override - public void onAnimationStart( - int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation."); - for (RemoteAnimationTarget a : apps) { - if (a.mode == MODE_CLOSING) { - mClosingTarget = a; - } - if (a.mode == MODE_OPENING) { - mEnteringTarget = a; - } - } - - startBackAnimation(); - mFinishCallback = finishedCallback; - } - - @Override - public void onAnimationCancelled() { - finishAnimation(); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt new file mode 100644 index 000000000000..7cb56605cc12 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2022 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.wm.shell.back + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.Configuration +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.os.RemoteException +import android.view.Choreographer +import android.view.Display +import android.view.IRemoteAnimationFinishedCallback +import android.view.IRemoteAnimationRunner +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.window.BackEvent +import android.window.BackMotionEvent +import android.window.BackProgressAnimator +import android.window.IOnBackInvokedCallback +import com.android.internal.jank.Cuj +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.annotations.ShellMainThread +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** Class that defines cross-activity animation. */ +@ShellMainThread +class CrossActivityBackAnimation @Inject constructor( + private val context: Context, + private val background: BackAnimationBackground, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +) : ShellBackAnimation() { + + private val startClosingRect = RectF() + private val targetClosingRect = RectF() + private val currentClosingRect = RectF() + + private val startEnteringRect = RectF() + private val targetEnteringRect = RectF() + private val currentEnteringRect = RectF() + + private val backAnimRect = Rect() + + private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + + private val backAnimationRunner = BackAnimationRunner( + Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY + ) + private val initialTouchPos = PointF() + private val transformMatrix = Matrix() + private val tmpFloat9 = FloatArray(9) + private var enteringTarget: RemoteAnimationTarget? = null + private var closingTarget: RemoteAnimationTarget? = null + private val transaction = SurfaceControl.Transaction() + private var triggerBack = false + private var finishCallback: IRemoteAnimationFinishedCallback? = null + private val progressAnimator = BackProgressAnimator() + private val displayBoundsMargin = + context.resources.getDimension(R.dimen.cross_task_back_vertical_margin) + private val enteringStartOffset = + context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) + + private val gestureInterpolator = Interpolators.BACK_GESTURE + private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN + private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator() + + private var scrimLayer: SurfaceControl? = null + private var maxScrimAlpha: Float = 0f + + override fun onConfigurationChanged(newConfiguration: Configuration) { + cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + } + + override fun getRunner() = backAnimationRunner + + private fun startBackAnimation(backMotionEvent: BackMotionEvent) { + if (enteringTarget == null || closingTarget == null) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, + "Entering target or closing target is null." + ) + return + } + triggerBack = backMotionEvent.triggerBack + initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY) + + transaction.setAnimationTransaction() + + // Offset start rectangle to align task bounds. + backAnimRect.set(closingTarget!!.localBounds) + backAnimRect.offsetTo(0, 0) + + startClosingRect.set(backAnimRect) + + // scale closing target into the middle for rhs and to the right for lhs + targetClosingRect.set(startClosingRect) + targetClosingRect.scaleCentered(MAX_SCALE) + if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) { + targetClosingRect.offset( + startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f + ) + } + + // the entering target starts 96dp to the left of the screen edge... + startEnteringRect.set(startClosingRect) + startEnteringRect.offset(-enteringStartOffset, 0f) + + // ...and gets scaled in sync with the closing target + targetEnteringRect.set(startEnteringRect) + targetEnteringRect.scaleCentered(MAX_SCALE) + + // Draw background with task background color. + background.ensureBackground( + closingTarget!!.windowConfiguration.bounds, + enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction + ) + ensureScrimLayer() + applyTransaction() + } + + private fun onGestureProgress(backEvent: BackEvent) { + val progress = gestureInterpolator.getInterpolation(backEvent.progress) + background.onBackProgressed(progress) + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + val yOffset = getYOffset(currentClosingRect, backEvent.touchY) + currentClosingRect.offset(0f, yOffset) + applyTransform(closingTarget?.leash, currentClosingRect, 1f) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + currentEnteringRect.offset(0f, yOffset) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + applyTransaction() + } + + private fun getYOffset(centeredRect: RectF, touchY: Float): Float { + val screenHeight = backAnimRect.height() + // Base the window movement in the Y axis on the touch movement in the Y axis. + val rawYDelta = touchY - initialTouchPos.y + val yDirection = (if (rawYDelta < 0) -1 else 1) + // limit yDelta interpretation to 1/2 of screen height in either direction + val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f) + val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio) + // limit y-shift so surface never passes 8dp screen margin + val deltaY = yDirection * interpolatedYRatio * max( + 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin + ) + return deltaY + } + + private fun onGestureCommitted() { + if (closingTarget?.leash == null || enteringTarget?.leash == null || + !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid + ) { + finishAnimation() + return + } + + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current + // coordinate of the gesture driven phase. Let's update the start and target rects and kick + // off the animator + startClosingRect.set(currentClosingRect) + startEnteringRect.set(currentEnteringRect) + targetEnteringRect.set(backAnimRect) + targetClosingRect.set(backAnimRect) + targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f) + + val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION) + valueAnimator.addUpdateListener { animation: ValueAnimator -> + val progress = animation.animatedFraction + onPostCommitProgress(progress) + if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) { + background.resetStatusBarCustomization() + } + } + valueAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + background.resetStatusBarCustomization() + finishAnimation() + } + }) + valueAnimator.start() + } + + private fun onPostCommitProgress(linearProgress: Float) { + val closingAlpha = max(1f - linearProgress * 2, 0f) + val progress = postCommitInterpolator.getInterpolation(linearProgress) + scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) } + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + applyTransaction() + } + + private fun finishAnimation() { + enteringTarget?.let { + if (it.leash != null && it.leash.isValid) { + transaction.setCornerRadius(it.leash, 0f) + it.leash.release() + } + enteringTarget = null + } + + closingTarget?.leash?.release() + closingTarget = null + + background.removeBackground(transaction) + applyTransaction() + transformMatrix.reset() + initialTouchPos.set(0f, 0f) + try { + finishCallback?.onAnimationFinished() + } catch (e: RemoteException) { + e.printStackTrace() + } + finishCallback = null + removeScrimLayer() + } + + private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) { + if (leash == null || !leash.isValid) return + val scale = rect.width() / backAnimRect.width() + transformMatrix.reset() + transformMatrix.setScale(scale, scale) + transformMatrix.postTranslate(rect.left, rect.top) + transaction.setAlpha(leash, alpha) + .setMatrix(leash, transformMatrix, tmpFloat9) + .setCrop(leash, backAnimRect) + .setCornerRadius(leash, cornerRadius) + } + + private fun applyTransaction() { + transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId) + transaction.apply() + } + + private fun ensureScrimLayer() { + if (scrimLayer != null) return + val isDarkTheme: Boolean = isDarkMode(context) + val scrimBuilder = SurfaceControl.Builder() + .setName("Cross-Activity back animation scrim") + .setCallsite("CrossActivityBackAnimation") + .setColorLayer() + .setOpaque(false) + .setHidden(false) + + rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder) + scrimLayer = scrimBuilder.build() + val colorComponents = floatArrayOf(0f, 0f, 0f) + maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT + transaction + .setColor(scrimLayer, colorComponents) + .setAlpha(scrimLayer!!, maxScrimAlpha) + .setCrop(scrimLayer!!, closingTarget!!.localBounds) + .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1) + .show(scrimLayer) + } + + private fun removeScrimLayer() { + scrimLayer?.let { + if (it.isValid) { + transaction.remove(it) + applyTransaction() + } + } + scrimLayer = null + } + + + private inner class Callback : IOnBackInvokedCallback.Default() { + override fun onBackStarted(backMotionEvent: BackMotionEvent) { + // in case we're still animating an onBackCancelled event, let's remove the finish- + // callback from the progress animator to prevent calling finishAnimation() before + // restarting a new animation + progressAnimator.removeOnBackCancelledFinishCallback() + + startBackAnimation(backMotionEvent) + progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent -> + onGestureProgress(backEvent) + } + } + + override fun onBackProgressed(backEvent: BackMotionEvent) { + triggerBack = backEvent.triggerBack + progressAnimator.onBackProgressed(backEvent) + } + + override fun onBackCancelled() { + progressAnimator.onBackCancelled { + finishAnimation() + } + } + + override fun onBackInvoked() { + progressAnimator.reset() + onGestureCommitted() + } + } + + private inner class Runner : IRemoteAnimationRunner.Default() { + override fun onAnimationStart( + transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>?, + nonApps: Array<RemoteAnimationTarget>?, + finishedCallback: IRemoteAnimationFinishedCallback + ) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation." + ) + for (a in apps) { + when (a.mode) { + RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a + RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a + } + } + finishCallback = finishedCallback + } + + override fun onAnimationCancelled() { + finishAnimation() + } + } + + companion object { + /** Max scale of the entering/closing window.*/ + private const val MAX_SCALE = 0.9f + + /** Duration of post animation after gesture committed. */ + private const val POST_ANIMATION_DURATION = 300L + + private const val MAX_SCRIM_ALPHA_DARK = 0.8f + private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f + } +} + +private fun isDarkMode(context: Context): Boolean { + return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES +} + +private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) { + require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" } + left = start.left + (target.left - start.left) * progress + top = start.top + (target.top - start.top) * progress + right = start.right + (target.right - start.right) * progress + bottom = start.bottom + (target.bottom - start.bottom) * progress +} + +private fun RectF.scaleCentered( + scale: Float, + pivotX: Float = left + width() / 2, + pivotY: Float = top + height() / 2 +) { + offset(-pivotX, -pivotY) // move pivot to origin + scale(scale) + offset(pivotX, pivotY) // Move back to the original position +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index 4b3154190910..ee898a73a291 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -29,11 +29,13 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.RemoteException; +import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationTarget; @@ -49,7 +51,7 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import javax.inject.Inject; @@ -79,7 +81,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private static final int POST_ANIMATION_DURATION_MS = 500; private final Rect mStartTaskRect = new Rect(); - private final float mCornerRadius; + private float mCornerRadius; // The closing window properties. private final Rect mClosingStartRect = new Rect(); @@ -91,7 +93,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private final PointF mInitialTouchPos = new PointF(); private final Interpolator mPostAnimationInterpolator = Interpolators.EMPHASIZED; - private final Interpolator mProgressInterpolator = Interpolators.STANDARD_DECELERATE; + private final Interpolator mProgressInterpolator = Interpolators.BACK_GESTURE; private final Interpolator mVerticalMoveInterpolator = new DecelerateInterpolator(); private final Matrix mTransformMatrix = new Matrix(); @@ -119,6 +121,11 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { mContext = context; } + @Override + public void onConfigurationChanged(Configuration newConfig) { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + } + private static float mapRange(float value, float min, float max) { return min + (value * (max - min)); } @@ -192,7 +199,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius); applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius); - mTransaction.apply(); + applyTransaction(); mBackground.onBackProgressed(progress); } @@ -242,6 +249,11 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { .setCornerRadius(leash, cornerRadius); } + private void applyTransaction() { + mTransaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + mTransaction.apply(); + } + private void finishAnimation() { if (mEnteringTarget != null) { mEnteringTarget.leash.release(); @@ -255,8 +267,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { if (mBackground != null) { mBackground.removeBackground(mTransaction); } - - mTransaction.apply(); + applyTransaction(); mBackInProgress = false; mTransformMatrix.reset(); mClosingCurrentRect.setEmpty(); @@ -275,8 +286,6 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private void onGestureProgress(@NonNull BackEvent backEvent) { if (!mBackInProgress) { - mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; - mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); mBackInProgress = true; } float progress = backEvent.getProgress(); @@ -305,7 +314,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) { mBackground.resetStatusBarCustomization(); } - mTransaction.apply(); + applyTransaction(); }); valueAnimator.addListener(new AnimatorListenerAdapter() { @@ -326,6 +335,13 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private final class Callback extends IOnBackInvokedCallback.Default { @Override public void onBackStarted(BackMotionEvent backEvent) { + // in case we're still animating an onBackCancelled event, let's remove the finish- + // callback from the progress animator to prevent calling finishAnimation() before + // restarting a new animation + mProgressAnimator.removeOnBackCancelledFinishCallback(); + + mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; + mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); mProgressAnimator.onBackStarted(backEvent, CrossTaskBackAnimation.this::onGestureProgress); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java index 5254ff466123..838dab43d6e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java @@ -29,6 +29,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Rect; import android.os.RemoteException; @@ -54,7 +55,7 @@ import com.android.internal.dynamicanimation.animation.SpringForce; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import javax.inject.Inject; @@ -63,7 +64,7 @@ import javax.inject.Inject; public class CustomizeActivityAnimation extends ShellBackAnimation { private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); private final BackAnimationRunner mBackAnimationRunner; - private final float mCornerRadius; + private float mCornerRadius; private final SurfaceControl.Transaction mTransaction; private final BackAnimationBackground mBackground; private RemoteAnimationTarget mEnteringTarget; @@ -88,6 +89,7 @@ public class CustomizeActivityAnimation extends ShellBackAnimation { final Transformation mTransformation = new Transformation(); private final Choreographer mChoreographer; + private final Context mContext; @Inject public CustomizeActivityAnimation(Context context, BackAnimationBackground background) { @@ -108,6 +110,12 @@ public class CustomizeActivityAnimation extends ShellBackAnimation { .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction; mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance(); + mContext = context; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); } private float getLatestProgress() { @@ -285,6 +293,11 @@ public class CustomizeActivityAnimation extends ShellBackAnimation { private final class Callback extends IOnBackInvokedCallback.Default { @Override public void onBackStarted(BackMotionEvent backEvent) { + // in case we're still animating an onBackCancelled event, let's remove the finish- + // callback from the progress animator to prevent calling finishAnimation() before + // restarting a new animation + mProgressAnimator.removeOnBackCancelledFinishCallback(); + mProgressAnimator.onBackStarted(backEvent, CustomizeActivityAnimation.this::onGestureProgress); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java index dc659197848e..8a0daaa72e24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java @@ -16,6 +16,7 @@ package com.android.wm.shell.back; +import android.content.res.Configuration; import android.window.BackNavigationInfo; import javax.inject.Qualifier; @@ -48,4 +49,8 @@ public abstract class ShellBackAnimation { public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) { return false; } + + void onConfigurationChanged(Configuration newConfig) { + + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java index 26d20972c751..7a6032c60cce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java @@ -18,6 +18,7 @@ package com.android.wm.shell.back; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.res.Configuration; import android.util.Log; import android.util.SparseArray; import android.window.BackNavigationInfo; @@ -27,8 +28,9 @@ public class ShellBackAnimationRegistry { private static final String TAG = "ShellBackPreview"; private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>(); - private final ShellBackAnimation mDefaultCrossActivityAnimation; + private ShellBackAnimation mDefaultCrossActivityAnimation; private final ShellBackAnimation mCustomizeActivityAnimation; + private final ShellBackAnimation mCrossTaskAnimation; public ShellBackAnimationRegistry( @ShellBackAnimation.CrossActivity @Nullable ShellBackAnimation crossActivityAnimation, @@ -57,6 +59,7 @@ public class ShellBackAnimationRegistry { mDefaultCrossActivityAnimation = crossActivityAnimation; mCustomizeActivityAnimation = customizeActivityAnimation; + mCrossTaskAnimation = crossTaskAnimation; // TODO(b/236760237): register dialog close animation when it's completed. } @@ -64,10 +67,18 @@ public class ShellBackAnimationRegistry { void registerAnimation( @BackNavigationInfo.BackTargetType int type, @NonNull BackAnimationRunner runner) { mAnimationDefinition.set(type, runner); + // Only happen in test + if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { + mDefaultCrossActivityAnimation = null; + } } void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) { mAnimationDefinition.remove(type); + // Only happen in test + if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { + mDefaultCrossActivityAnimation = null; + } } /** @@ -125,6 +136,18 @@ public class ShellBackAnimationRegistry { BackNavigationInfo.TYPE_CROSS_ACTIVITY, mDefaultCrossActivityAnimation.getRunner()); } + void onConfigurationChanged(Configuration newConfig) { + if (mCustomizeActivityAnimation != null) { + mCustomizeActivityAnimation.onConfigurationChanged(newConfig); + } + if (mDefaultCrossActivityAnimation != null) { + mDefaultCrossActivityAnimation.onConfigurationChanged(newConfig); + } + if (mCrossTaskAnimation != null) { + mCrossTaskAnimation.onConfigurationChanged(newConfig); + } + } + BackAnimationRunner getAnimationRunnerAndInit(BackNavigationInfo backNavigationInfo) { int type = backNavigationInfo.getType(); // Initiate customized cross-activity animation, or fall back to cross activity animation diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 96aaf02cb5e3..d2958779c0d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -101,13 +101,14 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ShellBackgroundThread; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -454,8 +455,7 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "onActivityRestartAttempt - taskId=%d selecting matching bubble=%s", task.taskId, b.getKey()); - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(b); return; } } @@ -592,13 +592,6 @@ public class BubbleController implements ConfigurationChangeListener, } } - private void openBubbleOverflow() { - ensureBubbleViewsAndWindowCreated(); - mBubbleData.setShowingOverflow(true); - mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); - mBubbleData.setExpanded(true); - } - /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). @@ -708,6 +701,30 @@ public class BubbleController implements ConfigurationChangeListener, return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen(); } + /** + * Returns current {@link BubbleBarLocation} if bubble bar is being used. + * Otherwise returns <code>null</code> + */ + @Nullable + public BubbleBarLocation getBubbleBarLocation() { + if (canShowAsBubbleBar()) { + return mBubblePositioner.getBubbleBarLocation(); + } + return null; + } + + /** + * Update bubble bar location and trigger and update to listeners + */ + public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + if (canShowAsBubbleBar()) { + mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; + mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + } + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -1179,7 +1196,7 @@ public class BubbleController implements ConfigurationChangeListener, */ @VisibleForTesting public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) { - mBubblePositioner.setBubbleBarPosition(bubbleBarBounds); + mBubblePositioner.setBubbleBarBounds(bubbleBarBounds); if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); @@ -1222,8 +1239,7 @@ public class BubbleController implements ConfigurationChangeListener, } if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { // already in the stack - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(b); } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { // promote it out of the overflow promoteBubbleFromOverflow(b); @@ -1234,20 +1250,21 @@ public class BubbleController implements ConfigurationChangeListener, * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble * exists for this entry, and it is able to bubble, a new bubble will be created. * - * This is the method to use when opening a bubble via a notification or in a state where + * <p>This is the method to use when opening a bubble via a notification or in a state where * the device might not be unlocked. * * @param entry the entry to use for the bubble. */ public void expandStackAndSelectBubble(BubbleEntry entry) { + ProtoLog.d(WM_SHELL_BUBBLES, "opening bubble from notification key=%s mIsStatusBarShade=%b", + entry.getKey(), mIsStatusBarShade); if (mIsStatusBarShade) { mNotifEntryToExpandOnShadeUnlock = null; String key = entry.getKey(); Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); if (bubble != null) { - mBubbleData.setSelectedBubble(bubble); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(bubble); } else { bubble = mBubbleData.getOverflowBubbleWithKey(key); if (bubble != null) { @@ -1340,8 +1357,7 @@ public class BubbleController implements ConfigurationChangeListener, } else { // App bubble is not selected, select it & expand Log.i(TAG, " showOrHideAppBubble, expand and select existing app bubble"); - mBubbleData.setSelectedBubble(existingAppBubble); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble); } } else { // Check if it exists in the overflow @@ -2302,6 +2318,17 @@ public class BubbleController implements ConfigurationChangeListener, mMainExecutor.execute(() -> mController.showUserEducation(new Point(positionX, positionY))); } + + @Override + public void setBubbleBarLocation(BubbleBarLocation location) { + mMainExecutor.execute(() -> + mController.setBubbleBarLocation(location)); + } + + @Override + public void setBubbleBarBounds(Rect bubbleBarBounds) { + mMainExecutor.execute(() -> mBubblePositioner.setBubbleBarBounds(bubbleBarBounds)); + } } private class BubblesImpl implements Bubbles { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 6c2f925119f3..ae3d0c559014 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -165,7 +165,7 @@ public class BubbleData { * used when {@link BubbleController#isShowingAsBubbleBar()} is true. */ BubbleBarUpdate getInitialState() { - BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + BubbleBarUpdate bubbleBarUpdate = BubbleBarUpdate.createInitialState(); bubbleBarUpdate.shouldShowEducation = shouldShowEducation; for (int i = 0; i < bubbles.size(); i++) { bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); @@ -255,7 +255,9 @@ public class BubbleData { * Returns a bubble bar update populated with the current list of active bubbles. */ public BubbleBarUpdate getInitialStateForBubbleBar() { - return mStateChange.getInitialState(); + BubbleBarUpdate initialState = mStateChange.getInitialState(); + initialState.bubbleBarLocation = mPositioner.getBubbleBarLocation(); + return initialState; } public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { @@ -363,6 +365,19 @@ public class BubbleData { mSelectedBubble = bubble; } + /** + * Sets the selected bubble and expands it. + * + * <p>This dispatches a single state update for both changes and should be used instead of + * calling {@link #setSelectedBubble(BubbleViewProvider)} followed by + * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleInternal(bubble); + setExpandedInternal(true); + dispatchPendingChanges(); + } + public void setSelectedBubble(BubbleViewProvider bubble) { setSelectedBubbleInternal(bubble); dispatchPendingChanges(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java index 6a5f785504c0..42de401d9db9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java @@ -24,6 +24,7 @@ import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; import android.animation.ArgbEvaluator; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -74,7 +75,7 @@ public class BubbleFlyoutView extends FrameLayout { private final int mFlyoutElevation; private final int mBubbleElevation; - private final int mFloatingBackgroundColor; + private int mFloatingBackgroundColor; private final float mCornerRadius; private final ViewGroup mFlyoutTextContainer; @@ -107,6 +108,9 @@ public class BubbleFlyoutView extends FrameLayout { /** Color of the 'new' dot that the flyout will transform into. */ private int mDotColor; + /** Keeps last used night mode flags **/ + private int mNightModeFlags; + /** The outline of the triangle, used for elevation shadows. */ private final Outline mTriangleOutline = new Outline(); @@ -176,11 +180,8 @@ public class BubbleFlyoutView extends FrameLayout { mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation); final TypedArray ta = mContext.obtainStyledAttributes( - new int[] { - com.android.internal.R.attr.materialColorSurfaceContainer, - android.R.attr.dialogCornerRadius}); - mFloatingBackgroundColor = ta.getColor(0, Color.WHITE); - mCornerRadius = ta.getDimensionPixelSize(1, 0); + new int[] {android.R.attr.dialogCornerRadius}); + mCornerRadius = ta.getDimensionPixelSize(0, 0); ta.recycle(); // Add padding for the pointer on either side, onDraw will draw it in this space. @@ -198,19 +199,17 @@ public class BubbleFlyoutView extends FrameLayout { // Use locale direction so the text is aligned correctly. setLayoutDirection(LAYOUT_DIRECTION_LOCALE); - mBgPaint.setColor(mFloatingBackgroundColor); - mLeftTriangleShape = new ShapeDrawable(TriangleShape.createHorizontal( mPointerSize, mPointerSize, true /* isPointingLeft */)); mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); - mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor); mRightTriangleShape = new ShapeDrawable(TriangleShape.createHorizontal( mPointerSize, mPointerSize, false /* isPointingLeft */)); mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); - mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + + applyConfigurationColors(getResources().getConfiguration()); } @Override @@ -244,6 +243,13 @@ public class BubbleFlyoutView extends FrameLayout { fade(false /* in */, stackPos, hideDot, afterFadeOut); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (applyColorsAccordingToConfiguration(newConfig)) { + invalidate(); + } + } + /* * Fade-out above or fade-in from below. */ @@ -424,6 +430,42 @@ public class BubbleFlyoutView extends FrameLayout { } /** + * Resolving and applying colors according to the ui mode, remembering most recent mode. + * + * @return {@code true} if night mode setting has changed since the last invocation, + * {@code false} otherwise + */ + boolean applyColorsAccordingToConfiguration(Configuration configuration) { + int nightModeFlags = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean flagsChanged = nightModeFlags != mNightModeFlags; + if (flagsChanged) { + mNightModeFlags = nightModeFlags; + applyConfigurationColors(configuration); + } + return flagsChanged; + } + + private void applyConfigurationColors(Configuration configuration) { + int nightModeFlags = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean isNightModeOn = nightModeFlags == Configuration.UI_MODE_NIGHT_YES; + try (TypedArray ta = mContext.obtainStyledAttributes( + new int[]{ + com.android.internal.R.attr.materialColorSurfaceContainer, + com.android.internal.R.attr.materialColorOnSurface, + com.android.internal.R.attr.materialColorOnSurfaceVariant})) { + mFloatingBackgroundColor = ta.getColor(0, + isNightModeOn ? Color.BLACK : Color.WHITE); + mSenderText.setTextColor(ta.getColor(1, + isNightModeOn ? Color.WHITE : Color.BLACK)); + mMessageText.setTextColor(ta.getColor(2, + isNightModeOn ? Color.WHITE : Color.BLACK)); + mBgPaint.setColor(mFloatingBackgroundColor); + mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + } + } + + /** * Renders the background, which is either the rounded 'chat bubble' flyout, or some state * between that and the 'new' dot over the bubbles. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index a5853d621cb5..14c3a0701c83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -32,6 +32,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all @@ -95,6 +96,7 @@ public class BubblePositioner { private PointF mRestingStackPosition; private boolean mShowingInBubbleBar; + private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; private final Rect mBubbleBarBounds = new Rect(); public BubblePositioner(Context context, WindowManager windowManager) { @@ -147,9 +149,10 @@ public class BubblePositioner { mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); if (mShowingInBubbleBar) { - mExpandedViewLargeScreenWidth = isLandscape() - ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT) - : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT); + mExpandedViewLargeScreenWidth = Math.min( + res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), + mPositionRect.width() - 2 * mExpandedViewPadding + ); } else if (mDeviceConfig.isSmallTablet()) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); @@ -797,14 +800,36 @@ public class BubblePositioner { mShowingInBubbleBar = showingInBubbleBar; } + public void setBubbleBarLocation(BubbleBarLocation location) { + mBubbleBarLocation = location; + } + + public BubbleBarLocation getBubbleBarLocation() { + return mBubbleBarLocation; + } + + /** + * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right + */ + public boolean isBubbleBarOnLeft() { + return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl()); + } + /** * Sets the position of the bubble bar in display coordinates. */ - public void setBubbleBarPosition(Rect bubbleBarBounds) { + public void setBubbleBarBounds(Rect bubbleBarBounds) { mBubbleBarBounds.set(bubbleBarBounds); } /** + * Returns the display coordinates of the bubble bar. + */ + public Rect getBubbleBarBounds() { + return mBubbleBarBounds; + } + + /** * How wide the expanded view should be when showing from the bubble bar. */ public int getExpandedViewWidthForBubbleBar(boolean isOverflow) { @@ -815,11 +840,42 @@ public class BubblePositioner { * How tall the expanded view should be when showing from the bubble bar. */ public int getExpandedViewHeightForBubbleBar(boolean isOverflow) { - return isOverflow - ? mOverflowHeight - : getExpandedViewBottomForBubbleBar() - mInsets.top - mExpandedViewPadding; + if (isOverflow) { + return mOverflowHeight; + } else { + return getBubbleBarExpandedViewHeightForLandscape(); + } + } + + /** + * Calculate the height of expanded view in landscape mode regardless current orientation. + * Here is an explanation: + * ------------------------ mScreenRect.top + * | top inset ↕ | + * |----------------------- + * | 16dp spacing ↕ | + * | --------- | --- expanded view top + * | | | | ↑ + * | | | | ↓ expanded view height + * | --------- | --- expanded view bottom + * | 16dp spacing ↕ | ↑ + * | @bubble bar@ | | height of the bubble bar container + * ------------------------ | already includes bottom inset and spacing + * | bottom inset ↕ | ↓ + * |----------------------| --- mScreenRect.bottom + */ + private int getBubbleBarExpandedViewHeightForLandscape() { + int heightOfBubbleBarContainer = + mScreenRect.height() - getExpandedViewBottomForBubbleBar(); + // getting landscape height from screen rect + int expandedViewHeight = Math.min(mScreenRect.width(), mScreenRect.height()); + expandedViewHeight -= heightOfBubbleBarContainer; /* removing bubble container height */ + expandedViewHeight -= mInsets.top; /* removing top inset */ + expandedViewHeight -= mExpandedViewPadding; /* removing spacing */ + return expandedViewHeight; } + /** The bottom position of the expanded view when showing above the bubble bar. */ public int getExpandedViewBottomForBubbleBar() { return mBubbleBarBounds.top - mExpandedViewPadding; @@ -833,9 +889,22 @@ public class BubblePositioner { } /** - * Returns the display coordinates of the bubble bar. + * Get bubble bar expanded view bounds on screen */ - public Rect getBubbleBarBounds() { - return mBubbleBarBounds; + public void getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded, + Rect out) { + final int padding = getBubbleBarExpandedViewPadding(); + final int width = getExpandedViewWidthForBubbleBar(isOverflowExpanded); + final int height = getExpandedViewHeightForBubbleBar(isOverflowExpanded); + + out.set(0, 0, width, height); + int left; + if (onLeft) { + left = getInsets().left + padding; + } else { + left = getAvailableRect().right - width - padding; + } + int top = getExpandedViewBottomForBubbleBar() - height; + out.offsetTo(left, top); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 474430eb44ab..8da85d2d6abf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -82,7 +82,6 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; @@ -95,6 +94,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.bubbles.RelativeTouchListener; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.animation.PhysicsAnimator; import java.io.PrintWriter; import java.math.BigDecimal; @@ -449,17 +449,21 @@ public class BubbleStackView extends FrameLayout @Override public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, - @NonNull MagnetizedObject draggedObject) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + @NonNull MagnetizedObject<?> draggedObject) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; animateDismissBubble(view, true); } } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - @NonNull MagnetizedObject draggedObject, + @NonNull MagnetizedObject<?> draggedObject, float velX, float velY, boolean wasFlungOut) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; animateDismissBubble(view, false); if (wasFlungOut) { @@ -474,7 +478,9 @@ public class BubbleStackView extends FrameLayout @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, @NonNull MagnetizedObject<?> draggedObject) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; mExpandedAnimationController.dismissDraggedOutBubble( view /* bubble */, mDismissView.getHeight() /* translationYBy */, @@ -1019,6 +1025,7 @@ public class BubbleStackView extends FrameLayout WindowManager.class))); onDisplaySizeChanged(); mExpandedAnimationController.updateResources(); + mExpandedAnimationController.onOrientationChanged(); mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); @@ -2474,11 +2481,12 @@ public class BubbleStackView extends FrameLayout // Let the expanded animation controller know that it shouldn't animate child adds/reorders // since we're about to animate collapsed. mExpandedAnimationController.notifyPreparingToCollapse(); - + final PointF collapsePosition = mStackAnimationController + .getStackPositionAlongNearestHorizontalEdge(); updateOverflowDotVisibility(false /* expanding */); final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(), + collapsePosition, /* fadeBubblesDuringCollapse= */ mRemovingLastBubbleWhileExpanded, () -> { mBubbleContainer.setActiveController(mStackAnimationController); @@ -2501,7 +2509,8 @@ public class BubbleStackView extends FrameLayout } mExpandedViewAnimationController.reset(); }; - mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after); + mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after, + collapsePosition); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { // When the animation completes, we should no longer be showing the content. // This won't actually update content visibility immediately, if we are currently diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 26077cf7057b..127a49fc7875 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -37,8 +37,8 @@ import android.window.ScreenCapture.SynchronousScreenCaptureListener; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 7a5afec934f5..c9f0f0d61713 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles; import android.content.Intent; import android.graphics.Rect; import com.android.wm.shell.bubbles.IBubblesListener; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; /** * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when @@ -42,4 +43,7 @@ interface IBubbles { oneway void showUserEducation(in int positionX, in int positionY) = 8; + oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; + + oneway void setBubbleBarBounds(in Rect bubbleBarBounds) = 10; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index 7798aa753aa2..1fb966f80ca0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -34,12 +34,12 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BadgedImageView; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.animation.PhysicsAnimator; import com.google.android.collect.Sets; @@ -614,6 +614,14 @@ public class ExpandedAnimationController } } + /** + * Call to update the bubble positions after an orientation change. + */ + public void onOrientationChanged() { + if (mLayout == null) return; + updateBubblePositions(); + } + private void updateBubblePositions() { if (mAnimatingExpand || mAnimatingCollapse) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java index 8a33780bc8d5..41755293f382 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java @@ -15,6 +15,8 @@ */ package com.android.wm.shell.bubbles.animation; +import android.graphics.PointF; + import com.android.wm.shell.bubbles.BubbleExpandedView; /** @@ -55,8 +57,9 @@ public interface ExpandedViewAnimationController { * @param startStackCollapse runnable that is triggered when bubbles can start moving back to * their collapsed location * @param after runnable to run after animation is complete + * @param collapsePosition the position on screen the stack will collapse to */ - void animateCollapse(Runnable startStackCollapse, Runnable after); + void animateCollapse(Runnable startStackCollapse, Runnable after, PointF collapsePosition); /** * Animate the view back to fully expanded state. @@ -69,6 +72,22 @@ public interface ExpandedViewAnimationController { void animateForImeVisibilityChange(boolean visible); /** + * Whether this controller should also animate the expansion for the bubble + */ + boolean shouldAnimateExpansion(); + + /** + * Animate the expansion of the bubble. + * + * @param startDelayMillis how long to delay starting the expansion animation + * @param after runnable to run after the animation is complete + * @param collapsePosition the position on screen the stack will collapse to (and expand from) + * @param bubblePosition the position of the bubble on screen that the view is associated with + */ + void animateExpansion(long startDelayMillis, Runnable after, PointF collapsePosition, + PointF bubblePosition); + + /** * Reset the view to fully expanded state */ void reset(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java index e43609fe8ff0..aa4129a14dbc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java @@ -28,6 +28,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.PointF; import android.view.HapticFeedbackConstants; import android.view.ViewConfiguration; @@ -187,9 +188,11 @@ public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimatio } @Override - public void animateCollapse(Runnable startStackCollapse, Runnable after) { - ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate collapse swipeVel=%f minFlingVel=%d", - mSwipeUpVelocity, mMinFlingVelocity); + public void animateCollapse(Runnable startStackCollapse, Runnable after, + PointF collapsePosition) { + ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate collapse swipeVel=%f minFlingVel=%d" + + " collapsePosition=%f,%f", mSwipeUpVelocity, mMinFlingVelocity, + collapsePosition.x, collapsePosition.y); if (mExpandedView != null) { // Mark it as animating immediately to avoid updates to the view before animation starts mExpandedView.setAnimating(true); @@ -274,6 +277,17 @@ public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimatio } @Override + public boolean shouldAnimateExpansion() { + return false; + } + + @Override + public void animateExpansion(long startDelayMillis, Runnable after, PointF collapsePosition, + PointF bubblePosition) { + // TODO - animate + } + + @Override public void reset() { ProtoLog.d(WM_SHELL_BUBBLES, "reset expandedView collapsed state"); if (mExpandedView == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java index ed00da848a14..bfddff0f72e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java @@ -378,6 +378,8 @@ public class PhysicsAnimationLayout extends FrameLayout { } final int oldIndex = indexOfChild(view); + if (oldIndex == index) return; + super.removeView(view); if (view.getParent() != null) { // View still has a parent. This could have been added as a transient view. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index bb0dd95b042f..47d4d07500d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -38,12 +38,12 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.wm.shell.R; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BadgedImageView; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.animation.PhysicsAnimator; import com.google.android.collect.Sets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 8946f41e96a7..45ad6319bbf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -43,12 +43,12 @@ import android.widget.FrameLayout; import androidx.annotation.Nullable; import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget; +import com.android.wm.shell.shared.animation.PhysicsAnimator; /** * Helper class to animate a {@link BubbleBarExpandedView} on a bubble. @@ -166,13 +166,8 @@ public class BubbleBarAnimationHelper { bbev.setTaskViewAlpha(0f); bbev.setVisibility(VISIBLE); - // Set the pivot point for the scale, so the view animates out from the bubble bar. - Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); - mExpandedViewContainerMatrix.setScale( - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - bubbleBarBounds.centerX(), - bubbleBarBounds.top); + setScaleFromBubbleBar(mExpandedViewContainerMatrix, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT); bbev.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -214,8 +209,8 @@ public class BubbleBarAnimationHelper { } bbev.setScaleX(1f); bbev.setScaleY(1f); - mExpandedViewContainerMatrix.setScaleX(1f); - mExpandedViewContainerMatrix.setScaleY(1f); + + setScaleFromBubbleBar(mExpandedViewContainerMatrix, 1f); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) @@ -240,6 +235,16 @@ public class BubbleBarAnimationHelper { mExpandedViewAlphaAnimator.reverse(); } + private void setScaleFromBubbleBar(AnimatableScaleMatrix matrix, float scale) { + // Set the pivot point for the scale, so the view animates out from the bubble bar. + Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); + matrix.setScale( + scale, + scale, + bubbleBarBounds.centerX(), + bubbleBarBounds.top); + } + /** * Animate the expanded bubble when it is being dragged */ @@ -477,7 +482,7 @@ public class BubbleBarAnimationHelper { private Point getExpandedViewRestPosition(Size size) { final int padding = mPositioner.getBubbleBarExpandedViewPadding(); Point point = new Point(); - if (mLayerView.isOnLeft()) { + if (mPositioner.isBubbleBarOnLeft()) { point.x = mPositioner.getInsets().left + padding; } else { point.x = mPositioner.getAvailableRect().width() - size.getWidth() - padding; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index 7d37d6068dfb..fe9c4d4c9094 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.bar import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View +import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.common.bubbles.DismissView import com.android.wm.shell.common.bubbles.RelativeTouchListener import com.android.wm.shell.common.magnetictarget.MagnetizedObject @@ -29,7 +30,9 @@ class BubbleBarExpandedViewDragController( private val expandedView: BubbleBarExpandedView, private val dismissView: DismissView, private val animationHelper: BubbleBarAnimationHelper, - private val onDismissed: () -> Unit + private val bubblePositioner: BubblePositioner, + private val pinController: BubbleExpandedViewPinController, + private val dragListener: DragListener ) { var isStuckToDismiss: Boolean = false @@ -45,11 +48,11 @@ class BubbleBarExpandedViewDragController( magnetizedExpandedView.magnetListener = MagnetListener() magnetizedExpandedView.animateStuckToTarget = { - target: MagnetizedObject.MagneticTarget, - _: Float, - _: Float, - _: Boolean, - after: (() -> Unit)? -> + target: MagnetizedObject.MagneticTarget, + _: Float, + _: Float, + _: Boolean, + after: (() -> Unit)? -> animationHelper.animateIntoTarget(target, after) } @@ -73,13 +76,25 @@ class BubbleBarExpandedViewDragController( } } + /** Listener to get notified about drag events */ + interface DragListener { + /** + * Bubble bar was released + * + * @param inDismiss `true` if view was release in dismiss target + */ + fun onReleased(inDismiss: Boolean) + } + private inner class HandleDragListener : RelativeTouchListener() { private var isMoving = false override fun onDown(v: View, ev: MotionEvent): Boolean { // While animating, don't allow new touch events - return !expandedView.isAnimating + if (expandedView.isAnimating) return false + pinController.onDragStart(bubblePositioner.isBubbleBarOnLeft) + return true } override fun onMove( @@ -97,6 +112,7 @@ class BubbleBarExpandedViewDragController( expandedView.translationX = expandedViewInitialTranslationX + dx expandedView.translationY = expandedViewInitialTranslationY + dy dismissView.show() + pinController.onDragUpdate(ev.rawX, ev.rawY) } override fun onUp( @@ -113,12 +129,15 @@ class BubbleBarExpandedViewDragController( } override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { + isStuckToDismiss = false finishDrag() } private fun finishDrag() { if (!isStuckToDismiss) { animationHelper.animateToRestPosition() + pinController.onDragEnd() + dragListener.onReleased(inDismiss = false) dismissView.hide() } isMoving = false @@ -127,30 +146,32 @@ class BubbleBarExpandedViewDragController( private inner class MagnetListener : MagnetizedObject.MagnetListener { override fun onStuckToTarget( - target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*> + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*> ) { isStuckToDismiss = true + pinController.setDropTargetHidden(true) } override fun onUnstuckFromTarget( - target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*>, - velX: Float, - velY: Float, - wasFlungOut: Boolean + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*>, + velX: Float, + velY: Float, + wasFlungOut: Boolean ) { isStuckToDismiss = false animationHelper.animateUnstuckFromDismissView(target) + pinController.setDropTargetHidden(false) } override fun onReleasedInTarget( - target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*> + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*> ) { - onDismissed() + dragListener.onReleased(inDismiss = true) + pinController.onDragEnd() dismissView.hide() } } } - diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 42799d975e1b..62cc4da3193e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -33,7 +33,6 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; -import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -42,6 +41,7 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; import com.android.wm.shell.common.bubbles.DismissView; import kotlin.Unit; @@ -68,6 +68,7 @@ public class BubbleBarLayerView extends FrameLayout private final BubbleBarAnimationHelper mAnimationHelper; private final BubbleEducationViewController mEducationViewController; private final View mScrimView; + private final BubbleExpandedViewPinController mBubbleExpandedViewPinController; @Nullable private BubbleViewProvider mExpandedBubble; @@ -112,6 +113,10 @@ public class BubbleBarLayerView extends FrameLayout setUpDismissView(); + mBubbleExpandedViewPinController = new BubbleExpandedViewPinController( + context, this, mPositioner); + mBubbleExpandedViewPinController.setListener(mBubbleController::setBubbleBarLocation); + setOnClickListener(view -> hideMenuOrCollapse()); } @@ -155,12 +160,6 @@ public class BubbleBarLayerView extends FrameLayout return mIsExpanded; } - // TODO(b/313661121) - when dragging is implemented, check user setting first - /** Whether the expanded view is positioned on the left or right side of the screen. */ - public boolean isOnLeft() { - return getLayoutDirection() == LAYOUT_DIRECTION_RTL; - } - /** Shows the expanded view of the provided bubble. */ public void showExpandedView(BubbleViewProvider b) { BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); @@ -207,15 +206,18 @@ public class BubbleBarLayerView extends FrameLayout } }); + DragListener dragListener = inDismiss -> { + if (inDismiss && mExpandedBubble != null) { + mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE); + } + }; mDragController = new BubbleBarExpandedViewDragController( mExpandedView, mDismissView, mAnimationHelper, - () -> { - mBubbleController.dismissBubble(mExpandedBubble.getKey(), - DISMISS_USER_GESTURE); - return Unit.INSTANCE; - }); + mPositioner, + mBubbleExpandedViewPinController, + dragListener); addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); } @@ -324,10 +326,7 @@ public class BubbleBarLayerView extends FrameLayout } mDismissView = new DismissView(getContext()); DismissViewUtils.setup(mDismissView); - int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation); - addView(mDismissView); - mDismissView.setElevation(elevation); } /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ @@ -343,21 +342,16 @@ public class BubbleBarLayerView extends FrameLayout /** Updates the expanded view size and position. */ private void updateExpandedView() { - if (mExpandedView == null) return; + if (mExpandedView == null || mExpandedBubble == null) return; boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); - final int padding = mPositioner.getBubbleBarExpandedViewPadding(); - final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); - final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); + mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(), + isOverflowExpanded, mTempRect); FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams(); - lp.width = width; - lp.height = height; + lp.width = mTempRect.width(); + lp.height = mTempRect.height(); mExpandedView.setLayoutParams(lp); - if (isOnLeft()) { - mExpandedView.setX(mPositioner.getInsets().left + padding); - } else { - mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); - } - mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); + mExpandedView.setX(mTempRect.left); + mExpandedView.setY(mTempRect.top); mExpandedView.updateLocation(); } @@ -386,4 +380,5 @@ public class BubbleBarLayerView extends FrameLayout outRegion.op(mTempRect, Region.Op.UNION); } } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 81e7582e0dba..02918db124e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.wm.shell.R; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.Bubble; +import com.android.wm.shell.shared.animation.PhysicsAnimator; import java.util.ArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt index ee552ae204b8..e108f7be48c7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt @@ -28,7 +28,6 @@ import androidx.core.view.doOnLayout import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.wm.shell.R -import com.android.wm.shell.animation.PhysicsAnimator import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME @@ -37,6 +36,7 @@ import com.android.wm.shell.bubbles.BubbleViewProvider import com.android.wm.shell.bubbles.setup import com.android.wm.shell.common.bubbles.BubblePopupDrawable import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.shared.animation.PhysicsAnimator import kotlin.math.roundToInt /** Manages bubble education presentation and animation */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt new file mode 100644 index 000000000000..5d391eca070c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Rect +import android.graphics.RectF +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting +import androidx.core.view.updateLayoutParams +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BubbleBarLocation + +/** + * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar + * expanded view + */ +class BubbleExpandedViewPinController( + private val context: Context, + private val container: FrameLayout, + private val positioner: BubblePositioner +) : BaseBubblePinController() { + + private var dropTargetView: View? = null + private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } + + override fun getScreenCenterX(): Int { + return positioner.screenRect.centerX() + } + + override fun getExclusionRect(): RectF { + val rect = + RectF( + 0f, + 0f, + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_width), + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_height) + ) + + val screenRect = positioner.screenRect + // Center it around the bottom center of the screen + rect.offsetTo( + screenRect.exactCenterX() - rect.width() / 2f, + screenRect.bottom - rect.height() + ) + return rect + } + + override fun createDropTargetView(): View { + return LayoutInflater.from(context) + .inflate(R.layout.bubble_bar_drop_target, container, false /* attachToRoot */) + .also { view: View -> + dropTargetView = view + // Add at index 0 to ensure it does not cover the bubble + container.addView(view, 0) + } + } + + override fun getDropTargetView(): View? { + return dropTargetView + } + + override fun removeDropTargetView(view: View) { + container.removeView(view) + dropTargetView = null + } + + override fun updateLocation(location: BubbleBarLocation) { + val view = dropTargetView ?: return + getBounds(location.isOnLeft(view.isLayoutRtl), tempRect) + view.updateLayoutParams<FrameLayout.LayoutParams> { + width = tempRect.width() + height = tempRect.height() + } + view.x = tempRect.left.toFloat() + view.y = tempRect.top.toFloat() + } + + private fun getBounds(onLeft: Boolean, out: Rect) { + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOverflowExpanded */, out) + val centerX = out.centerX() + val centerY = out.centerY() + out.scale(DROP_TARGET_SCALE) + // Move rect center back to the same position as before scale + out.offset(centerX - out.centerX(), centerY - out.centerY()) + } + + companion object { + @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java index 8b4ac1a8dc79..d17e8620ff12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java @@ -107,7 +107,7 @@ public class DevicePostureController { DeviceStateManager.class); if (deviceStateManager != null) { deviceStateManager.registerCallback(mMainExecutor, state -> onDevicePostureChanged( - mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN))); + mDeviceStateToPostureMap.get(state.getIdentifier(), DEVICE_POSTURE_UNKNOWN))); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index b828aac39040..2873d58439cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -28,7 +28,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 8353900be0ef..dcbc72ab0d32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -34,7 +34,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index ca06024a9adb..55dc793cc3b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -30,7 +30,7 @@ import android.view.inputmethod.ImeTracker; import androidx.annotation.BinderThread; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt index 4c34971c4fb1..9e8dfb5f0c6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt @@ -21,11 +21,9 @@ import android.content.Context import android.content.pm.LauncherApps import android.content.pm.PackageManager import android.os.UserHandle -import android.view.WindowManager import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.R -import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL import com.android.wm.shell.util.KtProtoLog import java.util.Arrays @@ -37,7 +35,8 @@ class MultiInstanceHelper @JvmOverloads constructor( private val context: Context, private val packageManager: PackageManager, private val staticAppsSupportingMultiInstance: Array<String> = context.resources - .getStringArray(R.array.config_appsSupportMultiInstancesSplit)) { + .getStringArray(R.array.config_appsSupportMultiInstancesSplit), + private val supportsMultiInstanceProperty: Boolean) { /** * Returns whether a specific component desires to be launched in multiple instances. @@ -59,6 +58,11 @@ class MultiInstanceHelper @JvmOverloads constructor( } } + if (!supportsMultiInstanceProperty) { + // If not checking the multi-instance properties, then return early + return false; + } + // Check the activity property first try { val activityProp = packageManager.getProperty( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java index 4c0281dcc517..e261d92bda5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -16,6 +16,8 @@ package com.android.wm.shell.common; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL; + import android.annotation.BinderThread; import android.annotation.NonNull; import android.os.RemoteException; @@ -26,6 +28,7 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.transition.LegacyTransitions; import java.util.ArrayList; @@ -204,6 +207,7 @@ public final class SyncTransactionQueue { @Override public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) { + ProtoLog.v(WM_SHELL, "SyncTransactionQueue.onTransactionReady(): syncId=%d", id); mMainExecutor.execute(() -> { synchronized (mQueue) { if (mId != id) { @@ -223,6 +227,8 @@ public final class SyncTransactionQueue { Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e); } } else { + ProtoLog.v(WM_SHELL, + "SyncTransactionQueue.onTransactionReady(): syncId=%d apply", id); t.apply(); t.close(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index e4cf6d13cb1f..98dccbbe33e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -48,6 +48,7 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.InputTransferToken; @@ -348,7 +349,7 @@ public class SystemWindows { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration newMergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) {} + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {} @Override public void insetsControlChanged(InsetsState insetsState, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java index 53683c67d825..43c92cab6a68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -33,7 +33,7 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.lang.annotation.Retention; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java deleted file mode 100644 index 4009ad21b9b8..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.android.wm.shell.common.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** - * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync - * instead of the app vsync. - */ -@Documented -@Inherited -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface ChoreographerSfVsync {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java deleted file mode 100644 index 7560f71d1f98..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.wm.shell.common.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** Annotates a method or class that is called from an external thread to the Shell threads. */ -@Documented -@Inherited -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface ExternalThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java deleted file mode 100644 index 0479f8780c79..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.wm.shell.common.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */ -@Documented -@Inherited -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface ShellAnimationThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java deleted file mode 100644 index 423f4ce3bfd4..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.wm.shell.common.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** Annotates a method or qualifies a provider that runs on the Shell main-thread */ -@Documented -@Inherited -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface ShellMainThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt new file mode 100644 index 000000000000..630ad6e7cafe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.bubbles + +import android.graphics.RectF +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ObjectAnimator +import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT + +/** + * Base class for common logic shared between different bubble views to support pinning bubble bar + * to left or right edge of screen. + * + * Handles drag events and allows a [LocationChangeListener] to be registered that is notified when + * location of the bubble bar should change. + * + * Shows a drop target when releasing a view would update the [BubbleBarLocation]. + */ +abstract class BaseBubblePinController { + + private var onLeft = false + private var dismissZone: RectF? = null + private var screenCenterX = 0 + private var listener: LocationChangeListener? = null + private var dropTargetAnimator: ObjectAnimator? = null + + /** + * Signal the controller that dragging interaction has started. + * + * @param initialLocationOnLeft side of the screen where bubble bar is pinned to + */ + fun onDragStart(initialLocationOnLeft: Boolean) { + onLeft = initialLocationOnLeft + dismissZone = getExclusionRect() + screenCenterX = getScreenCenterX() + } + + /** View has moved to [x] and [y] screen coordinates */ + fun onDragUpdate(x: Float, y: Float) { + if (dismissZone?.contains(x, y) == true) return + + if (onLeft && x > screenCenterX) { + onLeft = false + onLocationChange(RIGHT) + } else if (!onLeft && x < screenCenterX) { + onLeft = true + onLocationChange(LEFT) + } + } + + /** Temporarily hide the drop target view */ + fun setDropTargetHidden(hidden: Boolean) { + val targetView = getDropTargetView() ?: return + if (hidden) { + targetView.animateOut() + } else { + targetView.animateIn() + } + } + + /** Signal the controller that dragging interaction has finished. */ + fun onDragEnd() { + getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + dismissZone = null + } + + /** + * [LocationChangeListener] that is notified when dragging interaction has resulted in bubble + * bar to be pinned on the other edge + */ + fun setListener(listener: LocationChangeListener?) { + this.listener = listener + } + + /** Get screen center coordinate on the x axis. */ + protected abstract fun getScreenCenterX(): Int + + /** Optional exclusion rect where drag interactions are not processed */ + protected abstract fun getExclusionRect(): RectF? + + /** Create the drop target view and attach it to the parent */ + protected abstract fun createDropTargetView(): View + + /** Get the drop target view if it exists */ + protected abstract fun getDropTargetView(): View? + + /** Remove the drop target view */ + protected abstract fun removeDropTargetView(view: View) + + /** Update size and location of the drop target view */ + protected abstract fun updateLocation(location: BubbleBarLocation) + + private fun onLocationChange(location: BubbleBarLocation) { + showDropTarget(location) + listener?.onChange(location) + } + + private fun showDropTarget(location: BubbleBarLocation) { + val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f } + if (targetView.alpha > 0) { + targetView.animateOut { + updateLocation(location) + targetView.animateIn() + } + } else { + updateLocation(location) + targetView.animateIn() + } + } + + private fun View.animateIn() { + dropTargetAnimator?.cancel() + dropTargetAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f) + .setDuration(DROP_TARGET_ALPHA_IN_DURATION) + .addEndAction { dropTargetAnimator = null } + dropTargetAnimator?.start() + } + + private fun View.animateOut(endAction: Runnable? = null) { + dropTargetAnimator?.cancel() + dropTargetAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f) + .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) + .addEndAction { + endAction?.run() + dropTargetAnimator = null + } + dropTargetAnimator?.start() + } + + private fun <T : Animator> T.addEndAction(runnable: Runnable): T { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + runnable.run() + } + } + ) + return this + } + + /** Receive updates on location changes */ + interface LocationChangeListener { + /** + * Bubble bar [BubbleBarLocation] has changed as a result of dragging + * + * Triggered when drag gesture passes the middle of the screen and before touch up. Can be + * triggered multiple times per gesture. + * + * @param location new location as a result of the ongoing drag operation + */ + fun onChange(location: BubbleBarLocation) + } + + companion object { + @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L + @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl new file mode 100644 index 000000000000..3c5beeb48806 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.bubbles; + +parcelable BubbleBarLocation;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt new file mode 100644 index 000000000000..f0bdfdef1073 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.bubbles + +import android.os.Parcel +import android.os.Parcelable + +/** + * The location of the bubble bar. + */ +enum class BubbleBarLocation : Parcelable { + /** + * Place bubble bar at the default location for the chosen system language. + * If an RTL language is used, it is on the left. Otherwise on the right. + */ + DEFAULT, + /** Default bubble bar location is overridden. Place bubble bar on the left. */ + LEFT, + /** Default bubble bar location is overridden. Place bubble bar on the right. */ + RIGHT; + + /** + * Returns whether bubble bar is pinned to the left edge or right edge. + */ + fun isOnLeft(isRtl: Boolean): Boolean { + if (this == DEFAULT) { + return isRtl + } + return this == LEFT + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object { + @JvmField + val CREATOR = object : Parcelable.Creator<BubbleBarLocation> { + override fun createFromParcel(parcel: Parcel): BubbleBarLocation { + return parcel.readString()?.let { valueOf(it) } ?: DEFAULT + } + + override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java index fc627a8dcb36..e5f6c370da84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java @@ -33,6 +33,7 @@ public class BubbleBarUpdate implements Parcelable { public static final String BUNDLE_KEY = "update"; + public final boolean initialState; public boolean expandedChanged; public boolean expanded; public boolean shouldShowEducation; @@ -46,6 +47,8 @@ public class BubbleBarUpdate implements Parcelable { public String suppressedBubbleKey; @Nullable public String unsupressedBubbleKey; + @Nullable + public BubbleBarLocation bubbleBarLocation; // This is only populated if bubbles have been removed. public List<RemovedBubble> removedBubbles = new ArrayList<>(); @@ -56,10 +59,17 @@ public class BubbleBarUpdate implements Parcelable { // This is only populated the first time a listener is connected so it gets the current state. public List<BubbleInfo> currentBubbleList = new ArrayList<>(); + public BubbleBarUpdate() { + this(false); + } + + private BubbleBarUpdate(boolean initialState) { + this.initialState = initialState; } public BubbleBarUpdate(Parcel parcel) { + initialState = parcel.readBoolean(); expandedChanged = parcel.readBoolean(); expanded = parcel.readBoolean(); shouldShowEducation = parcel.readBoolean(); @@ -75,6 +85,8 @@ public class BubbleBarUpdate implements Parcelable { parcel.readStringList(bubbleKeysInOrder); currentBubbleList = parcel.readParcelableList(new ArrayList<>(), BubbleInfo.class.getClassLoader()); + bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(), + BubbleBarLocation.class); } /** @@ -89,12 +101,15 @@ public class BubbleBarUpdate implements Parcelable { || !bubbleKeysInOrder.isEmpty() || suppressedBubbleKey != null || unsupressedBubbleKey != null - || !currentBubbleList.isEmpty(); + || !currentBubbleList.isEmpty() + || bubbleBarLocation != null; } @Override public String toString() { - return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + return "BubbleBarUpdate{" + + " initialState=" + initialState + + " expandedChanged=" + expandedChanged + " expanded=" + expanded + " selectedBubbleKey=" + selectedBubbleKey + " shouldShowEducation=" + shouldShowEducation @@ -105,6 +120,7 @@ public class BubbleBarUpdate implements Parcelable { + " removedBubbles=" + removedBubbles + " bubbles=" + bubbleKeysInOrder + " currentBubbleList=" + currentBubbleList + + " bubbleBarLocation=" + bubbleBarLocation + " }"; } @@ -115,6 +131,7 @@ public class BubbleBarUpdate implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBoolean(initialState); parcel.writeBoolean(expandedChanged); parcel.writeBoolean(expanded); parcel.writeBoolean(shouldShowEducation); @@ -126,6 +143,16 @@ public class BubbleBarUpdate implements Parcelable { parcel.writeParcelableList(removedBubbles, flags); parcel.writeStringList(bubbleKeysInOrder); parcel.writeParcelableList(currentBubbleList, flags); + parcel.writeParcelable(bubbleBarLocation, flags); + } + + /** + * Create update for initial set of values. + * <p> + * Used when bubble bar is newly created. + */ + public static BubbleBarUpdate createInitialState() { + return new BubbleBarUpdate(true); } @NonNull diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt index 9094739d0d88..e06de9e9353c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt @@ -35,7 +35,7 @@ import androidx.core.content.ContextCompat import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW -import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimator /** * View that handles interactions between DismissCircleView and BubbleStackView. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt index 11e477716eb0..123d4dc49199 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt @@ -28,7 +28,7 @@ import android.view.ViewConfiguration import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce -import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimator import kotlin.math.abs import kotlin.math.hypot diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java index 317e48e19c13..c421dec025f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java @@ -28,7 +28,7 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import java.io.PrintWriter; import java.util.Map; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt new file mode 100644 index 000000000000..6781d08c9904 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("AppCompatUtils") + +package com.android.wm.shell.compatui + +import android.app.TaskInfo +fun isSingleTopActivityTranslucent(task: TaskInfo) = + task.isTopActivityTransparent && task.numActivities == 1 + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java index fa2e23647a39..cf3ad4299cea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java @@ -24,8 +24,8 @@ import android.provider.DeviceConfig; import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.shared.annotations.ShellMainThread; import javax.inject.Inject; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 216da070754b..011093718671 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -31,10 +31,9 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.pip.TvPipModule; -import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.tv.TvSplitScreenController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 8b2ec0a35685..73228de83c0f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -29,6 +29,7 @@ import android.window.SystemPerformanceHinter; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.ProtoLogController; import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; @@ -57,10 +58,6 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ShellAnimationThread; -import com.android.wm.shell.common.annotations.ShellBackgroundThread; -import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.common.annotations.ShellSplashscreenThread; import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -91,6 +88,11 @@ import com.android.wm.shell.performance.PerfHintController; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.shared.ShellTransitions; +import com.android.wm.shell.shared.annotations.ShellAnimationThread; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -105,7 +107,6 @@ import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.HomeTransitionObserver; -import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -326,7 +327,8 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static MultiInstanceHelper provideMultiInstanceHelper(Context context) { - return new MultiInstanceHelper(context, context.getPackageManager()); + return new MultiInstanceHelper(context, context.getPackageManager(), + Flags.supportsMultiInstanceSystemUi()); } // @@ -846,8 +848,10 @@ public abstract class WMShellBaseModule { static ShellController provideShellController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, + DisplayInsetsController displayInsetsController, @ShellMainThread ShellExecutor mainExecutor) { - return new ShellController(context, shellInit, shellCommandHandler, mainExecutor); + return new ShellController(context, shellInit, shellCommandHandler, + displayInsetsController, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index 0cc545a7724a..c5644a8f6876 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -33,11 +33,11 @@ import androidx.annotation.Nullable; import com.android.wm.shell.R; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ExternalMainThread; -import com.android.wm.shell.common.annotations.ShellAnimationThread; -import com.android.wm.shell.common.annotations.ShellBackgroundThread; -import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.common.annotations.ShellSplashscreenThread; +import com.android.wm.shell.shared.annotations.ExternalMainThread; +import com.android.wm.shell.shared.annotations.ShellAnimationThread; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; import dagger.Module; import dagger.Provides; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index fb3c35b6a1e3..1408eadf544e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -52,14 +52,14 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ShellAnimationThread; -import com.android.wm.shell.common.annotations.ShellBackgroundThread; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; @@ -75,6 +75,9 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.shared.annotations.ShellAnimationThread; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -239,6 +242,7 @@ public abstract class WMShellModule { mainChoreographer, taskOrganizer, displayController, + rootTaskDisplayAreaOrganizer, syncQueue, transitions); } @@ -509,6 +513,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, @@ -518,7 +523,8 @@ public abstract class WMShellModule { displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, + dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor); } @@ -562,6 +568,34 @@ public abstract class WMShellModule { return new DesktopModeTaskRepository(); } + @WMSingleton + @Provides + static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + Transitions transitions, + ShellInit shellInit + ) { + return desktopModeTaskRepository.flatMap(repository -> + Optional.of(new DesktopTasksTransitionObserver(repository, transitions, shellInit)) + ); + } + + @WMSingleton + @Provides + static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( + ShellInit shellInit, + Transitions transitions, + DesktopModeEventLogger desktopModeEventLogger) { + return new DesktopModeLoggerTransitionObserver( + shellInit, transitions, desktopModeEventLogger); + } + + @WMSingleton + @Provides + static DesktopModeEventLogger provideDesktopModeEventLogger() { + return new DesktopModeEventLogger(); + } + // // Drag and drop // @@ -602,7 +636,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - DefaultMixedHandler defaultMixedHandler) { + DefaultMixedHandler defaultMixedHandler, + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 1e3d7fb06da2..d644006cde81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -29,7 +29,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -56,6 +55,7 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.pip.phone.PipController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 458ea05e620d..4eff3f03670e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -23,21 +23,27 @@ import android.os.Handler; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; +import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; +import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -62,6 +68,7 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, + PipTouchHandler pipTouchHandler, @NonNull PipScheduler pipScheduler) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); @@ -93,8 +100,9 @@ public abstract class Pip2Module { @Provides static PipScheduler providePipScheduler(Context context, PipBoundsState pipBoundsState, - @ShellMainThread ShellExecutor mainExecutor) { - return new PipScheduler(context, pipBoundsState, mainExecutor); + @ShellMainThread ShellExecutor mainExecutor, + ShellTaskOrganizer shellTaskOrganizer) { + return new PipScheduler(context, pipBoundsState, mainExecutor, shellTaskOrganizer); } @WMSingleton @@ -108,4 +116,34 @@ public abstract class Pip2Module { return new PhonePipMenuController(context, pipBoundsState, pipMediaController, systemWindows, pipUiEventLogger, mainExecutor, mainHandler); } + + + @WMSingleton + @Provides + static PipTouchHandler providePipTouchHandler(Context context, + ShellInit shellInit, + PhonePipMenuController menuPhoneController, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + @NonNull SizeSpecSource sizeSpecSource, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, + pipBoundsState, sizeSpecSource, pipMotionHelper, floatingContentCoordinator, + pipUiEventLogger, mainExecutor, pipPerfHintControllerOptional); + } + + @WMSingleton + @Provides + static PipMotionHelper providePipMotionHelper(Context context, + PipBoundsState pipBoundsState, PhonePipMenuController menuController, + PipSnapAlgorithm pipSnapAlgorithm, + FloatingContentCoordinator floatingContentCoordinator, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, + floatingContentCoordinator, pipPerfHintControllerOptional); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java index 54c2aeab4976..8d1b15c1e631 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java @@ -29,7 +29,6 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.LegacySizeSpecSource; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipDisplayLayoutState; @@ -53,6 +52,7 @@ import com.android.wm.shell.pip.tv.TvPipMenuController; import com.android.wm.shell.pip.tv.TvPipNotificationController; import com.android.wm.shell.pip.tv.TvPipTaskOrganizer; import com.android.wm.shell.pip.tv.TvPipTransition; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 1071d728a56d..df1b06225fda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,7 +18,7 @@ package com.android.wm.shell.desktopmode; import android.graphics.Region; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -49,8 +49,11 @@ public interface DesktopMode { /** Called when requested to go to desktop mode from the current focused app. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); /** Called when requested to go to fullscreen from the current focused desktop app. */ void moveFocusedTaskToFullscreen(int displayId); + + /** Called when requested to go to split screen from the current focused desktop app. */ + void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt new file mode 100644 index 000000000000..a10c7c093c60 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.TaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.SparseArray +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import androidx.annotation.VisibleForTesting +import androidx.core.util.containsKey +import androidx.core.util.forEach +import androidx.core.util.isEmpty +import androidx.core.util.isNotEmpty +import androidx.core.util.plus +import androidx.core.util.putAll +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.KtProtoLog + +/** + * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log + * appropriate desktop mode session log events. This observes transitions related to desktop mode + * and other transitions that originate both within and outside shell. + */ +class DesktopModeLoggerTransitionObserver( + shellInit: ShellInit, + private val transitions: Transitions, + private val desktopModeEventLogger: DesktopModeEventLogger +) : Transitions.TransitionObserver { + + private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) } + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + shellInit.addInitCallback(this::onInit, this) + } + } + + // A sparse array of visible freeform tasks and taskInfos + private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray() + + // Caching the taskInfos to handle canceled recents animations, if we identify that the recents + // animation was cancelled, we restore these tasks to calculate the post-Transition state + private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray() + + // The instanceId for the current logging session + private var loggerInstanceId: InstanceId? = null + + private val isSessionActive: Boolean + get() = loggerInstanceId != null + + private fun setSessionInactive() { + loggerInstanceId = null + } + + fun onInit() { + transitions.registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + // this was a new recents animation + if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: Recents animation running, saving tasks for later" + ) + // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled + // recents animation + + // when recents animation is running, all freeform tasks are sent TO_BACK temporarily + // if the user ends up at home, we need to update the visible freeform tasks + // if the user cancels the animation, the subsequent transition is NONE + // if the user opens a new task, the subsequent transition is OPEN with flag + tasksSavedForRecents.putAll(visibleFreeformTaskInfos) + } + + // figure out what the new state of freeform tasks would be post transition + var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info) + + // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if + // that's the case, we might have accidentally logged a session exit and would need to + // revaluate again. Add all the tasks back. + // This will start a new desktop mode session. + if ( + info.type == WindowManager.TRANSIT_NONE && + info.flags == 0 && + tasksSavedForRecents.isNotEmpty() + ) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: Canceled recents animation, restoring tasks" + ) + // restore saved tasks in the updated set and clear for next use + postTransitionVisibleFreeformTasks += tasksSavedForRecents + tasksSavedForRecents.clear() + } + + // identify if we need to log any changes and update the state of visible freeform tasks + identifyLogEventAndUpdateState( + transitionInfo = info, + preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos, + postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks + ) + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {} + + private fun getPostTransitionVisibleFreeformTaskInfos( + info: TransitionInfo + ): SparseArray<TaskInfo> { + // device is sleeping, so no task will be visible anymore + if (info.type == WindowManager.TRANSIT_SLEEP) { + return SparseArray() + } + + // filter changes involving freeform tasks or tasks that were cached in previous state + val changesToFreeformWindows = + info.changes + .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID } + .filter { + it.requireTaskInfo().isFreeformWindow() || + visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId) + } + + val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray() + // start off by adding all existing tasks + postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos) + + // the combined set of taskInfos we are interested in this transition change + for (change in changesToFreeformWindows) { + val taskInfo = change.requireTaskInfo() + + // check if this task existed as freeform window in previous cached state and it's now + // changing window modes + if ( + visibleFreeformTaskInfos.containsKey(taskInfo.taskId) && + visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() && + !taskInfo.isFreeformWindow() + ) { + postTransitionFreeformTasks.remove(taskInfo.taskId) + // no need to evaluate new visibility of this task, since it's no longer a freeform + // window + continue + } + + // check if the task is visible after this change, otherwise remove it + if (isTaskVisibleAfterChange(change)) { + postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo) + } else { + postTransitionFreeformTasks.remove(taskInfo.taskId) + } + } + + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: taskInfo map after processing changes %s", + postTransitionFreeformTasks.size() + ) + + return postTransitionFreeformTasks + } + + /** + * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this + * change is processed + */ + private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean = + when { + TransitionUtil.isOpeningType(change.mode) -> true + TransitionUtil.isClosingType(change.mode) -> false + // change mode TRANSIT_CHANGE is only for visible to visible transitions + change.mode == WindowManager.TRANSIT_CHANGE -> true + else -> false + } + + /** + * Log the appropriate log event based on the new state of TasksInfos and previously cached + * state and update it + */ + private fun identifyLogEventAndUpdateState( + transitionInfo: TransitionInfo, + preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, + postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> + ) { + if ( + postTransitionVisibleFreeformTasks.isEmpty() && + preTransitionVisibleFreeformTasks.isNotEmpty() && + isSessionActive + ) { + // Sessions is finishing, log task updates followed by an exit event + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + + desktopModeEventLogger.logSessionExit( + loggerInstanceId!!.id, + getExitReason(transitionInfo) + ) + + setSessionInactive() + } else if ( + postTransitionVisibleFreeformTasks.isNotEmpty() && + preTransitionVisibleFreeformTasks.isEmpty() && + !isSessionActive + ) { + // Session is starting, log enter event followed by task updates + loggerInstanceId = idSequence.newInstanceId() + desktopModeEventLogger.logSessionEnter( + loggerInstanceId!!.id, + getEnterReason(transitionInfo) + ) + + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + } else if (isSessionActive) { + // Session is neither starting, nor finishing, log task updates if there are any + identifyAndLogTaskUpdates( + loggerInstanceId!!.id, + preTransitionVisibleFreeformTasks, + postTransitionVisibleFreeformTasks + ) + } + + // update the state to the new version + visibleFreeformTaskInfos.clear() + visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks) + } + + // TODO(b/326231724) - Add logging around taskInfoChanges Updates + /** Compare the old and new state of taskInfos and identify and log the changes */ + private fun identifyAndLogTaskUpdates( + sessionId: Int, + preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, + postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> + ) { + // find new tasks that were added + postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> + if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) { + desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo)) + } + } + + // find old tasks that were removed + preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> + if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { + desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) + } + } + } + + // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo + private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate { + val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId) + // add task x, y if available + taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) } + + return taskUpdate + } + + /** Get [EnterReason] for this session enter */ + private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason { + // TODO(b/326231756) - Add support for missing enter reasons + return when (transitionInfo.type) { + WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON + Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG + Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON + WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT + else -> EnterReason.UNKNOWN_ENTER + } + } + + /** Get [ExitReason] for this session exit */ + private fun getExitReason(transitionInfo: TransitionInfo): ExitReason { + // TODO(b/326231756) - Add support for missing exit reasons + return when { + transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF + transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED + transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT + transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW + else -> ExitReason.UNKNOWN_EXIT + } + } + + /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */ + @VisibleForTesting + fun addTaskInfosToCachedMap(taskInfo: TaskInfo) { + visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) + } + + @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id + + @VisibleForTesting + fun setLoggerSessionId(id: Int) { + loggerInstanceId = InstanceId.fakeInstanceId(id) + } + + private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { + return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") + } + + private fun TaskInfo.isFreeformWindow(): Boolean { + return this.windowingMode == WINDOWING_MODE_FREEFORM + } + + private fun TransitionInfo.isRecentsTransition(): Boolean { + return this.type == WindowManager.TRANSIT_TO_FRONT && + this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index e1e41ee1e64d..f1a475a42452 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -36,7 +36,14 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl true } } - + "moveToNextDisplay" -> { + if (!runMoveToNextDisplay(args, pw)) { + pw.println("Task not found. Please enter a valid taskId.") + false + } else { + true + } + } else -> { pw.println("Invalid command: ${args[0]}") false @@ -61,8 +68,28 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return controller.moveToDesktop(taskId, WindowContainerTransaction()) } + private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + + val taskId = try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + controller.moveToNextDisplay(taskId) + return true + } + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { pw.println("$prefix moveToDesktop <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") + pw.println("$prefix moveToNextDisplay <taskId> ") + pw.println("$prefix Move a task with given id to next display.") } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 22ba70860587..32c22c01a828 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -16,8 +16,12 @@ package com.android.wm.shell.desktopmode; +import android.annotation.NonNull; +import android.content.Context; import android.os.SystemProperties; +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; /** @@ -67,6 +71,12 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_use_rounded_corners", true); /** + * Flag to indicate whether to restrict desktop mode to supported devices. + */ + private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); + + /** * Return {@code true} if desktop windowing is enabled */ public static boolean isEnabled() { @@ -104,4 +114,27 @@ public class DesktopModeStatus { public static boolean useRoundedCorners() { return USE_ROUNDED_CORNERS; } + + /** + * Return {@code true} if desktop mode should be restricted to supported devices. + */ + @VisibleForTesting + public static boolean enforceDeviceRestrictions() { + return ENFORCE_DEVICE_RESTRICTIONS; + } + + /** + * Return {@code true} if the current device supports desktop mode. + */ + @VisibleForTesting + public static boolean isDesktopModeSupported(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); + } + + /** + * Return {@code true} if desktop mode can be entered on the current device. + */ + public static boolean canEnterDesktopMode(@NonNull Context context) { + return !enforceDeviceRestrictions() || isDesktopModeSupported(context); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 7c8fcbb16711..50cea01fa281 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -16,10 +16,13 @@ package com.android.wm.shell.desktopmode +import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray +import android.view.Display.INVALID_DISPLAY +import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator @@ -47,6 +50,8 @@ class DesktopModeTaskRepository { var stashed: Boolean = false ) + // Token of the current wallpaper activity, used to remove it when the last task is removed + var wallpaperActivityToken: WindowContainerToken? = null // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). private val freeformTasksInZOrder = mutableListOf<Int>() private val activeTasksListeners = ArraySet<ActiveTasksListener>() @@ -54,6 +59,8 @@ class DesktopModeTaskRepository { private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>() // Track corner/caption regions of desktop tasks, used to determine gesture exclusion private val desktopExclusionRegions = SparseArray<Region>() + // Track last bounds of task before toggled to stable bounds + private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>() private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null @@ -196,6 +203,15 @@ class DesktopModeTaskRepository { } /** + * Check if a task with the given [taskId] is the only active task on its display + */ + fun isOnlyActiveTask(taskId: Int): Boolean { + return displayData.valueIterator().asSequence().any { data -> + data.activeTasks.singleOrNull() == taskId + } + } + + /** * Get a set of the active tasks for given [displayId] */ fun getActiveTasks(displayId: Int): ArraySet<Int> { @@ -226,6 +242,14 @@ class DesktopModeTaskRepository { displayData[otherDisplayId].visibleTasks.size) } } + } else if (displayId == INVALID_DISPLAY) { + // Task has vanished. Check which display to remove the task from. + displayData.forEach { displayId, data -> + if (data.visibleTasks.remove(taskId)) { + notifyVisibleTaskListeners(displayId, data.visibleTasks.size) + } + } + return } val prevCount = getVisibleTaskCount(displayId) @@ -236,6 +260,7 @@ class DesktopModeTaskRepository { } val newCount = getVisibleTaskCount(displayId) + // Check if count changed if (prevCount != newCount) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, @@ -244,10 +269,6 @@ class DesktopModeTaskRepository { visible, displayId ) - } - - // Check if count changed - if (prevCount != newCount) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: visibleTaskCount has changed from %d to %d", @@ -301,6 +322,7 @@ class DesktopModeTaskRepository { taskId ) freeformTasksInZOrder.remove(taskId) + boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() @@ -352,6 +374,20 @@ class DesktopModeTaskRepository { } /** + * Removes and returns the bounds saved before maximizing the given task. + */ + fun removeBoundsBeforeMaximize(taskId: Int): Rect? { + return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) + } + + /** + * Saves the bounds of the given task before maximizing. + */ + fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) { + boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) + } + + /** * Check if display with id [displayId] has desktop tasks stashed */ fun isStashed(displayId: Int): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 7091c4b7210a..6a3c8d2f599a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -98,6 +98,7 @@ public class DesktopModeVisualIndicator { * Based on the coordinates of the current drag event, determine which indicator type we should * display, including no visible indicator. */ + @NonNull IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) { final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. @@ -136,18 +137,18 @@ public class DesktopModeVisualIndicator { Region calculateFullscreenRegion(DisplayLayout layout, @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { final Region region = new Region(); - int edgeTransitionHeight = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); + int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM + ? mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) + : 2 * layout.stableInsets().top; // A thin, short Rect at the top of the screen. if (windowingMode == WINDOWING_MODE_FREEFORM) { int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); - int fromFreeformHeight = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height); region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), -captionHeight, (layout.width() / 2) + (fromFreeformWidth / 2), - fromFreeformHeight)); + transitionHeight)); } // A screen-wide, shorter Rect if the task is in fullscreen or split. if (windowingMode == WINDOWING_MODE_FULLSCREEN @@ -155,7 +156,7 @@ public class DesktopModeVisualIndicator { region.union(new Rect(0, -captionHeight, layout.width(), - edgeTransitionHeight)); + transitionHeight)); } return region; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index d1328cabdc99..068661a6a666 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions import android.app.PendingIntent +import android.app.TaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -39,6 +40,7 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.RemoteTransition import android.window.TransitionInfo @@ -46,6 +48,7 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread import com.android.internal.policy.ScreenDecorationsUtils +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController @@ -59,15 +62,17 @@ import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.common.annotations.ExternalThread -import com.android.wm.shell.common.annotations.ShellMainThread import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.compatui.isSingleTopActivityTranslucent import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener +import com.android.wm.shell.shared.annotations.ExternalThread +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE import com.android.wm.shell.sysui.ShellCommandHandler @@ -77,8 +82,11 @@ import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.KtProtoLog +import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener +import com.android.wm.shell.windowdecor.extension.isFreeform +import com.android.wm.shell.windowdecor.extension.isFullscreen import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer @@ -101,6 +109,7 @@ class DesktopTasksController( ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @@ -112,7 +121,6 @@ class DesktopTasksController( private var visualIndicator: DesktopModeVisualIndicator? = null private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) - private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> visualIndicator?.releaseVisualIndicator(t) @@ -140,7 +148,7 @@ class DesktopTasksController( private val transitionAreaHeight get() = context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_height + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height ) private val transitionAreaWidth @@ -161,8 +169,11 @@ class DesktopTasksController( private fun onInit() { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") shellCommandHandler.addDumpCallback(this::dump, this) - shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, - this) + shellCommandHandler.addCommandCallback( + "desktopmode", + desktopModeShellCommandHandler, + this + ) shellController.addExternalInterface( ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, { createExternalInterface() }, @@ -252,7 +263,7 @@ class DesktopTasksController( } /** Enter desktop by using the focused task in given `displayId` */ - fun enterDesktop(displayId: Int) { + fun moveFocusedTaskToDesktop(displayId: Int) { val allFocusedTasks = shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> taskInfo.isFocused && @@ -266,9 +277,11 @@ class DesktopTasksController( // Split-screen case where there are two focused tasks, then we find the child // task to move to desktop. val splitFocusedTask = - if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) + if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) { allFocusedTasks[1] - else allFocusedTasks[0] + } else { + allFocusedTasks[0] + } moveToDesktop(splitFocusedTask) } 1 -> { @@ -305,6 +318,22 @@ class DesktopTasksController( task: RunningTaskInfo, wct: WindowContainerTransaction = WindowContainerTransaction() ) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: Cannot enter desktop, " + + "display does not meet minimum size requirements" + ) + return + } + if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: Cannot enter desktop, " + + "translucent top activity found. This is likely a modal dialog." + ) + return + } KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: moveToDesktop taskId=%d", @@ -353,7 +382,6 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - moveHomeTaskToFront(wct) bringDesktopAppsToFront(taskInfo.displayId, wct) addMoveToDesktopChanges(wct, taskInfo) wct.setBounds(taskInfo.token, freeformBounds) @@ -373,6 +401,22 @@ class DesktopTasksController( shellTaskOrganizer.applyTransaction(wct) } + /** + * Perform clean up of the desktop wallpaper activity if the closed window task is + * the last active task. + * + * @param wct transaction to modify if the last active task is closed + * @param taskId task id of the window that's being closed + */ + fun onDesktopWindowClose( + wct: WindowContainerTransaction, + taskId: Int + ) { + if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) { + removeWallpaperActivity(wct) + } + } + /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> @@ -382,14 +426,8 @@ class DesktopTasksController( /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ fun enterFullscreen(displayId: Int) { - if (DesktopModeStatus.isEnabled()) { - shellTaskOrganizer - .getRunningTasks(displayId) - .find { taskInfo -> - taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM - } - ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) } - } + getFocusedFreeformTask(displayId) + ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) } } /** Move a desktop app to split screen. */ @@ -443,7 +481,11 @@ class DesktopTasksController( if (Transitions.ENABLE_SHELL_TRANSITIONS) { exitDesktopTaskTransitionHandler.startTransition( - Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback) + Transitions.TRANSIT_EXIT_DESKTOP_MODE, + wct, + position, + mOnAnimationFinishedCallback + ) } else { shellTaskOrganizer.applyTransaction(wct) releaseVisualIndicator() @@ -489,8 +531,12 @@ class DesktopTasksController( KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) return } - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d", - taskId, task.displayId) + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "moveToNextDisplay: taskId=%d taskDisplayId=%d", + taskId, + task.displayId + ) val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() // Get the first display id that is higher than current task display id @@ -512,8 +558,12 @@ class DesktopTasksController( * No-op if task is already on that display per [RunningTaskInfo.displayId]. */ private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d", - task.taskId, displayId) + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "moveToDisplay: taskId=%d displayId=%d", + task.taskId, + displayId + ) if (task.displayId == displayId) { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") @@ -535,7 +585,10 @@ class DesktopTasksController( } } - /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */ + /** + * Quick-resizes a desktop task, toggling between the stable bounds and the last saved bounds + * if available or the default bounds otherwise. + */ fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return @@ -543,11 +596,21 @@ class DesktopTasksController( displayLayout.getStableBounds(stableBounds) val destinationBounds = Rect() if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) { - // The desktop task is currently occupying the whole stable bounds, toggle to the - // default bounds. - getDefaultDesktopTaskBounds(displayLayout, destinationBounds) + // The desktop task is currently occupying the whole stable bounds. If the bounds + // before the task was toggled to stable bounds were saved, toggle the task to those + // bounds. Otherwise, toggle to the default bounds. + val taskBoundsBeforeMaximize = + desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId) + if (taskBoundsBeforeMaximize != null) { + destinationBounds.set(taskBoundsBeforeMaximize) + } else { + destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout)) + } } else { - // Toggle to the stable bounds. + // Save current bounds so that task can be restored back to original bounds if necessary + // and toggle to the stable bounds. + val taskBounds = taskInfo.configuration.windowConfiguration.bounds + desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, taskBounds) destinationBounds.set(stableBounds) } @@ -565,52 +628,53 @@ class DesktopTasksController( * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. */ fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + val destinationBounds = getSnapBounds(taskInfo, position) + + if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return + + val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + toggleResizeDesktopTaskTransitionHandler.startTransition(wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect { + // TODO(b/319819547): Account for app constraints so apps do not become letterboxed + val desiredWidth = (displayLayout.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() + val desiredHeight = (displayLayout.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() + val heightOffset = (displayLayout.height() - desiredHeight) / 2 + val widthOffset = (displayLayout.width() - desiredWidth) / 2 + return Rect(widthOffset, heightOffset, + desiredWidth + widthOffset, desiredHeight + heightOffset) + } + + private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect() val stableBounds = Rect() displayLayout.getStableBounds(stableBounds) val destinationWidth = stableBounds.width() / 2 - val destinationBounds = when (position) { + return when (position) { SnapPosition.LEFT -> { Rect( - stableBounds.left, - stableBounds.top, - stableBounds.left + destinationWidth, - stableBounds.bottom + stableBounds.left, + stableBounds.top, + stableBounds.left + destinationWidth, + stableBounds.bottom ) } SnapPosition.RIGHT -> { Rect( - stableBounds.right - destinationWidth, - stableBounds.top, - stableBounds.right, - stableBounds.bottom + stableBounds.right - destinationWidth, + stableBounds.top, + stableBounds.right, + stableBounds.bottom ) } } - - if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return - - val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - toggleResizeDesktopTaskTransitionHandler.startTransition(wct) - } else { - shellTaskOrganizer.applyTransaction(wct) - } - } - - private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout, outBounds: Rect) { - // TODO(b/319819547): Account for app constraints so apps do not become letterboxed - val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height()) - // Update width and height with default desktop mode values - val desiredWidth = screenBounds.width().times(DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() - val desiredHeight = screenBounds.height().times(DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() - outBounds.set(0, 0, desiredWidth, desiredHeight) - // Center the task in screen bounds - outBounds.offset( - screenBounds.centerX() - outBounds.centerX(), - screenBounds.centerY() - outBounds.centerY()) } /** @@ -628,9 +692,15 @@ class DesktopTasksController( KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) - // First move home to front and then other tasks on top of it - moveHomeTaskToFront(wct) + if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Add translucent wallpaper activity to show the wallpaper underneath + addWallpaperActivity(wct) + } else { + // Move home to front + moveHomeTaskToFront(wct) + } + // Then move other tasks on top of it val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() activeTasks // Sort descending as the top task is at index 0. It should be ordered to top last @@ -646,7 +716,27 @@ class DesktopTasksController( ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } } - private fun releaseVisualIndicator() { + private fun addWallpaperActivity(wct: WindowContainerTransaction) { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper") + val intent = Intent(context, DesktopWallpaperActivity::class.java) + val options = ActivityOptions.makeBasic().apply { + isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + } + val pendingIntent = PendingIntent.getActivity(context, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE) + wct.sendPendingIntent(pendingIntent, intent, options.toBundle()) + } + + private fun removeWallpaperActivity(wct: WindowContainerTransaction) { + desktopModeTaskRepository.wallpaperActivityToken?.let { token -> + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper") + wct.removeTask(token) + } + } + + fun releaseVisualIndicator() { val t = SurfaceControl.Transaction() visualIndicator?.releaseVisualIndicator(t) visualIndicator = null @@ -693,6 +783,9 @@ class DesktopTasksController( reason = "recents animation is running" false } + // Handle back navigation for the last window if wallpaper available + shouldRemoveWallpaper(request) -> + true // Only handle open or to front transitions request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { reason = "transition type not handled (${request.type})" @@ -729,12 +822,15 @@ class DesktopTasksController( val result = triggerTask?.let { task -> when { + request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) // If display has tasks stashed, handle as stashed launch - desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) + task.isStashed -> handleStashedTaskLaunch(task) + // Check if the task has a top transparent activity + shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task) // Check if fullscreen task should be updated - task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) + task.isFullscreen -> handleFullscreenTaskLaunch(task) // Check if freeform task should be updated - task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) + task.isFreeform -> handleFreeformTaskLaunch(task) else -> { null } @@ -767,6 +863,21 @@ class DesktopTasksController( .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } + private val TaskInfo.isStashed: Boolean + get() = desktopModeTaskRepository.isStashed(displayId) + + private fun shouldLaunchAsModal(task: TaskInfo): Boolean { + return Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) + } + + private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { + return Flags.enableDesktopWindowingWallpaperActivity() && + request.type == TRANSIT_TO_BACK && + request.triggerTask?.let { task -> + desktopModeTaskRepository.isOnlyActiveTask(task.taskId) + } ?: false + } + private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) @@ -814,12 +925,36 @@ class DesktopTasksController( return wct } + // Always launch transparent tasks in fullscreen. + private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + // Already fullscreen, no-op. + if (task.isFullscreen) + return null + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task) + } + } + + /** Handle back navigation by removing wallpaper activity if it's the last active task */ + private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? { + if (desktopModeTaskRepository.isOnlyActiveTask(task.taskId) && + desktopModeTaskRepository.wallpaperActivityToken != null) { + // Remove wallpaper activity when the last active task is removed + return WindowContainerTransaction().also { wct -> + removeWallpaperActivity(wct) + } + } else { + return null + } + } + private fun addMoveToDesktopChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode - val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) { + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! + val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) { // Display windowing is freeform, set to undefined and inherit it WINDOWING_MODE_UNDEFINED } else { @@ -836,8 +971,9 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode - val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) { + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! + val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) { // Display windowing is fullscreen, set to undefined and inherit it WINDOWING_MODE_UNDEFINED } else { @@ -866,20 +1002,40 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + /** Enter split by using the focused desktop task in given `displayId`. */ + fun enterSplit( + displayId: Int, + leftOrTop: Boolean + ) { + getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } + } + + private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { + return shellTaskOrganizer.getRunningTasks(displayId) + .find { taskInfo -> taskInfo.isFocused && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } + } + /** * Requests a task be transitioned from desktop to split select. Applies needed windowing * changes if this transition is enabled. */ + @JvmOverloads fun requestSplit( - taskInfo: RunningTaskInfo + taskInfo: RunningTaskInfo, + leftOrTop: Boolean = false, ) { val windowingMode = taskInfo.windowingMode if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM ) { val wct = WindowContainerTransaction() addMoveToSplitChanges(wct, taskInfo) - splitScreenController.requestEnterSplitSelect(taskInfo, wct, - SPLIT_POSITION_BOTTOM_OR_RIGHT, taskInfo.configuration.windowConfiguration.bounds) + splitScreenController.requestEnterSplitSelect( + taskInfo, + wct, + if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT, + taskInfo.configuration.windowConfiguration.bounds + ) } } @@ -927,20 +1083,18 @@ class DesktopTasksController( taskSurface: SurfaceControl, inputX: Float, taskTop: Float - ) { + ): DesktopModeVisualIndicator.IndicatorType { // If the visual indicator does not exist, create it. - if (visualIndicator == null) { - visualIndicator = DesktopModeVisualIndicator( - syncQueue, taskInfo, displayController, context, taskSurface, - rootTaskDisplayAreaOrganizer) - } - // Then, update the indicator type. - val indicator = visualIndicator ?: return - indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode) + val indicator = visualIndicator ?: DesktopModeVisualIndicator( + syncQueue, taskInfo, displayController, context, taskSurface, + rootTaskDisplayAreaOrganizer) + if (visualIndicator == null) visualIndicator = indicator + return indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode) } /** - * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area. + * Perform checks required on drag end. If indicator indicates a windowing mode change, perform + * that change. Otherwise, ensure bounds are up to date. * * @param taskInfo the task being dragged. * @param position position of surface when drag ends. @@ -951,25 +1105,45 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, position: Point, inputCoordinate: PointF, - taskBounds: Rect + taskBounds: Rect, + validDragArea: Rect ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return } - if (taskBounds.top <= transitionAreaHeight) { - moveToFullscreenWithAnimation(taskInfo, position) - return - } - if (inputCoordinate.x <= transitionAreaWidth) { - releaseVisualIndicator() - snapToHalfScreen(taskInfo, SnapPosition.LEFT) - return - } - if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width() - ?.minus(transitionAreaWidth) ?: return)) { - releaseVisualIndicator() - snapToHalfScreen(taskInfo, SnapPosition.RIGHT) - return + + val indicator = visualIndicator ?: return + val indicatorType = indicator.updateIndicatorType( + PointF(inputCoordinate.x, taskBounds.top.toFloat()), + taskInfo.windowingMode + ) + when (indicatorType) { + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + moveToFullscreenWithAnimation(taskInfo, position) + } + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + releaseVisualIndicator() + snapToHalfScreen(taskInfo, SnapPosition.LEFT) + } + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + releaseVisualIndicator() + snapToHalfScreen(taskInfo, SnapPosition.RIGHT) + } + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> { + // If task bounds are outside valid drag area, snap them inward and perform a + // transaction to set bounds. + if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + taskBounds, validDragArea)) { + val wct = WindowContainerTransaction() + wct.setBounds(taskInfo.token, taskBounds) + transitions.startTransition(TRANSIT_CHANGE, wct, null) + } + releaseVisualIndicator() + } + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { + throw IllegalArgumentException("Should not be receiving TO_DESKTOP_INDICATOR for " + + "a freeform task.") + } } // A freeform drag-move ended, remove the indicator immediately. releaseVisualIndicator() @@ -981,15 +1155,26 @@ class DesktopTasksController( * @param taskInfo the task being dragged. * @param y height of drag, to be checked against status bar height. */ - fun onDragPositioningEndThroughStatusBar( - taskInfo: RunningTaskInfo, - freeformBounds: Rect - ) { - finalizeDragToDesktop(taskInfo, freeformBounds) - } - - private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int { - return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) { + val indicator = visualIndicator ?: return + val indicatorType = indicator + .updateIndicatorType(inputCoordinates, taskInfo.windowingMode) + when (indicatorType) { + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout)) + } + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + cancelDragToDesktop(taskInfo) + } + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.LEFT)) + } + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.RIGHT)) + } + } } /** @@ -1055,7 +1240,8 @@ class DesktopTasksController( pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED + ) isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } val wct = WindowContainerTransaction() @@ -1100,9 +1286,9 @@ class DesktopTasksController( } } - override fun enterDesktop(displayId: Int) { + override fun moveFocusedTaskToDesktop(displayId: Int) { mainExecutor.execute { - this@DesktopTasksController.enterDesktop(displayId) + this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) } } @@ -1111,6 +1297,12 @@ class DesktopTasksController( this@DesktopTasksController.enterFullscreen(displayId) } } + + override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) { + mainExecutor.execute { + this@DesktopTasksController.enterSplit(displayId, leftOrTop) + } + } } /** The interface for calls from outside the host process. */ @@ -1224,6 +1416,13 @@ class DesktopTasksController( "setTaskListener" ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } } + + override fun moveToDesktop(taskId: Int) { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "moveToDesktop" + ) { c -> c.moveToDesktop(taskId) } + } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt new file mode 100644 index 000000000000..20df26428649 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.KtProtoLog + +/** + * A [Transitions.TransitionObserver] that observes shell transitions and updates + * the [DesktopModeTaskRepository] state TODO: b/332682201 + * This observes transitions related to desktop mode + * and other transitions that originate both within and outside shell. + */ +class DesktopTasksTransitionObserver( + private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val transitions: Transitions, + shellInit: ShellInit +) : Transitions.TransitionObserver { + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + shellInit.addInitCallback(::onInit, this) + } + } + + fun onInit() { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit") + transitions.registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + // TODO: b/332682201 Update repository state + updateWallpaperToken(info) + } + + override fun onTransitionStarting(transition: IBinder) { + // TODO: b/332682201 Update repository state + } + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + // TODO: b/332682201 Update repository state + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + // TODO: b/332682201 Update repository state + } + + private fun updateWallpaperToken(info: TransitionInfo) { + if (!enableDesktopWindowingWallpaperActivity()) { + return + } + info.changes.forEach { change -> + change.taskInfo?.let { taskInfo -> + if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { + when (change.mode) { + WindowManager.TRANSIT_OPEN -> + desktopModeTaskRepository.wallpaperActivityToken = taskInfo.token + WindowManager.TRANSIT_CLOSE -> + desktopModeTaskRepository.wallpaperActivityToken = null + else -> {} + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt new file mode 100644 index 000000000000..c4a4474689fa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.Activity +import android.app.ActivityManager +import android.content.ComponentName +import android.os.Bundle +import android.view.WindowManager +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.util.KtProtoLog + +/** + * A transparent activity used in the desktop mode to show the wallpaper under the freeform windows. + * This activity will be running in `FULLSCREEN` windowing mode, which ensures it hides Launcher. + * When entering desktop, we would ensure that it's added behind desktop apps and removed when + * leaving the desktop mode. + * + * Note! This activity should NOT interact directly with any other code in the Shell without calling + * onto the shell main thread. Activities are always started on the main thread. + */ +class DesktopWallpaperActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate") + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + } + + companion object { + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" + private val wallpaperActivityComponent = + ComponentName(SYSTEM_UI_PACKAGE_NAME, DesktopWallpaperActivity::class.java.name) + + @JvmStatic + fun isWallpaperTask(taskInfo: ActivityManager.RunningTaskInfo) = + taskInfo.baseIntent.component?.let(::isWallpaperComponent) ?: false + + @JvmStatic + fun isWallpaperComponent(component: ComponentName) = + component == wallpaperActivityComponent + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index af26e2980afe..0061d03af8e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -15,6 +15,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FILL_IN_COMPONENT import android.graphics.Rect +import android.os.Bundle import android.os.IBinder import android.os.SystemClock import android.view.SurfaceControl @@ -124,7 +125,7 @@ class DragToDesktopTransitionHandler( options.toBundle() ) val wct = WindowContainerTransaction() - wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle()) val startTransitionToken = transitions .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this) @@ -368,67 +369,50 @@ class DragToDesktopTransitionHandler( val startBounds = draggedTaskChange.startAbsBounds val endBounds = draggedTaskChange.endAbsBounds - // TODO(b/301106941): Instead of forcing-finishing the animation that scales the - // surface down and then starting another that scales it back up to the final size, - // blend the two animations. - state.dragAnimator.endAnimator() - // Using [DRAG_FREEFORM_SCALE] to calculate animated width/height is possible because - // it is known that the animation scale is finished because the animation was - // force-ended above. This won't be true when the two animations are blended. - val animStartWidth = (startBounds.width() * DRAG_FREEFORM_SCALE).toInt() - val animStartHeight = (startBounds.height() * DRAG_FREEFORM_SCALE).toInt() - // Using end bounds here to find the left/top also assumes the center animation has - // finished and the surface is placed exactly in the center of the screen which matches - // the end/default bounds of the now freeform task. - val animStartLeft = endBounds.centerX() - (animStartWidth / 2) - val animStartTop = endBounds.centerY() - (animStartHeight / 2) - val animStartBounds = Rect( - animStartLeft, - animStartTop, - animStartLeft + animStartWidth, - animStartTop + animStartHeight + // Pause any animation that may be currently playing; we will use the relevant + // details of that animation here. + state.dragAnimator.cancelAnimator() + // We still apply scale to task bounds; as we animate the bounds to their + // end value, animate scale to 1. + val startScale = state.dragAnimator.scale + val startPosition = state.dragAnimator.position + val unscaledStartWidth = startBounds.width() + val unscaledStartHeight = startBounds.height() + val unscaledStartBounds = Rect( + startPosition.x.toInt(), + startPosition.y.toInt(), + startPosition.x.toInt() + unscaledStartWidth, + startPosition.y.toInt() + unscaledStartHeight ) - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t) - t.apply { - setScale(draggedTaskLeash, 1f, 1f) - setPosition( - draggedTaskLeash, - animStartBounds.left.toFloat(), - animStartBounds.top.toFloat() - ) - setWindowCrop( - draggedTaskLeash, - animStartBounds.width(), - animStartBounds.height() - ) - } // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. - onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t, animStartBounds) + onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t, + unscaledStartBounds) finishCallback.onTransitionFinished(null /* wct */) - // Because the task surface was scaled down during the drag, we must use the animated - // bounds instead of the [startAbsBounds]. val tx: SurfaceControl.Transaction = transactionSupplier.get() - ValueAnimator.ofObject(rectEvaluator, animStartBounds, endBounds) + ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds) .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) .apply { addUpdateListener { animator -> val animBounds = animator.animatedValue as Rect + val animFraction = animator.animatedFraction + // Progress scale from starting value to 1 as animation plays. + val animScale = startScale + animFraction * (1 - startScale) tx.apply { - setScale(draggedTaskLeash, 1f, 1f) - setPosition( - draggedTaskLeash, - animBounds.left.toFloat(), - animBounds.top.toFloat() - ) + setScale(draggedTaskLeash, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() + ) setWindowCrop( - draggedTaskLeash, - animBounds.width(), - animBounds.height() + draggedTaskLeash, + animBounds.width(), + animBounds.height() ) } onTaskResizeAnimationListener.onBoundsChange( @@ -492,10 +476,8 @@ class DragToDesktopTransitionHandler( val draggedTaskChange = state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change") val sc = draggedTaskChange.leash - // TODO(b/301106941): Don't end the animation and start one to scale it back, merge them - // instead. - // End the animation that shrinks the window when task is first dragged from fullscreen - dragToDesktopAnimator.endAnimator() + // Pause the animation that shrinks the window when task is first dragged from fullscreen + dragToDesktopAnimator.cancelAnimator() // Then animate the scaled window back to its original bounds. val x: Float = dragToDesktopAnimator.position.x val y: Float = dragToDesktopAnimator.position.y diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index 6bdaf1eadb8a..fa4352241193 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -45,4 +45,7 @@ interface IDesktopMode { /** Set listener that will receive callbacks about updates to desktop tasks */ oneway void setTaskListener(IDesktopTaskListener listener); + + /** Move a task with given `taskId` to desktop */ + void moveToDesktop(int taskId); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 7da1b23dd5b1..165feec58455 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -67,8 +67,8 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ExternalMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index eb82da8a8e9f..6a7d297e83e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,6 +16,7 @@ package com.android.wm.shell.draganddrop; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; @@ -301,16 +302,14 @@ public class DragAndDropPolicy { position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_DENIED); // TODO(b/255649902): Rework this so that SplitScreenController can always use the options // instead of a fillInIntent since it's assuming that the PendingIntent is mutable baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); final Bundle opts = baseActivityOpts.toBundle(); - // Put BAL flags to avoid activity start aborted. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); - mStarter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), null /* fillIntent */, position, opts); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt index 8826141fb406..31214eba8dd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt @@ -17,6 +17,8 @@ package com.android.wm.shell.draganddrop import android.app.ActivityManager import android.os.RemoteException +import android.os.Trace +import android.os.Trace.TRACE_TAG_WINDOW_MANAGER import android.util.Log import android.view.DragEvent import android.view.IWindowManager @@ -27,6 +29,7 @@ import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.protolog.ShellProtoLogGroup import java.util.function.Consumer +import kotlin.random.Random /** * Manages the listener and callbacks for unhandled global drags. @@ -101,10 +104,15 @@ class GlobalDragListener( @VisibleForTesting fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) { + val traceCookie = Random.nextInt() + Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop", + traceCookie); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "onUnhandledDrop: %s", dragEvent) if (callback == null) { wmCallback.notifyUnhandledDropComplete(false) + Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop", + traceCookie); return } @@ -112,6 +120,8 @@ class GlobalDragListener( ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Notifying onUnhandledDrop complete: %b", it) wmCallback.notifyUnhandledDropComplete(it) + Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop", + traceCookie); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index 73de231fb63a..863a51ad575b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -48,8 +48,8 @@ import android.window.WindowContainerTransaction; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java index 33c299f0b161..4215b2cc5f29 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java @@ -19,7 +19,7 @@ package com.android.wm.shell.keyguard; import android.annotation.NonNull; import android.window.IRemoteTransition; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; /** * Interface exposed to SystemUI Keyguard to register handlers for running diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index 2ee334873780..b000e3228b9a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -18,7 +18,7 @@ package com.android.wm.shell.onehanded; import android.os.SystemProperties; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; /** * Interface to engage one handed feature. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 679d4ca2ac48..39b9000856f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -55,7 +55,7 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index a9aa6badcfe2..7b1ef5c6cddd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -18,7 +18,7 @@ package com.android.wm.shell.pip; import android.graphics.Rect; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bd186ba22588..57cf99211b6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -82,7 +82,6 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; @@ -92,10 +91,12 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -522,9 +523,27 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); t.setLayer(overlay, Integer.MAX_VALUE); t.apply(); + // This serves as a last resort in case the Shell Transition is not handled properly. + // We want to make sure the overlay passed from Launcher gets removed eventually. + mayRemoveContentOverlay(overlay); } } + private void mayRemoveContentOverlay(SurfaceControl overlay) { + final WeakReference<SurfaceControl> overlayRef = new WeakReference<>(overlay); + final long timeoutDuration = (mEnterAnimationDuration + + CONTENT_OVERLAY_FADE_OUT_DELAY_MS + + EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS) * 2L; + mMainExecutor.executeDelayed(() -> { + final SurfaceControl overlayLeash = overlayRef.get(); + if (overlayLeash != null && overlayLeash.isValid() && overlayLeash == mPipOverlay) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Cleanup the overlay(%s) as a last resort.", overlayLeash); + removeContentOverlay(overlayLeash, null /* callback */); + } + }, timeoutDuration); + } + /** * Callback when launcher aborts swipe-pip-to-home operation. */ @@ -1957,6 +1976,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface); return; } if (surface == null || !surface.isValid()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 6a1a62ea30a1..d60f5a631044 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -27,6 +27,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; @@ -840,8 +841,11 @@ public class PipTransition extends PipTransitionController { && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED && !change.getContainer().equals(mCurrentPipTaskToken)) { // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps - // that enter PiP instantly on opening, mostly from CTS/Flicker tests) - if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN) { + // that enter PiP instantly on opening, mostly from CTS/Flicker tests). + // TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also + // be allowed to animate if the task in question is pinned already - see b/308054074. + if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN + || transitType == TRANSIT_TO_FRONT) { return true; } // This can happen if the request to enter PIP happens when we are collecting for diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 84afed18b8d4..139cde2c66f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -122,6 +122,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb private static final long PIP_KEEP_CLEAR_AREAS_DELAY = SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); + private static final long ENABLE_TOUCH_DELAY_MS = 200L; + private Context mContext; protected ShellExecutor mMainExecutor; private DisplayController mDisplayController; @@ -152,6 +154,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb private final Runnable mMovePipInResponseToKeepClearAreasChangeCallback = this::onKeepClearAreasChangedCallback; + private final Runnable mEnableTouchCallback = () -> mTouchHandler.setTouchEnabled(true); + private void onKeepClearAreasChangedCallback() { if (mIsKeyguardShowingOrAnimating) { // early bail out if the change was caused by keyguard showing up @@ -1043,6 +1047,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb saveReentryState(pipBounds); } // Disable touches while the animation is running + mMainExecutor.removeCallbacks(mEnableTouchCallback); mTouchHandler.setTouchEnabled(false); if (mPinnedStackAnimationRecentsCallback != null) { mPinnedStackAnimationRecentsCallback.onPipAnimationStarted(); @@ -1073,7 +1078,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION); // Re-enable touches after the animation completes - mTouchHandler.setTouchEnabled(true); + mMainExecutor.executeDelayed(mEnableTouchCallback, ENABLE_TOUCH_DELAY_MS); mTouchHandler.onPinnedStackAnimationEnded(direction); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index df67707e2014..ef468434db6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -37,7 +37,6 @@ import android.os.Debug; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; -import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipAppOpsListener; @@ -47,6 +46,7 @@ import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.PhysicsAnimator; import kotlin.Unit; import kotlin.jvm.functions.Function0; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 62156fc7443b..6b5bdd2299e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -64,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private TvPipBackgroundView mPipBackgroundView; private boolean mIsReloading; + private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000; + private final Runnable mClosePipMenuRunnable = this::closeMenu; @TvPipMenuMode private int mCurrentMenuMode = MODE_NO_MENU; @@ -280,6 +282,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: closeMenu()", TAG); requestMenuMode(MODE_NO_MENU); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); } @Override @@ -488,13 +491,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private void requestMenuMode(@TvPipMenuMode int menuMode) { if (isMenuOpen() == isMenuOpen(menuMode)) { + if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) { + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } // No need to request a focus change. We can directly switch to the new mode. switchToMenuMode(menuMode); } else { if (isMenuOpen(menuMode)) { + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); mMenuModeOnFocus = menuMode; } - // Send a request to gain window focus if the menu is open, or lose window focus // otherwise. Once the focus change happens, we will request the new mode in the // callback {@link #onPipWindowFocusChanged}. @@ -584,6 +591,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override + public void onUserInteracting() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString()); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + + } + @Override public void onPipMovement(int keycode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index b259e8d584a6..4a767ef2a113 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -491,30 +491,33 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (event.getKeyCode() == KEYCODE_BACK) { mListener.onExitCurrentMenuMode(); return true; } - - if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { - switch (event.getKeyCode()) { - case KEYCODE_DPAD_UP: - case KEYCODE_DPAD_DOWN: - case KEYCODE_DPAD_LEFT: - case KEYCODE_DPAD_RIGHT: + switch (event.getKeyCode()) { + case KEYCODE_DPAD_UP: + case KEYCODE_DPAD_DOWN: + case KEYCODE_DPAD_LEFT: + case KEYCODE_DPAD_RIGHT: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onPipMovement(event.getKeyCode()); return true; - case KEYCODE_ENTER: - case KEYCODE_DPAD_CENTER: + } + break; + case KEYCODE_ENTER: + case KEYCODE_DPAD_CENTER: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onExitCurrentMenuMode(); return true; - default: - // Dispatch key event as normal below - } + } + break; + default: + // Dispatch key event as normal below } } - return super.dispatchKeyEvent(event); } @@ -637,6 +640,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L interface Listener { /** + * Called when any button (that affects the menu) on current menu mode was pressed. + */ + void onUserInteracting(); + + /** * Called when a button for exiting the current menu mode was pressed. */ void onExitCurrentMenuMode(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index e73a85003881..1e18b8c002db 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -32,13 +32,16 @@ import android.view.SurfaceControl; import androidx.annotation.BinderThread; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.pip.IPip; import com.android.wm.shell.common.pip.IPipAnimationListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -57,15 +60,40 @@ public class PipController implements ConfigurationChangeListener, DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); - private Context mContext; - private ShellController mShellController; - private DisplayController mDisplayController; - private DisplayInsetsController mDisplayInsetsController; - private PipBoundsState mPipBoundsState; - private PipBoundsAlgorithm mPipBoundsAlgorithm; - private PipDisplayLayoutState mPipDisplayLayoutState; - private PipScheduler mPipScheduler; - private ShellExecutor mMainExecutor; + private final Context mContext; + private final ShellController mShellController; + private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final PipScheduler mPipScheduler; + private final ShellExecutor mMainExecutor; + + // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. + private PipAnimationListener mPipRecentsAnimationListener; + + @VisibleForTesting + interface PipAnimationListener { + /** + * Notifies the listener that the Pip animation is started. + */ + void onPipAnimationStarted(); + + /** + * Notifies the listener about PiP resource dimensions changed. + * Listener can expect an immediate callback the first time they attach. + * + * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. + * @param shadowRadius the pixel value of the shadow radius, zero means it's disabled. + */ + void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius); + + /** + * Notifies the listener that user leaves PiP by tapping on the expand button. + */ + void onExpandPip(); + } private PipController(Context context, ShellInit shellInit, @@ -92,14 +120,27 @@ public class PipController implements ConfigurationChangeListener, } } - @Override - public Context getContext() { - return mContext; - } - - @Override - public ShellExecutor getRemoteCallExecutor() { - return mMainExecutor; + /** + * Instantiates {@link PipController}, returns {@code null} if the feature not supported. + */ + public static PipController create(Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipDisplayLayoutState pipDisplayLayoutState, + PipScheduler pipScheduler, + ShellExecutor mainExecutor) { + if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Device doesn't support Pip feature", TAG); + return null; + } + return new PipController(context, shellInit, shellController, displayController, + displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, + pipScheduler, mainExecutor); } private void onInit() { @@ -109,7 +150,6 @@ public class PipController implements ConfigurationChangeListener, DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); mPipDisplayLayoutState.setDisplayLayout(layout); - mShellController.addConfigurationChangeListener(this); mDisplayController.addDisplayWindowListener(this); mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), new DisplayInsetsController.OnInsetsChangedListener() { @@ -123,45 +163,50 @@ public class PipController implements ConfigurationChangeListener, // Allow other outside processes to bind to PiP controller using the key below. mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, this::createExternalInterface, this); - } - - /** - * Instantiates {@link PipController}, returns {@code null} if the feature not supported. - */ - public static PipController create(Context context, - ShellInit shellInit, - ShellController shellController, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, - PipDisplayLayoutState pipDisplayLayoutState, - PipScheduler pipScheduler, - ShellExecutor mainExecutor) { - if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Device doesn't support Pip feature", TAG); - return null; - } - return new PipController(context, shellInit, shellController, displayController, - displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, - pipScheduler, mainExecutor); + mShellController.addConfigurationChangeListener(this); } private ExternalInterfaceBinder createExternalInterface() { return new IPipImpl(this); } + // + // RemoteCallable implementations + // + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + // + // ConfigurationChangeListener implementations + // + @Override public void onConfigurationChanged(Configuration newConfiguration) { mPipDisplayLayoutState.onConfigurationChanged(); } @Override + public void onDensityOrFontScaleChanged() { + onPipResourceDimensionsChanged(); + } + + @Override public void onThemeChanged() { onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay())); } + // + // DisplayController.OnDisplaysChangedListener implementations + // + @Override public void onDisplayAdded(int displayId) { if (displayId != mPipDisplayLayoutState.getDisplayId()) { @@ -182,6 +227,10 @@ public class PipController implements ConfigurationChangeListener, mPipDisplayLayoutState.setDisplayLayout(layout); } + // + // IPip Binder stub helpers + // + private Rect getSwipePipToHomeBounds(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams, int launcherRotation, Rect hotseatKeepClearArea) { @@ -196,8 +245,26 @@ public class PipController implements ConfigurationChangeListener, Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onSwipePipToHomeAnimationStart: %s", componentName); - mPipScheduler.setInSwipePipToHomeTransition(true); - // TODO: cache the overlay if provided for reparenting later. + mPipScheduler.onSwipePipToHomeAnimationStart(taskId, componentName, destinationBounds, + overlay, appBounds); + mPipRecentsAnimationListener.onPipAnimationStarted(); + } + + // + // IPipAnimationListener Binder proxy helpers + // + + private void setPipRecentsAnimationListener(PipAnimationListener pipAnimationListener) { + mPipRecentsAnimationListener = pipAnimationListener; + onPipResourceDimensionsChanged(); + } + + private void onPipResourceDimensionsChanged() { + if (mPipRecentsAnimationListener != null) { + mPipRecentsAnimationListener.onPipResourceDimensionsChanged( + mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius), + mContext.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius)); + } } /** @@ -206,9 +273,29 @@ public class PipController implements ConfigurationChangeListener, @BinderThread private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder { private PipController mController; + private final SingleInstanceRemoteListener<PipController, IPipAnimationListener> mListener; + private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() { + @Override + public void onPipAnimationStarted() { + mListener.call(l -> l.onPipAnimationStarted()); + } + + @Override + public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) { + mListener.call(l -> l.onPipResourceDimensionsChanged(cornerRadius, shadowRadius)); + } + + @Override + public void onExpandPip() { + mListener.call(l -> l.onExpandPip()); + } + }; IPipImpl(PipController controller) { mController = controller; + mListener = new SingleInstanceRemoteListener<>(mController, + (cntrl) -> cntrl.setPipRecentsAnimationListener(mPipAnimationListener), + (cntrl) -> cntrl.setPipRecentsAnimationListener(null)); } /** @@ -217,6 +304,7 @@ public class PipController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + mListener.unregister(); } @Override @@ -257,7 +345,14 @@ public class PipController implements ConfigurationChangeListener, @Override public void setPipAnimationListener(IPipAnimationListener listener) { - // TODO: set a proper animation listener to update the Launcher state as needed. + executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", + (controller) -> { + if (listener != null) { + mListener.register(listener); + } else { + mListener.unregister(); + } + }); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java new file mode 100644 index 000000000000..e7e797096c0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.DismissViewUtils; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.bubbles.DismissCircleView; +import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.common.pip.PipUiEventLogger; + +import kotlin.Unit; + +/** + * Handler of all Magnetized Object related code for PiP. + */ +public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListener { + + /* The multiplier to apply scale the target size by when applying the magnetic field radius */ + private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; + + /** + * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move + * PIP. + */ + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Container for the dismiss circle, so that it can be animated within the container via + * translation rather than within the WindowManager via slow layout animations. + */ + private DismissView mTargetViewContainer; + + /** Circle view used to render the dismiss target. */ + private DismissCircleView mTargetView; + + /** + * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. + */ + private MagnetizedObject.MagneticTarget mMagneticTarget; + + // Allow dragging the PIP to a location to close it + private boolean mEnableDismissDragToEdge; + + private int mTargetSize; + private int mDismissAreaHeight; + private float mMagneticFieldRadiusPercent = 1f; + private WindowInsets mWindowInsets; + + private SurfaceControl mTaskLeash; + private boolean mHasDismissTargetSurface; + + private final Context mContext; + private final PipMotionHelper mMotionHelper; + private final PipUiEventLogger mPipUiEventLogger; + private final WindowManager mWindowManager; + private final ShellExecutor mMainExecutor; + + public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, + PipMotionHelper motionHelper, ShellExecutor mainExecutor) { + mContext = context; + mPipUiEventLogger = pipUiEventLogger; + mMotionHelper = motionHelper; + mMainExecutor = mainExecutor; + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + } + + void init() { + Resources res = mContext.getResources(); + mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + + if (mTargetViewContainer != null) { + // init can be called multiple times, remove the old one from view hierarchy first. + cleanUpDismissTarget(); + } + + mTargetViewContainer = new DismissView(mContext); + DismissViewUtils.setup(mTargetViewContainer); + mTargetView = mTargetViewContainer.getCircle(); + mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + updateMagneticTargetSize(); + } + return windowInsets; + }); + + mMagnetizedPip = mMotionHelper.getMagnetizedPip(); + mMagnetizedPip.clearAllTargets(); + mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); + updateMagneticTargetSize(); + + mMagnetizedPip.setAnimateStuckToTarget( + (target, velX, velY, flung, after) -> { + if (mEnableDismissDragToEdge) { + mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); + } + return Unit.INSTANCE; + }); + mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { + // Show the dismiss target, in case the initial touch event occurred within + // the magnetic field radius. + if (mEnableDismissDragToEdge) { + showDismissTargetMaybe(); + } + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject, + float velX, float velY, boolean wasFlungOut) { + if (wasFlungOut) { + mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */); + hideDismissTargetMaybe(); + } else { + mMotionHelper.setSpringingToTouch(true); + } + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { + if (mEnableDismissDragToEdge) { + mMainExecutor.executeDelayed(() -> { + mMotionHelper.notifyDismissalPending(); + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + }, 0); + } + } + }); + + } + + @Override + public boolean onPreDraw() { + mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); + mHasDismissTargetSurface = true; + updateDismissTargetLayer(); + return true; + } + + /** + * Potentially start consuming future motion events if PiP is currently near the magnetized + * object. + */ + public boolean maybeConsumeMotionEvent(MotionEvent ev) { + return mMagnetizedPip.maybeConsumeMotionEvent(ev); + } + + /** + * Update the magnet size. + */ + public void updateMagneticTargetSize() { + if (mTargetView == null) { + return; + } + if (mTargetViewContainer != null) { + mTargetViewContainer.updateResources(); + } + + final Resources res = mContext.getResources(); + mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + + // Set the magnetic field radius equal to the target size from the center of the target + setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent); + } + + /** + * Increase or decrease the field radius of the magnet object, e.g. with larger percent, + * PiP will magnetize to the field sooner. + */ + public void setMagneticFieldRadiusPercent(float percent) { + mMagneticFieldRadiusPercent = percent; + mMagneticTarget.setMagneticFieldRadiusPx((int) (mMagneticFieldRadiusPercent * mTargetSize + * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); + } + + public void setTaskLeash(SurfaceControl taskLeash) { + mTaskLeash = taskLeash; + } + + private void updateDismissTargetLayer() { + if (!mHasDismissTargetSurface || mTaskLeash == null) { + // No dismiss target surface, can just return + return; + } + + final SurfaceControl targetViewLeash = + mTargetViewContainer.getViewRootImpl().getSurfaceControl(); + if (!targetViewLeash.isValid()) { + // The surface of mTargetViewContainer is somehow not ready, bail early + return; + } + + // Put the dismiss target behind the task + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setRelativeLayer(targetViewLeash, mTaskLeash, -1); + t.apply(); + } + + /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ + public void createOrUpdateDismissTarget() { + if (mTargetViewContainer.getParent() == null) { + mTargetViewContainer.cancelAnimators(); + + mTargetViewContainer.setVisibility(View.INVISIBLE); + mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); + mHasDismissTargetSurface = false; + + mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); + } else { + mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); + } + } + + /** Returns layout params for the dismiss target, using the latest display metrics. */ + private WindowManager.LayoutParams getDismissTargetLayoutParams() { + final Point windowSize = new Point(); + mWindowManager.getDefaultDisplay().getRealSize(windowSize); + int height = Math.min(windowSize.y, mDismissAreaHeight); + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + height, + 0, windowSize.y - height, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + lp.setTitle("pip-dismiss-overlay"); + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + lp.setFitInsetsTypes(0 /* types */); + + return lp; + } + + /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ + public void showDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + + createOrUpdateDismissTarget(); + + if (mTargetViewContainer.getVisibility() != View.VISIBLE) { + mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this); + } + // always invoke show, since the target might still be VISIBLE while playing hide animation, + // so we want to ensure it will show back again + mTargetViewContainer.show(); + } + + /** Animates the magnetic dismiss target out and then sets it to GONE. */ + public void hideDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + mTargetViewContainer.hide(); + } + + /** + * Removes the dismiss target and cancels any pending callbacks to show it. + */ + public void cleanUpDismissTarget() { + if (mTargetViewContainer.getParent() != null) { + mWindowManager.removeViewImmediate(mTargetViewContainer); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java new file mode 100644 index 000000000000..03547a55fa27 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.IWindowManager; +import android.view.InputChannel; +import android.view.InputEvent; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * Manages the input consumer that allows the Shell to directly receive input. + */ +public class PipInputConsumer { + + private static final String TAG = PipInputConsumer.class.getSimpleName(); + + /** + * Listener interface for callers to subscribe to input events. + */ + public interface InputListener { + /** Handles any input event. */ + boolean onInputEvent(InputEvent ev); + } + + /** + * Listener interface for callers to learn when this class is registered or unregistered with + * window manager + */ + private interface RegistrationListener { + void onRegistrationChanged(boolean isRegistered); + } + + /** + * Input handler used for the input consumer. Input events are batched and consumed with the + * SurfaceFlinger vsync. + */ + private final class InputEventReceiver extends BatchedInputEventReceiver { + + InputEventReceiver(InputChannel inputChannel, Looper looper, + Choreographer choreographer) { + super(inputChannel, looper, choreographer); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = true; + try { + if (mListener != null) { + handled = mListener.onInputEvent(event); + } + } finally { + finishInputEvent(event, handled); + } + } + } + + private final IWindowManager mWindowManager; + private final IBinder mToken; + private final String mName; + private final ShellExecutor mMainExecutor; + + private InputEventReceiver mInputEventReceiver; + private InputListener mListener; + private RegistrationListener mRegistrationListener; + + /** + * @param name the name corresponding to the input consumer that is defined in the system. + */ + public PipInputConsumer(IWindowManager windowManager, String name, + ShellExecutor mainExecutor) { + mWindowManager = windowManager; + mToken = new Binder(); + mName = name; + mMainExecutor = mainExecutor; + } + + /** + * Sets the input listener. + */ + public void setInputListener(InputListener listener) { + mListener = listener; + } + + /** + * Sets the registration listener. + */ + public void setRegistrationListener(RegistrationListener listener) { + mRegistrationListener = listener; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null); + } + }); + } + + /** + * Check if the InputConsumer is currently registered with WindowManager + * + * @return {@code true} if registered, {@code false} if not. + */ + public boolean isRegistered() { + return mInputEventReceiver != null; + } + + /** + * Registers the input consumer. + */ + public void registerInputConsumer() { + if (mInputEventReceiver != null) { + return; + } + final InputChannel inputChannel = new InputChannel(); + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); + mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to create input consumer, %s", TAG, e); + } + mMainExecutor.execute(() -> { + mInputEventReceiver = new InputEventReceiver(inputChannel, + Looper.myLooper(), Choreographer.getInstance()); + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(true /* isRegistered */); + } + }); + } + + /** + * Unregisters the input consumer. + */ + public void unregisterInputConsumer() { + if (mInputEventReceiver == null) { + return; + } + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to destroy input consumer, %s", TAG, e); + } + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(false /* isRegistered */); + } + }); + } + + /** + * Dumps the {@link PipInputConsumer} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null)); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java new file mode 100644 index 000000000000..619bed4e19ca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY; +import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; +import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; + +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_DISMISS; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Debug; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.FloatProperties; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.common.pip.PipAppOpsListener; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipSnapAlgorithm; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.PhysicsAnimator; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + +import java.util.Optional; +import java.util.function.Consumer; + +/** + * A helper to animate and manipulate the PiP. + */ +public class PipMotionHelper implements PipAppOpsListener.Callback, + FloatingContentCoordinator.FloatingContent { + private static final String TAG = "PipMotionHelper"; + private static final boolean DEBUG = false; + + private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; + private static final int EXPAND_STACK_TO_MENU_DURATION = 250; + private static final int UNSTASH_DURATION = 250; + private static final int LEAVE_PIP_DURATION = 300; + private static final int SHIFT_DURATION = 300; + + /** Friction to use for PIP when it moves via physics fling animations. */ + private static final float DEFAULT_FRICTION = 1.9f; + /** How much of the dismiss circle size to use when scaling down PIP. **/ + private static final float DISMISS_CIRCLE_PERCENT = 0.85f; + + private final Context mContext; + private @NonNull PipBoundsState mPipBoundsState; + + private PhonePipMenuController mMenuController; + private PipSnapAlgorithm mSnapAlgorithm; + + /** The region that all of PIP must stay within. */ + private final Rect mFloatingAllowedArea = new Rect(); + + /** Coordinator instance for resolving conflicts with other floating content. */ + private FloatingContentCoordinator mFloatingContentCoordinator; + + @Nullable private final PipPerfHintController mPipPerfHintController; + @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + /** + * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} + * using physics animations. + */ + private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator; + + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}. + */ + private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; + + /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ + private PhysicsAnimator.FlingConfig mFlingConfigX; + private PhysicsAnimator.FlingConfig mFlingConfigY; + /** FlingConfig instances provided to PhysicsAnimator for stashing. */ + private PhysicsAnimator.FlingConfig mStashConfigX; + + /** SpringConfig to use for fling-then-spring animations. */ + private final PhysicsAnimator.SpringConfig mSpringConfig = + new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig used for animating into the dismiss region, matches the one in + * {@link MagnetizedObject}. */ + private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig = + new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss + * drag region. */ + private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig = + new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig to use for springing PIP away from conflicting floating content. */ + private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = + new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); + + private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { + if (mPipBoundsState.getBounds().equals(newBounds)) { + return; + } + + mMenuController.updateMenuLayout(newBounds); + mPipBoundsState.setBounds(newBounds); + }; + + /** + * Whether we're springing to the touch event location (vs. moving it to that position + * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was + * 'stuck' in the target and needs to catch up to the touch location. + */ + private boolean mSpringingToTouch = false; + + /** + * Whether PIP was released in the dismiss target, and will be animated out and dismissed + * shortly. + */ + private boolean mDismissalPending = false; + + /** + * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is + * used to show menu activity when the expand animation is completed. + */ + private Runnable mPostPipTransitionCallback; + + public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, + FloatingContentCoordinator floatingContentCoordinator, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + mContext = context; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mSnapAlgorithm = snapAlgorithm; + mFloatingContentCoordinator = floatingContentCoordinator; + mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mResizePipUpdateListener = (target, values) -> { + if (mPipBoundsState.getMotionBoundsState().isInMotion()) { + /* + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), + mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null); + */ + } + }; + } + + void init() { + mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + } + + @NonNull + @Override + public Rect getFloatingBoundsOnScreen() { + return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() + ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); + } + + @NonNull + @Override + public Rect getAllowedFloatingBoundsRegion() { + return mFloatingAllowedArea; + } + + @Override + public void moveToBounds(@NonNull Rect bounds) { + animateToBounds(bounds, mConflictResolutionSpringConfig); + } + + /** + * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. + */ + void synchronizePinnedStackBounds() { + cancelPhysicsAnimation(); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + + /* + if (mPipTaskOrganizer.isInPip()) { + mFloatingContentCoordinator.onContentMoved(this); + } + */ + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + */ + void movePip(Rect toBounds) { + movePip(toBounds, false /* isDragging */); + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + * + * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we + * won't notify the floating content coordinator of this move, since that will + * happen when the gesture ends. + */ + void movePip(Rect toBounds, boolean isDragging) { + if (!isDragging) { + mFloatingContentCoordinator.onContentMoved(this); + } + + if (!mSpringingToTouch) { + // If we are moving PIP directly to the touch event locations, cancel any animations and + // move PIP to the given bounds. + cancelPhysicsAnimation(); + + if (!isDragging) { + resizePipUnchecked(toBounds); + mPipBoundsState.setBounds(toBounds); + } else { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds); + /* + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds, + (Rect newBounds) -> { + mMenuController.updateMenuLayout(newBounds); + }); + */ + } + } else { + // If PIP is 'catching up' after being stuck in the dismiss target, update the animation + // to spring towards the new touch location. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig) + .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig) + .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig); + + startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */); + } + } + + /** Animates the PIP into the dismiss target, scaling it down. */ + void animateIntoDismissTarget( + MagnetizedObject.MagneticTarget target, + float velX, float velY, + boolean flung, Function0<Unit> after) { + final PointF targetCenter = target.getCenterOnScreen(); + + // PIP should fit in the circle + final float dismissCircleSize = mContext.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_size); + + final float width = getBounds().width(); + final float height = getBounds().height(); + final float ratio = width / height; + + // Width should be a little smaller than the circle size. + final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT; + final float desiredHeight = desiredWidth / ratio; + final float destinationX = targetCenter.x - (desiredWidth / 2f); + final float destinationY = targetCenter.y - (desiredHeight / 2f); + + // If we're already in the dismiss target area, then there won't be a move to set the + // temporary bounds, so just initialize it to the current bounds. + if (!mPipBoundsState.getMotionBoundsState().isInMotion()) { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig) + .withEndActions(after); + + startBoundsAnimator(destinationX, destinationY); + } + + /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ + void setSpringingToTouch(boolean springingToTouch) { + mSpringingToTouch = springingToTouch; + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * * fullscreen depending on the display area's windowing mode. + */ + void expandLeavePip(boolean skipAnimation) { + expandLeavePip(skipAnimation, false /* enterSplit */); + } + + /** + * Resizes the pinned task to split-screen mode. + */ + void expandIntoSplit() { + expandLeavePip(false, true /* enterSplit */); + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * fullscreen depending on the display area's windowing mode. + */ + private void expandLeavePip(boolean skipAnimation, boolean enterSplit) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exitPip: skipAnimation=%s" + + " callers=\n%s", TAG, skipAnimation, Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); + // mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit); + } + + /** + * Dismisses the pinned stack. + */ + @Override + public void dismissPip() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); + // mPipTaskOrganizer.removePip(); + } + + /** Sets the movement bounds to use to constrain PIP position animations. */ + void onMovementBoundsChanged() { + rebuildFlingConfigs(); + + // The movement bounds represent the area within which we can move PIP's top-left position. + // The allowed area for all of PIP is those bounds plus PIP's width and height. + mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds()); + mFloatingAllowedArea.right += getBounds().width(); + mFloatingAllowedArea.bottom += getBounds().height(); + } + + /** + * @return the PiP bounds. + */ + private Rect getBounds() { + return mPipBoundsState.getBounds(); + } + + /** + * Flings the PiP to the closest snap target. + */ + void flingToSnapTarget( + float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) { + movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */); + } + + /** + * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion. + */ + void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) { + velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY; + movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */); + } + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + private void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + private void movetoTarget( + float velocityX, + float velocityY, + @Nullable Runnable postBoundsUpdateCallback, + boolean isStash) { + // If we're flinging to a snap target now, we're not springing to catch up to the touch + // location now. + mSpringingToTouch = false; + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) + .flingThenSpring( + FloatProperties.RECT_X, velocityX, + isStash ? mStashConfigX : mFlingConfigX, + mSpringConfig, true /* flingMustReachMinOrMax */) + .flingThenSpring( + FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig); + + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + final float leftEdge = isStash + ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() + + insetBounds.left + : mPipBoundsState.getMovementBounds().left; + final float rightEdge = isStash + ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() + - insetBounds.right + : mPipBoundsState.getMovementBounds().right; + + final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; + + final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top; + final float estimatedFlingYEndValue = + PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY); + + startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, + postBoundsUpdateCallback); + } + + /** + * Animates PIP to the provided bounds, using physics animations and the given spring + * configuration + */ + void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + // Animate from the current bounds if we're not already animating. + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, bounds.left, springConfig) + .spring(FloatProperties.RECT_Y, bounds.top, springConfig); + startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */); + } + + /** + * Animates the dismissal of the PiP off the edge of the screen. + */ + void animateDismiss() { + // Animate off the bottom of the screen, then dismiss PIP. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_Y, + mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, + 0, + mSpringConfig) + .withEndActions(this::dismissPip); + + startBoundsAnimator( + getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); + + mDismissalPending = false; + } + + /** + * Animates the PiP to the expanded state to show the menu. + */ + float animateToExpandedState(Rect expandedBounds, Rect movementBounds, + Rect expandedMovementBounds, Runnable callback) { + float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), + movementBounds); + mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); + mPostPipTransitionCallback = callback; + resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); + return savedSnapFraction; + } + + /** + * Animates the PiP from the expanded state to the normal state after the menu is hidden. + */ + void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, + Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { + if (savedSnapFraction < 0f) { + // If there are no saved snap fractions, then just use the current bounds + savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), + currentMovementBounds, mPipBoundsState.getStashedState()); + } + + mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction, + mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), + mPipBoundsState.getDisplayBounds(), + mPipBoundsState.getDisplayLayout().stableInsets()); + + if (immediate) { + movePip(normalBounds); + } else { + resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); + } + } + + /** + * Animates the PiP to the stashed state, choosing the closest edge. + */ + void animateToStashedClosestEdge() { + Rect tmpBounds = new Rect(); + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + final int stashType = + mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left + ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT; + final float leftEdge = stashType == STASH_TYPE_LEFT + ? mPipBoundsState.getStashOffset() + - mPipBoundsState.getBounds().width() + insetBounds.left + : mPipBoundsState.getDisplayBounds().right + - mPipBoundsState.getStashOffset() - insetBounds.right; + tmpBounds.set((int) leftEdge, + mPipBoundsState.getBounds().top, + (int) (leftEdge + mPipBoundsState.getBounds().width()), + mPipBoundsState.getBounds().bottom); + resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION); + mPipBoundsState.setStashed(stashType); + } + + /** + * Animates the PiP from stashed state into un-stashed, popping it out from the edge. + */ + void animateToUnStashedBounds(Rect unstashedBounds) { + resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION); + } + + /** + * Animates the PiP to offset it from the IME or shelf. + */ + void animateToOffset(Rect originalBounds, int offset) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: animateToOffset: originalBounds=%s offset=%s" + + " callers=\n%s", TAG, originalBounds, offset, + Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + /* + mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, + mUpdateBoundsCallback); + */ + } + + /** + * Cancels all existing animations. + */ + private void cancelPhysicsAnimation() { + mTemporaryBoundsPhysicsAnimator.cancel(); + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + } + + /** Set new fling configs whose min/max values respect the given movement bounds. */ + private void rebuildFlingConfigs() { + mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().left, + mPipBoundsState.getMovementBounds().right); + mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().top, + mPipBoundsState.getMovementBounds().bottom); + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + mStashConfigX = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, + mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() + + insetBounds.left, + mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() + - insetBounds.right); + } + + private void startBoundsAnimator(float toX, float toY) { + startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */); + } + + /** + * Starts the physics animator which will update the animated PIP bounds using physics + * animations, as well as the TimeAnimator which will apply those bounds to PIP. + * + * This will also add end actions to the bounds animator that cancel the TimeAnimator and update + * the 'real' bounds to equal the final animated bounds. + * + * If one wishes to supply a callback after all the 'real' bounds update has happened, + * pass @param postBoundsUpdateCallback. + */ + private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) { + if (!mSpringingToTouch) { + cancelPhysicsAnimation(); + } + + setAnimatingToBounds(new Rect( + (int) toX, + (int) toY, + (int) toX + getBounds().width(), + (int) toY + getBounds().height())); + + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + if (mPipPerfHintController != null) { + // Start a high perf session with a timeout callback. + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "startBoundsAnimator"); + } + if (postBoundsUpdateCallback != null) { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd, + postBoundsUpdateCallback); + } else { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd); + } + } + + mTemporaryBoundsPhysicsAnimator.start(); + } + + /** + * Notify that PIP was released in the dismiss target and will be animated out and dismissed + * shortly. + */ + void notifyDismissalPending() { + mDismissalPending = true; + } + + private void onBoundsPhysicsAnimationEnd() { + // The physics animation ended, though we may not necessarily be done animating, such as + // when we're still dragging after moving out of the magnetic target. + if (!mDismissalPending + && !mSpringingToTouch + && !mMagnetizedPip.getObjectStuckToTarget()) { + // All motion operations have actually finished. + mPipBoundsState.setBounds( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + if (!mDismissalPending) { + // do not schedule resize if PiP is dismissing, which may cause app re-open to + // mBounds instead of its normal bounds. + // mPipTaskOrganizer.scheduleFinishResizePip(getBounds()); + } + } + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + mDismissalPending = false; + cleanUpHighPerfSessionMaybe(); + } + + /** + * Notifies the floating coordinator that we're moving, and sets the animating to bounds so + * we return these bounds from + * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + */ + private void setAnimatingToBounds(Rect bounds) { + mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds); + mFloatingContentCoordinator.onContentMoved(this); + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizePipUnchecked(Rect toBounds) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizePipUnchecked: toBounds=%s" + + " callers=\n%s", TAG, toBounds, Debug.getCallers(5, " ")); + } + if (!toBounds.equals(getBounds())) { + // mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); + } + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizeAndAnimatePipUnchecked: toBounds=%s" + + " duration=%s callers=\n%s", TAG, toBounds, duration, + Debug.getCallers(5, " ")); + } + + // Intentionally resize here even if the current bounds match the destination bounds. + // This is so all the proper callbacks are performed. + + // mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, + // TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); + // setAnimatingToBounds(toBounds); + } + + /** + * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the + * magnetic dismiss target so it can calculate PIP's size and position. + */ + MagnetizedObject<Rect> getMagnetizedPip() { + if (mMagnetizedPip == null) { + mMagnetizedPip = new MagnetizedObject<Rect>( + mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), + FloatProperties.RECT_X, FloatProperties.RECT_Y) { + @Override + public float getWidth(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.width(); + } + + @Override + public float getHeight(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.height(); + } + + @Override + public void getLocationOnScreen( + @NonNull Rect animatedPipBounds, @NonNull int[] loc) { + loc[0] = animatedPipBounds.left; + loc[1] = animatedPipBounds.top; + } + }; + mMagnetizedPip.setFlingToTargetEnabled(false); + } + + return mMagnetizedPip; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java new file mode 100644 index 000000000000..04cf350ddd3e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Looper; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; +import com.android.wm.shell.common.pip.PipUiEventLogger; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to + * trigger dynamic resize. + */ +public class PipResizeGestureHandler { + + private static final String TAG = "PipResizeGestureHandler"; + private static final int PINCH_RESIZE_SNAP_DURATION = 250; + private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f; + + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipBoundsState mPipBoundsState; + private final PipTouchState mPipTouchState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipUiEventLogger mPipUiEventLogger; + private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; + private final int mDisplayId; + private final ShellExecutor mMainExecutor; + + private final PointF mDownPoint = new PointF(); + private final PointF mDownSecondPoint = new PointF(); + private final PointF mLastPoint = new PointF(); + private final PointF mLastSecondPoint = new PointF(); + private final Point mMaxSize = new Point(); + private final Point mMinSize = new Point(); + private final Rect mLastResizeBounds = new Rect(); + private final Rect mUserResizeBounds = new Rect(); + private final Rect mDownBounds = new Rect(); + private final Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateResizeBoundsCallback; + + private float mTouchSlop; + + private boolean mAllowGesture; + private boolean mIsAttached; + private boolean mIsEnabled; + private boolean mEnablePinchResize; + private boolean mIsSysUiStateValid; + private boolean mThresholdCrossed; + private boolean mOngoingPinchToResize = false; + private float mAngle = 0; + int mFirstIndex = -1; + int mSecondIndex = -1; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + @Nullable + private final PipPerfHintController mPipPerfHintController; + + @Nullable + private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + private int mCtrlType; + private int mOhmOffset; + + public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipTouchState pipTouchState, + Runnable updateMovementBoundsRunnable, + PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, + ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { + mContext = context; + mDisplayId = context.getDisplayId(); + mMainExecutor = mainExecutor; + mPipPerfHintController = pipPerfHintController; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mPipTouchState = pipTouchState; + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + mPhonePipMenuController = menuActivityController; + mPipUiEventLogger = pipUiEventLogger; + mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + // mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; + } + + void init() { + mContext.getDisplay().getRealSize(mMaxSize); + reloadResources(); + + final Resources res = mContext.getResources(); + mEnablePinchResize = res.getBoolean(R.bool.config_pipEnablePinchResize); + } + + void onConfigurationChanged() { + reloadResources(); + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mIsSysUiStateValid = isSysUiStateValid; + } + + private void reloadResources() { + mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + void onActivityPinned() { + mIsAttached = true; + updateIsEnabled(); + } + + void onActivityUnpinned() { + mIsAttached = false; + mUserResizeBounds.setEmpty(); + updateIsEnabled(); + } + + private void updateIsEnabled() { + boolean isEnabled = mIsAttached; + if (isEnabled == mIsEnabled) { + return; + } + mIsEnabled = isEnabled; + disposeInputChannel(); + + if (mIsEnabled) { + // Register input event receiver + mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( + "pip-resize", mDisplayId); + try { + mMainExecutor.executeBlocking(() -> { + mInputEventReceiver = new PipResizeInputEventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to create input event receiver", e); + } + } + } + + @VisibleForTesting + void onInputEvent(InputEvent ev) { + if (!mEnablePinchResize) { + // No need to handle anything if neither form of resizing is enabled. + return; + } + + if (!mPipTouchState.getAllowInputEvents()) { + // No need to handle anything if touches are not enabled + return; + } + + // Don't allow resize when PiP is stashed. + if (mPipBoundsState.isStashed()) { + return; + } + + if (ev instanceof MotionEvent) { + MotionEvent mv = (MotionEvent) ev; + int action = mv.getActionMasked(); + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (!pipBounds.contains((int) mv.getRawX(), (int) mv.getRawY()) + && mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mEnablePinchResize && mOngoingPinchToResize) { + onPinchResize(mv); + } + } + } + + /** + * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize. + */ + public boolean hasOngoingGesture() { + return mCtrlType != CTRL_NONE || mOngoingPinchToResize; + } + + public boolean isUsingPinchToZoom() { + return mEnablePinchResize; + } + + public boolean isResizing() { + return mAllowGesture; + } + + boolean willStartResizeGesture(MotionEvent ev) { + if (isInValidSysUiState()) { + if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; + } + } + } + return false; + } + + private boolean isInValidSysUiState() { + return mIsSysUiStateValid; + } + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + private void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + @VisibleForTesting + void onPinchResize(MotionEvent ev) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + mAllowGesture = false; + finishResize(); + cleanUpHighPerfSessionMaybe(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1 + && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) + && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { + mAllowGesture = true; + mFirstIndex = 0; + mSecondIndex = 1; + mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + mDownBounds.set(pipBounds); + + mLastPoint.set(mDownPoint); + mLastSecondPoint.set(mLastSecondPoint); + mLastResizeBounds.set(mDownBounds); + + // start the high perf session as the second pointer gets detected + if (mPipPerfHintController != null) { + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "onPinchResize"); + } + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + mLastPoint.set(x0, y0); + mLastSecondPoint.set(x1, y1); + + // Capture inputs + if (!mThresholdCrossed + && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop + || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) { + pilferPointers(); + mThresholdCrossed = true; + // Reset the down to begin resizing from this point + mDownPoint.set(mLastPoint); + mDownSecondPoint.set(mLastSecondPoint); + + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mThresholdCrossed) { + mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint, + mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, + mDownBounds, mLastResizeBounds); + + /* + mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, + mAngle, null); + */ + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + + private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { + final int leftEdge = bounds.left; + + + final int fromLeft = Math.abs(leftEdge - movementBounds.left); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + + // The PIP will be snapped to either the right or left edge, so calculate which one + // is closest to the current position. + final int newLeft = fromLeft < fromRight + ? movementBounds.left : movementBounds.right; + + bounds.offsetTo(newLeft, mLastResizeBounds.top); + } + + /** + * Resizes the pip window and updates user-resized bounds. + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + Rect finalBounds = new Rect(bounds); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); + + // snap the target bounds to the either left or right edge, by choosing the closer one + snapToMovementBoundsEdge(finalBounds, movementBounds); + + // apply the requested snap fraction onto the target bounds + mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); + + // resize from current bounds to target bounds without animation + // mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); + // set the flag that pip has been resized + mPipBoundsState.setHasUserResizedPip(true); + + // finish the resize operation and update the state of the bounds + // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); + } + + private void finishResize() { + if (!mLastResizeBounds.isEmpty()) { + // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped + // position correctly. Drag-resize does not need to move, so just finalize resize. + if (mOngoingPinchToResize) { + final Rect startBounds = new Rect(mLastResizeBounds); + // If user resize is pretty close to max size, just auto resize to max. + if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x + || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); + } + + // If user resize is smaller than min size, auto resize to min + if (mLastResizeBounds.width() < mMinSize.x + || mLastResizeBounds.height() < mMinSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y); + } + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mLastResizeBounds, movementBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + + // disable any touch events beyond resizing too + mPipTouchState.setAllowInputEvents(false); + + /* + mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, + PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { + // enable touch events + mPipTouchState.setAllowInputEvents(true); + }); + */ + } else { + /* + mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, + TRANSITION_DIRECTION_USER_RESIZE, + mUpdateResizeBoundsCallback); + */ + } + final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); + } else { + resetState(); + } + } + + private void resetState() { + mCtrlType = CTRL_NONE; + mAngle = 0; + mOngoingPinchToResize = false; + mAllowGesture = false; + mThresholdCrossed = false; + } + + void setUserResizeBounds(Rect bounds) { + mUserResizeBounds.set(bounds); + } + + void invalidateUserResizeBounds() { + mUserResizeBounds.setEmpty(); + } + + Rect getUserResizeBounds() { + return mUserResizeBounds; + } + + @VisibleForTesting + Rect getLastResizeBounds() { + return mLastResizeBounds; + } + + @VisibleForTesting + void pilferPointers() { + mInputMonitor.pilferPointers(); + } + + + void updateMaxSize(int maxX, int maxY) { + mMaxSize.set(maxX, maxY); + } + + void updateMinSize(int minX, int minY) { + mMinSize.set(minX, minY); + } + + void setOhmOffset(int offset) { + mOhmOffset = offset; + } + + private float distanceBetween(PointF p1, PointF p2) { + return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); + } + + private void resizeRectAboutCenter(Rect rect, int w, int h) { + int cx = rect.centerX(); + int cy = rect.centerY(); + int l = cx - w / 2; + int r = l + w; + int t = cy - h / 2; + int b = t + h; + rect.set(l, t, r, b); + } + + /** + * Dumps the {@link PipResizeGestureHandler} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); + pw.println(innerPrefix + "mIsAttached=" + mIsAttached); + pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); + pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); + pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); + pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset); + pw.println(innerPrefix + "mMinSize=" + mMinSize); + pw.println(innerPrefix + "mMaxSize=" + mMaxSize); + } + + class PipResizeInputEventReceiver extends BatchedInputEventReceiver { + PipResizeInputEventReceiver(InputChannel channel, Looper looper) { + super(channel, looper, Choreographer.getInstance()); + } + + public void onInputEvent(InputEvent event) { + PipResizeGestureHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 895c793007a5..b4ca7df10292 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -30,9 +31,11 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; @@ -52,6 +55,7 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; private final ShellExecutor mMainExecutor; + private final ShellTaskOrganizer mShellTaskOrganizer; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; @@ -66,6 +70,16 @@ public class PipScheduler { // true if Launcher has started swipe PiP to home animation private boolean mInSwipePipToHomeTransition; + // Overlay leash potentially used during swipe PiP to home transition; + // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. + @Nullable + SurfaceControl mSwipePipToHomeOverlay; + + // App bounds used when as a starting point to swipe PiP to home animation in Launcher; + // these are also used to calculate the app icon overlay buffer size. + @NonNull + final Rect mSwipePipToHomeAppBounds = new Rect(); + /** * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -101,11 +115,14 @@ public class PipScheduler { } } - public PipScheduler(Context context, PipBoundsState pipBoundsState, - ShellExecutor mainExecutor) { + public PipScheduler(Context context, + PipBoundsState pipBoundsState, + ShellExecutor mainExecutor, + ShellTaskOrganizer shellTaskOrganizer) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; + mShellTaskOrganizer = shellTaskOrganizer; if (PipUtils.isPip2ExperimentEnabled()) { // temporary broadcast receiver to initiate exit PiP via expand @@ -115,6 +132,10 @@ public class PipScheduler { } } + ShellExecutor getMainExecutor() { + return mMainExecutor; + } + void setPipTransitionController(PipTransitionController pipTransitionController) { mPipTransitionController = pipTransitionController; } @@ -171,8 +192,26 @@ public class PipScheduler { mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); } - void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { + void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, + Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { mInSwipePipToHomeTransition = true; + mSwipePipToHomeOverlay = overlay; + mSwipePipToHomeAppBounds.set(appBounds); + if (overlay != null) { + // Shell transitions might use a root animation leash, which will be removed when + // the Recents transition is finished. Launcher attaches the overlay leash to this + // animation target leash; thus, we need to reparent it to the actual Task surface now. + // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP + // transition. + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx); + tx.setLayer(overlay, Integer.MAX_VALUE); + tx.apply(); + } + } + + void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { + mInSwipePipToHomeTransition = inSwipePipToHome; } boolean isInSwipePipToHomeTransition() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java new file mode 100644 index 000000000000..efa5fc8bf8b1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +/** + * A generic interface for a touch gesture. + */ +public abstract class PipTouchGesture { + + /** + * Handle the touch down. + */ + public void onDown(PipTouchState touchState) {} + + /** + * Handle the touch move, and return whether the event was consumed. + */ + public boolean onMove(PipTouchState touchState) { + return false; + } + + /** + * Handle the touch up, and return whether the gesture was consumed. + */ + public boolean onUp(PipTouchState touchState) { + return false; + } + + /** + * Cleans up the high performance hint session if needed. + */ + public void cleanUpHighPerfSessionMaybe() {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java new file mode 100644 index 000000000000..cc8e3e0934e6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -0,0 +1,1081 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_NONE; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.provider.DeviceConfig; +import android.util.Size; +import android.view.DisplayCutout; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.common.pip.SizeSpecSource; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.sysui.ShellInit; + +import java.io.PrintWriter; +import java.util.Optional; + +/** + * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding + * the PIP. + */ +public class PipTouchHandler { + + private static final String TAG = "PipTouchHandler"; + private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + + // Allow PIP to resize to a slightly bigger state upon touch + private boolean mEnableResize; + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + @NonNull private final PipBoundsState mPipBoundsState; + @NonNull private final SizeSpecSource mSizeSpecSource; + private final PipUiEventLogger mPipUiEventLogger; + private final PipDismissTargetHandler mPipDismissTargetHandler; + private final ShellExecutor mMainExecutor; + @Nullable private final PipPerfHintController mPipPerfHintController; + + private PipResizeGestureHandler mPipResizeGestureHandler; + + private final PhonePipMenuController mMenuController; + private final AccessibilityManager mAccessibilityManager; + + /** + * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the + * screen, it will be shown in "stashed" mode, where PIP will only show partially. + */ + private boolean mEnableStash = true; + + private float mStashVelocityThreshold; + + // The reference inset bounds, used to determine the dismiss fraction + private final Rect mInsetBounds = new Rect(); + + // Used to workaround an issue where the WM rotation happens before we are notified, allowing + // us to send stale bounds + private int mDeferResizeToNormalBoundsUntilRotation = -1; + private int mDisplayRotation; + + // Behaviour states + private int mMenuState = MENU_STATE_NONE; + private boolean mIsImeShowing; + private int mImeHeight; + private int mImeOffset; + private boolean mIsShelfShowing; + private int mShelfHeight; + private int mMovementBoundsExtraOffsets; + private int mBottomOffsetBufferPx; + private float mSavedSnapFraction = -1f; + private boolean mSendingHoverAccessibilityEvents; + private boolean mMovementWithinDismiss; + + // Touch state + private final PipTouchState mTouchState; + private final FloatingContentCoordinator mFloatingContentCoordinator; + private PipMotionHelper mMotionHelper; + private PipTouchGesture mGesture; + + // Temp vars + private final Rect mTmpBounds = new Rect(); + + /** + * A listener for the PIP menu activity. + */ + private class PipMenuListener implements PhonePipMenuController.Listener { + @Override + public void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + PipTouchHandler.this.onPipMenuStateChangeStart(menuState, resize, callback); + } + + @Override + public void onPipMenuStateChangeFinish(int menuState) { + setMenuState(menuState); + } + + @Override + public void onPipExpand() { + mMotionHelper.expandLeavePip(false /* skipAnimation */); + } + + @Override + public void onPipDismiss() { + mTouchState.removeDoubleTapTimeoutCallback(); + mMotionHelper.dismissPip(); + } + + @Override + public void onPipShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); + } + } + + @SuppressLint("InflateParams") + public PipTouchHandler(Context context, + ShellInit shellInit, + PhonePipMenuController menuController, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + @NonNull SizeSpecSource sizeSpecSource, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + ShellExecutor mainExecutor, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + mContext = context; + mMainExecutor = mainExecutor; + mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mSizeSpecSource = sizeSpecSource; + mMenuController = menuController; + mPipUiEventLogger = pipUiEventLogger; + mFloatingContentCoordinator = floatingContentCoordinator; + mMenuController.addListener(new PipMenuListener()); + mGesture = new DefaultPipTouchGesture(); + mMotionHelper = pipMotionHelper; + mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, + mMotionHelper, mainExecutor); + mTouchState = new PipTouchState(ViewConfiguration.get(context), + () -> { + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); + } + }, + menuController::hideMenu, + mainExecutor); + mPipResizeGestureHandler = + new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, + mTouchState, this::updateMovementBounds, pipUiEventLogger, + menuController, mainExecutor, mPipPerfHintController); + + if (PipUtils.isPip2ExperimentEnabled()) { + shellInit.addInitCallback(this::onInit, this); + } + } + + /** + * Called when the touch handler is initialized. + */ + public void onInit() { + Resources res = mContext.getResources(); + mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); + reloadResources(); + + mMotionHelper.init(); + mPipResizeGestureHandler.init(); + mPipDismissTargetHandler.init(); + + mEnableStash = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASHING, + /* defaultValue = */ true); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mMainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASHING)) { + mEnableStash = properties.getBoolean( + PIP_STASHING, /* defaultValue = */ true); + } + }); + mStashVelocityThreshold = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mMainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) { + mStashVelocityThreshold = properties.getFloat( + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + } + }); + } + + public PipTransitionController getTransitionHandler() { + // return mPipTaskOrganizer.getTransitionController(); + return null; + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); + mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); + mPipDismissTargetHandler.updateMagneticTargetSize(); + } + + void onOverlayChanged() { + // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly. + mPipDismissTargetHandler.init(); + } + + private boolean shouldShowResizeHandle() { + return false; + } + + void setTouchGesture(PipTouchGesture gesture) { + mGesture = gesture; + } + + void setTouchEnabled(boolean enabled) { + mTouchState.setAllowTouches(enabled); + } + + void showPictureInPictureMenu() { + // Only show the menu if the user isn't currently interacting with the PiP + if (!mTouchState.isUserInteracting()) { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + false /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + } + + void onActivityPinned() { + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + + mPipResizeGestureHandler.onActivityPinned(); + mFloatingContentCoordinator.onContentAdded(mMotionHelper); + } + + void onActivityUnpinned(ComponentName topPipActivity) { + if (topPipActivity == null) { + // Clean up state after the last PiP activity is removed + mPipDismissTargetHandler.cleanUpDismissTarget(); + + mFloatingContentCoordinator.onContentRemoved(mMotionHelper); + } + mPipResizeGestureHandler.onActivityUnpinned(); + } + + void onPinnedStackAnimationEnded( + @PipAnimationController.TransitionDirection int direction) { + // Always synchronize the motion helper bounds once PiP animations finish + mMotionHelper.synchronizePinnedStackBounds(); + updateMovementBounds(); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // Set the initial bounds as the user resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + } + } + + void onConfigurationChanged() { + mPipResizeGestureHandler.onConfigurationChanged(); + mMotionHelper.synchronizePinnedStackBounds(); + reloadResources(); + + /* + if (mPipTaskOrganizer.isInPip()) { + // Recreate the dismiss target for the new orientation. + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + } + */ + } + + void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mIsImeShowing = imeVisible; + mImeHeight = imeHeight; + } + + void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { + mIsShelfShowing = shelfVisible; + mShelfHeight = shelfHeight; + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid); + } + + void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { + final Rect toMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0); + final int prevBottom = mPipBoundsState.getMovementBounds().bottom + - mMovementBoundsExtraOffsets; + if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { + outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); + } + } + + /** + * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. + */ + public void onAspectRatioChanged() { + mPipResizeGestureHandler.invalidateUserResizeBounds(); + } + + void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, + boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { + // Set the user resized bounds equal to the new normal bounds in case they were + // invalidated (e.g. by an aspect ratio change). + if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { + mPipResizeGestureHandler.setUserResizeBounds(normalBounds); + } + + final int bottomOffset = mIsImeShowing ? mImeHeight : 0; + final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); + if (fromDisplayRotationChanged) { + mTouchState.reset(); + } + + // Re-calculate the expanded bounds + Rect normalMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds, + normalMovementBounds, bottomOffset); + + if (mPipBoundsState.getMovementBounds().isEmpty()) { + // mMovementBounds is not initialized yet and a clean movement bounds without + // bottom offset shall be used later in this function. + mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, + mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */); + } + + // Calculate the expanded size + float aspectRatio = (float) normalBounds.width() / normalBounds.height(); + Size expandedSize = mSizeSpecSource.getDefaultSize(aspectRatio); + mPipBoundsState.setExpandedBounds( + new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight())); + Rect expandedMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds( + mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds, + bottomOffset); + + updatePipSizeConstraints(normalBounds, aspectRatio); + + // The extra offset does not really affect the movement bounds, but are applied based on the + // current state (ime showing, or shelf offset) when we need to actually shift + int extraOffset = Math.max( + mIsImeShowing ? mImeOffset : 0, + !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); + + // Update the movement bounds after doing the calculations based on the old movement bounds + // above + mPipBoundsState.setNormalMovementBounds(normalMovementBounds); + mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); + mDisplayRotation = displayRotation; + mInsetBounds.set(insetBounds); + updateMovementBounds(); + mMovementBoundsExtraOffsets = extraOffset; + + // If we have a deferred resize, apply it now + if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { + mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, + mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(), + true /* immediate */); + mSavedSnapFraction = -1f; + mDeferResizeToNormalBoundsUntilRotation = -1; + } + } + + /** + * Update the values for min/max allowed size of picture in picture window based on the aspect + * ratio. + * @param aspectRatio aspect ratio to use for the calculation of min/max size + */ + public void updateMinMaxSize(float aspectRatio) { + updatePipSizeConstraints(mPipBoundsState.getNormalBounds(), + aspectRatio); + } + + private void updatePipSizeConstraints(Rect normalBounds, + float aspectRatio) { + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + updatePinchResizeSizeConstraints(aspectRatio); + } else { + mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), + mPipBoundsState.getExpandedBounds().height()); + } + } + + private void updatePinchResizeSizeConstraints(float aspectRatio) { + mPipBoundsState.updateMinMaxSize(aspectRatio); + mPipResizeGestureHandler.updateMinSize(mPipBoundsState.getMinSize().x, + mPipBoundsState.getMinSize().y); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + } + + /** + * TODO Add appropriate description + */ + public void onRegistrationChanged(boolean isRegistered) { + if (isRegistered) { + // Register the accessibility connection. + } else { + mAccessibilityManager.setPictureInPictureActionReplacingConnection(null); + } + if (!isRegistered && mTouchState.isUserInteracting()) { + // If the input consumer is unregistered while the user is interacting, then we may not + // get the final TOUCH_UP event, so clean up the dismiss target as well + mPipDismissTargetHandler.cleanUpDismissTarget(); + } + } + + private void onAccessibilityShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + + /** + * TODO Add appropriate description + */ + public boolean handleTouchEvent(InputEvent inputEvent) { + // Skip any non motion events + if (!(inputEvent instanceof MotionEvent)) { + return true; + } + + // do not process input event if not allowed + if (!mTouchState.getAllowInputEvents()) { + return true; + } + + MotionEvent ev = (MotionEvent) inputEvent; + if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) { + // Initialize the touch state for the gesture, but immediately reset to invalidate the + // gesture + mTouchState.onTouchEvent(ev); + mTouchState.reset(); + return true; + } + + if (mPipResizeGestureHandler.hasOngoingGesture()) { + mGesture.cleanUpHighPerfSessionMaybe(); + mPipDismissTargetHandler.hideDismissTargetMaybe(); + return true; + } + + if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) + && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) { + // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event + // to the touch state. Touch state needs a DOWN event in order to later process MOVE + // events it'll receive if the object is dragged out of the magnetic field. + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mTouchState.onTouchEvent(ev); + } + + // Continue tracking velocity when the object is in the magnetic field, since we want to + // respect touch input velocity if the object is dragged out and then flung. + mTouchState.addMovementToVelocityTracker(ev); + + return true; + } + + if (!mTouchState.isUserInteracting()) { + ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE, + "%s: Waiting to start the entry animation, skip the motion event.", TAG); + return true; + } + + // Update the touch state + mTouchState.onTouchEvent(ev); + + boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mGesture.onDown(mTouchState); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mGesture.onMove(mTouchState)) { + break; + } + + shouldDeliverToMenu = !mTouchState.isDragging(); + break; + } + case MotionEvent.ACTION_UP: { + // Update the movement bounds again if the state has changed since the user started + // dragging (ie. when the IME shows) + updateMovementBounds(); + + if (mGesture.onUp(mTouchState)) { + break; + } + } + // Fall through to clean up + case MotionEvent.ACTION_CANCEL: { + shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); + mTouchState.reset(); + break; + } + case MotionEvent.ACTION_HOVER_ENTER: { + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.removeHoverExitTimeoutCallback(); + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + false /* allowMenuTimeout */, false /* willResizeMenu */, + shouldShowResizeHandle()); + } + } + // Fall through + case MotionEvent.ACTION_HOVER_MOVE: { + if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mSendingHoverAccessibilityEvents = true; + } + break; + } + case MotionEvent.ACTION_HOVER_EXIT: { + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.scheduleHoverExitTimeoutCallback(); + } + if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mSendingHoverAccessibilityEvents = false; + } + break; + } + } + + shouldDeliverToMenu &= !mPipBoundsState.isStashed(); + + // Deliver the event to PipMenuActivity to handle button click if the menu has shown. + if (shouldDeliverToMenu) { + final MotionEvent cloneEvent = MotionEvent.obtain(ev); + // Send the cancel event and cancel menu timeout if it starts to drag. + if (mTouchState.startedDragging()) { + cloneEvent.setAction(MotionEvent.ACTION_CANCEL); + mMenuController.pokeMenu(); + } + + mMenuController.handlePointerEvent(cloneEvent); + cloneEvent.recycle(); + } + + return true; + } + + private void sendAccessibilityHoverEvent(int type) { + if (!mAccessibilityManager.isEnabled()) { + return; + } + + AccessibilityEvent event = AccessibilityEvent.obtain(type); + event.setImportantForAccessibility(true); + event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); + event.setWindowId( + AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); + mAccessibilityManager.sendAccessibilityEvent(event); + } + + /** + * Called when the PiP menu state is in the process of animating/changing from one to another. + */ + private void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + if (mMenuState == menuState && !resize) { + return; + } + + if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { + // Save the current snap fraction and if we do not drag or move the PiP, then + // we store back to this snap fraction. Otherwise, we'll reset the snap + // fraction and snap to the closest edge. + if (resize) { + // PIP is too small to show the menu actions and thus needs to be resized to a + // size that can fit them all. Resize to the default size. + animateToNormalSize(callback); + } + } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { + // Try and restore the PiP to the closest edge, using the saved snap fraction + // if possible + if (resize && !mPipResizeGestureHandler.isResizing()) { + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + // This is a very special case: when the menu is expanded and visible, + // navigating to another activity can trigger auto-enter PiP, and if the + // revealed activity has a forced rotation set, then the controller will get + // updated with the new rotation of the display. However, at the same time, + // SystemUI will try to hide the menu by creating an animation to the normal + // bounds which are now stale. In such a case we defer the animation to the + // normal bounds until after the next onMovementBoundsChanged() call to get the + // bounds in the new orientation + int displayRotation = mContext.getDisplay().getRotation(); + if (mDisplayRotation != displayRotation) { + mDeferResizeToNormalBoundsUntilRotation = displayRotation; + } + } + + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + animateToUnexpandedState(getUserResizeBounds()); + } + } else { + mSavedSnapFraction = -1f; + } + } + } + + private void setMenuState(int menuState) { + mMenuState = menuState; + updateMovementBounds(); + // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip + // as well, or it can't handle a11y focus and pip menu can't perform any action. + onRegistrationChanged(menuState == MENU_STATE_NONE); + if (menuState == MENU_STATE_NONE) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); + } else if (menuState == MENU_STATE_FULL) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); + } + } + + private void animateToMaximizedState(Runnable callback) { + Rect maxMovementBounds = new Rect(); + Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds, + mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds, + mPipBoundsState.getMovementBounds(), maxMovementBounds, + callback); + } + + private void animateToNormalSize(Runnable callback) { + // Save the current bounds as the user-resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + + final Size minMenuSize = mMenuController.getEstimatedMinMenuSize(); + final Rect normalBounds = mPipBoundsState.getNormalBounds(); + final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, + minMenuSize); + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(destBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds, + mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback); + } + + private void animateToUnexpandedState(Rect restoreBounds) { + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(restoreBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, + restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */); + mSavedSnapFraction = -1f; + } + + private void animateToUnStashedState() { + final Rect pipBounds = mPipBoundsState.getBounds(); + final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; + final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); + unStashedBounds.left = onLeftEdge ? mInsetBounds.left + : mInsetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() + : mInsetBounds.right; + mMotionHelper.animateToUnStashedBounds(unStashedBounds); + } + + /** + * @return the motion helper. + */ + public PipMotionHelper getMotionHelper() { + return mMotionHelper; + } + + @VisibleForTesting + public PipResizeGestureHandler getPipResizeGestureHandler() { + return mPipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { + mPipResizeGestureHandler = pipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipMotionHelper(PipMotionHelper pipMotionHelper) { + mMotionHelper = pipMotionHelper; + } + + Rect getUserResizeBounds() { + return mPipResizeGestureHandler.getUserResizeBounds(); + } + + /** + * Sets the user resize bounds tracked by {@link PipResizeGestureHandler} + */ + void setUserResizeBounds(Rect bounds) { + mPipResizeGestureHandler.setUserResizeBounds(bounds); + } + + /** + * Gesture controlling normal movement of the PIP. + */ + private class DefaultPipTouchGesture extends PipTouchGesture { + private final Point mStartPosition = new Point(); + private final PointF mDelta = new PointF(); + private boolean mShouldHideMenuAfterFling; + + @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + @Override + public void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + @Override + public void onDown(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return; + } + + if (mPipPerfHintController != null) { + // Cache the PiP high perf session to close it upon touch up. + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "DefaultPipTouchGesture#onDown"); + } + + Rect bounds = getPossiblyMotionBounds(); + mDelta.set(0f, 0f); + mStartPosition.set(bounds.left, bounds.top); + mMovementWithinDismiss = touchState.getDownTouchPosition().y + >= mPipBoundsState.getMovementBounds().bottom; + mMotionHelper.setSpringingToTouch(false); + // mPipDismissTargetHandler.setTaskLeash(mPipTaskOrganizer.getSurfaceControl()); + + // If the menu is still visible then just poke the menu + // so that it will timeout after the user stops touching it + if (mMenuState != MENU_STATE_NONE && !mPipBoundsState.isStashed()) { + mMenuController.pokeMenu(); + } + } + + @Override + public boolean onMove(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + + if (touchState.startedDragging()) { + mSavedSnapFraction = -1f; + mPipDismissTargetHandler.showDismissTargetMaybe(); + } + + if (touchState.isDragging()) { + mPipBoundsState.setHasUserMovedPip(true); + + // Move the pinned stack freely + final PointF lastDelta = touchState.getLastTouchDelta(); + float lastX = mStartPosition.x + mDelta.x; + float lastY = mStartPosition.y + mDelta.y; + float left = lastX + lastDelta.x; + float top = lastY + lastDelta.y; + + // Add to the cumulative delta after bounding the position + mDelta.x += left - lastX; + mDelta.y += top - lastY; + + mTmpBounds.set(getPossiblyMotionBounds()); + mTmpBounds.offsetTo((int) left, (int) top); + mMotionHelper.movePip(mTmpBounds, true /* isDragging */); + + final PointF curPos = touchState.getLastTouchPosition(); + if (mMovementWithinDismiss) { + // Track if movement remains near the bottom edge to identify swipe to dismiss + mMovementWithinDismiss = curPos.y >= mPipBoundsState.getMovementBounds().bottom; + } + return true; + } + return false; + } + + @Override + public boolean onUp(PipTouchState touchState) { + mPipDismissTargetHandler.hideDismissTargetMaybe(); + mPipDismissTargetHandler.setTaskLeash(null); + + if (!touchState.isUserInteracting()) { + return false; + } + + final PointF vel = touchState.getVelocity(); + + if (touchState.isDragging()) { + if (mMenuState != MENU_STATE_NONE) { + // If the menu is still visible, then just poke the menu so that + // it will timeout after the user stops touching it + mMenuController.showMenu(mMenuState, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; + + // Reset the touch state on up before the fling settles + mTouchState.reset(); + if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) { + mMotionHelper.stashToEdge(vel.x, vel.y, this::stashEndAction /* endAction */); + } else { + if (mPipBoundsState.isStashed()) { + // Reset stashed state if previously stashed + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } + mMotionHelper.flingToSnapTarget(vel.x, vel.y, + this::flingEndAction /* endAction */); + } + } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed() + && mMenuState != MENU_STATE_FULL) { + // If using pinch to zoom, double-tap functions as resizing between max/min size + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + final boolean toExpand = mPipBoundsState.getBounds().width() + < mPipBoundsState.getMaxSize().x + && mPipBoundsState.getBounds().height() + < mPipBoundsState.getMaxSize().y; + if (mMenuController.isMenuVisible()) { + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); + } + + // the size to toggle to after a double tap + int nextSize = PipDoubleTapHelper + .nextSizeSpec(mPipBoundsState, getUserResizeBounds()); + + // actually toggle to the size chosen + if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToMaximizedState(null); + } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToNormalSize(null); + } else { + animateToUnexpandedState(getUserResizeBounds()); + } + } else { + // Expand to fullscreen if this is a double tap + // the PiP should be frozen until the transition ends + setTouchEnabled(false); + mMotionHelper.expandLeavePip(false /* skipAnimation */); + } + } else if (mMenuState != MENU_STATE_FULL) { + if (mPipBoundsState.isStashed()) { + // Unstash immediately if stashed, and don't wait for the double tap timeout + animateToUnStashedState(); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + mTouchState.removeDoubleTapTimeoutCallback(); + } else if (!mTouchState.isWaitingForDoubleTap()) { + // User has stalled long enough for this not to be a drag or a double tap, + // just expand the menu + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } else { + // Next touch event _may_ be the second tap for the double-tap, schedule a + // fallback runnable to trigger the menu if no touch event occurs before the + // next tap + mTouchState.scheduleDoubleTapTimeoutCallback(); + } + } + cleanUpHighPerfSessionMaybe(); + return true; + } + + private void stashEndAction() { + if (mPipBoundsState.getBounds().left < 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT); + mPipBoundsState.setStashed(STASH_TYPE_LEFT); + } else if (mPipBoundsState.getBounds().left >= 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT); + mPipBoundsState.setStashed(STASH_TYPE_RIGHT); + } + mMenuController.hideMenu(); + } + + private void flingEndAction() { + if (mShouldHideMenuAfterFling) { + // If the menu is not visible, then we can still be showing the activity for the + // dismiss overlay, so just finish it after the animation completes + mMenuController.hideMenu(); + } + } + + private boolean shouldStash(PointF vel, Rect motionBounds) { + final boolean flingToLeft = vel.x < -mStashVelocityThreshold; + final boolean flingToRight = vel.x > mStashVelocityThreshold; + final int offset = motionBounds.width() / 2; + final boolean droppingOnLeft = + motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset; + final boolean droppingOnRight = + motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset; + + // Do not allow stash if the destination edge contains display cutout. We only + // compare the left and right edges since we do not allow stash on top / bottom. + final DisplayCutout displayCutout = + mPipBoundsState.getDisplayLayout().getDisplayCutout(); + if (displayCutout != null) { + if ((flingToLeft || droppingOnLeft) + && !displayCutout.getBoundingRectLeft().isEmpty()) { + return false; + } else if ((flingToRight || droppingOnRight) + && !displayCutout.getBoundingRectRight().isEmpty()) { + return false; + } + } + + // If user flings the PIP window above the minimum velocity, stash PIP. + // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite + // edge. + final boolean stashFromFlingToEdge = + (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) + || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT); + + // If User releases the PIP window while it's out of the display bounds, put + // PIP into stashed mode. + final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight; + + return stashFromFlingToEdge || stashFromDroppingOnEdge; + } + } + + /** + * Updates the current movement bounds based on whether the menu is currently visible and + * resized. + */ + private void updateMovementBounds() { + mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), + mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); + mMotionHelper.onMovementBoundsChanged(); + } + + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; + } + + /** + * @return {@code true} if the menu should be resized on tap because app explicitly specifies + * PiP window size that is too small to hold all the actions. + */ + private boolean willResizeMenu() { + if (!mEnableResize) { + return false; + } + final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); + if (estimatedMinMenuSize == null) { + ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get estimated menu size", TAG); + return false; + } + final Rect currentBounds = mPipBoundsState.getBounds(); + return currentBounds.width() < estimatedMinMenuSize.getWidth() + || currentBounds.height() < estimatedMinMenuSize.getHeight(); + } + + /** + * Returns the PIP bounds if we're not in the middle of a motion operation, or the current, + * temporary motion bounds otherwise. + */ + Rect getPossiblyMotionBounds() { + return mPipBoundsState.getMotionBoundsState().isInMotion() + ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion() + : mPipBoundsState.getBounds(); + } + + void setOhmOffset(int offset) { + mPipResizeGestureHandler.setOhmOffset(offset); + } + + /** + * Dumps the {@link PipTouchHandler} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); + pw.println(innerPrefix + "mImeHeight=" + mImeHeight); + pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); + pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); + pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mTouchState.dump(pw, innerPrefix); + if (mPipResizeGestureHandler != null) { + mPipResizeGestureHandler.dump(pw, innerPrefix); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java new file mode 100644 index 000000000000..d093f1e5ccc1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import android.graphics.PointF; +import android.view.Display; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * This keeps track of the touch state throughout the current touch gesture. + */ +public class PipTouchState { + private static final String TAG = "PipTouchState"; + private static final boolean DEBUG = false; + + @VisibleForTesting + public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + static final long HOVER_EXIT_TIMEOUT = 50; + + private final ShellExecutor mMainExecutor; + private final ViewConfiguration mViewConfig; + private final Runnable mDoubleTapTimeoutCallback; + private final Runnable mHoverExitTimeoutCallback; + + private VelocityTracker mVelocityTracker; + private long mDownTouchTime = 0; + private long mLastDownTouchTime = 0; + private long mUpTouchTime = 0; + private final PointF mDownTouch = new PointF(); + private final PointF mDownDelta = new PointF(); + private final PointF mLastTouch = new PointF(); + private final PointF mLastDelta = new PointF(); + private final PointF mVelocity = new PointF(); + private boolean mAllowTouches = true; + + // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing + private boolean mAllowInputEvents = true; + private boolean mIsUserInteracting = false; + // Set to true only if the multiple taps occur within the double tap timeout + private boolean mIsDoubleTap = false; + // Set to true only if a gesture + private boolean mIsWaitingForDoubleTap = false; + private boolean mIsDragging = false; + // The previous gesture was a drag + private boolean mPreviouslyDragging = false; + private boolean mStartedDragging = false; + private boolean mAllowDraggingOffscreen = false; + private int mActivePointerId; + private int mLastTouchDisplayId = Display.INVALID_DISPLAY; + + public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback, + Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) { + mViewConfig = viewConfig; + mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; + mHoverExitTimeoutCallback = hoverExitTimeoutCallback; + mMainExecutor = mainExecutor; + } + + /** + * @return true if input processing is enabled for PiP in general. + */ + public boolean getAllowInputEvents() { + return mAllowInputEvents; + } + + /** + * @param allowInputEvents true to enable input processing for PiP in general. + */ + public void setAllowInputEvents(boolean allowInputEvents) { + mAllowInputEvents = allowInputEvents; + } + + /** + * Resets this state. + */ + public void reset() { + mAllowDraggingOffscreen = false; + mIsDragging = false; + mStartedDragging = false; + mIsUserInteracting = false; + mLastTouchDisplayId = Display.INVALID_DISPLAY; + } + + /** + * Processes a given touch event and updates the state. + */ + public void onTouchEvent(MotionEvent ev) { + mLastTouchDisplayId = ev.getDisplayId(); + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (!mAllowTouches) { + return; + } + + // Initialize the velocity tracker + initOrResetVelocityTracker(); + addMovementToVelocityTracker(ev); + + mActivePointerId = ev.getPointerId(0); + if (DEBUG) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId); + } + mLastTouch.set(ev.getRawX(), ev.getRawY()); + mDownTouch.set(mLastTouch); + mAllowDraggingOffscreen = true; + mIsUserInteracting = true; + mDownTouchTime = ev.getEventTime(); + mIsDoubleTap = !mPreviouslyDragging + && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; + mIsWaitingForDoubleTap = false; + mIsDragging = false; + mLastDownTouchTime = mDownTouchTime; + if (mDoubleTapTimeoutCallback != null) { + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + } + break; + } + case MotionEvent.ACTION_MOVE: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId); + break; + } + + float x = ev.getRawX(pointerIndex); + float y = ev.getRawY(pointerIndex); + mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); + mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); + + boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); + if (!mIsDragging) { + if (hasMovedBeyondTap) { + mIsDragging = true; + mStartedDragging = true; + } + } else { + mStartedDragging = false; + } + mLastTouch.set(x, y); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // Select a new active pointer id and reset the movement state + final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (DEBUG) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Relinquish active pointer id on POINTER_UP: %d", + TAG, mActivePointerId); + } + mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); + } + break; + } + case MotionEvent.ACTION_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + mVelocityTracker.computeCurrentVelocity(1000, + mViewConfig.getScaledMaximumFlingVelocity()); + mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId); + break; + } + + mUpTouchTime = ev.getEventTime(); + mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); + mPreviouslyDragging = mIsDragging; + mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging + && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; + + } + // fall through to clean up + case MotionEvent.ACTION_CANCEL: { + recycleVelocityTracker(); + break; + } + case MotionEvent.ACTION_BUTTON_PRESS: { + removeHoverExitTimeoutCallback(); + break; + } + } + } + + /** + * @return the velocity of the active touch pointer at the point it is lifted off the screen. + */ + public PointF getVelocity() { + return mVelocity; + } + + /** + * @return the last touch position of the active pointer. + */ + public PointF getLastTouchPosition() { + return mLastTouch; + } + + /** + * @return the movement delta between the last handled touch event and the previous touch + * position. + */ + public PointF getLastTouchDelta() { + return mLastDelta; + } + + /** + * @return the down touch position. + */ + public PointF getDownTouchPosition() { + return mDownTouch; + } + + /** + * @return the movement delta between the last handled touch event and the down touch + * position. + */ + public PointF getDownTouchDelta() { + return mDownDelta; + } + + /** + * @return whether the user has started dragging. + */ + public boolean isDragging() { + return mIsDragging; + } + + /** + * @return whether the user is currently interacting with the PiP. + */ + public boolean isUserInteracting() { + return mIsUserInteracting; + } + + /** + * @return whether the user has started dragging just in the last handled touch event. + */ + public boolean startedDragging() { + return mStartedDragging; + } + + /** + * @return Display ID of the last touch event. + */ + public int getLastTouchDisplayId() { + return mLastTouchDisplayId; + } + + /** + * Sets whether touching is currently allowed. + */ + public void setAllowTouches(boolean allowTouches) { + mAllowTouches = allowTouches; + + // If the user happens to touch down before this is sent from the system during a transition + // then block any additional handling by resetting the state now + if (mIsUserInteracting) { + reset(); + } + } + + /** + * Disallows dragging offscreen for the duration of the current gesture. + */ + public void setDisallowDraggingOffscreen() { + mAllowDraggingOffscreen = false; + } + + /** + * @return whether dragging offscreen is allowed during this gesture. + */ + public boolean allowDraggingOffscreen() { + return mAllowDraggingOffscreen; + } + + /** + * @return whether this gesture is a double-tap. + */ + public boolean isDoubleTap() { + return mIsDoubleTap; + } + + /** + * @return whether this gesture will potentially lead to a following double-tap. + */ + public boolean isWaitingForDoubleTap() { + return mIsWaitingForDoubleTap; + } + + /** + * Schedules the callback to run if the next double tap does not occur. Only runs if + * isWaitingForDoubleTap() is true. + */ + public void scheduleDoubleTapTimeoutCallback() { + if (mIsWaitingForDoubleTap) { + long delay = getDoubleTapTimeoutCallbackDelay(); + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + mMainExecutor.executeDelayed(mDoubleTapTimeoutCallback, delay); + } + } + + long getDoubleTapTimeoutCallbackDelay() { + if (mIsWaitingForDoubleTap) { + return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); + } + return -1; + } + + /** + * Removes the timeout callback if it's in queue. + */ + public void removeDoubleTapTimeoutCallback() { + mIsWaitingForDoubleTap = false; + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + } + + void scheduleHoverExitTimeoutCallback() { + mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback); + mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT); + } + + void removeHoverExitTimeoutCallback() { + mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback); + } + + void addMovementToVelocityTracker(MotionEvent event) { + if (mVelocityTracker == null) { + return; + } + + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + float deltaX = event.getRawX() - event.getX(); + float deltaY = event.getRawY() - event.getY(); + event.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(event); + event.offsetLocation(-deltaX, -deltaY); + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Dumps the {@link PipTouchState}. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); + pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents); + pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); + pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId); + pw.println(innerPrefix + "mDownTouch=" + mDownTouch); + pw.println(innerPrefix + "mDownDelta=" + mDownDelta); + pw.println(innerPrefix + "mLastTouch=" + mLastTouch); + pw.println(innerPrefix + "mLastDelta=" + mLastDelta); + pw.println(innerPrefix + "mVelocity=" + mVelocity); + pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); + pw.println(innerPrefix + "mIsDragging=" + mIsDragging); + pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); + pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index dfb04758c851..e829d4ef650e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -17,13 +17,18 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; @@ -44,6 +49,7 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.pip.PipContentOverlay; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -55,6 +61,11 @@ import java.util.function.Consumer; */ public class PipTransition extends PipTransitionController { private static final String TAG = PipTransition.class.getSimpleName(); + /** + * The fixed start delay in ms when fading out the content overlay from bounds animation. + * The fadeout animation is guaranteed to start after the client has drawn under the new config. + */ + private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400; private final Context mContext; private final PipScheduler mPipScheduler; @@ -150,7 +161,7 @@ public class PipTransition extends PipTransitionController { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition == mEnterTransition) { + if (transition == mEnterTransition || info.getType() == TRANSIT_PIP) { mEnterTransition = null; if (mPipScheduler.isInSwipePipToHomeTransition()) { // If this is the second transition as a part of swipe PiP to home cuj, @@ -173,6 +184,10 @@ public class PipTransition extends PipTransitionController { mResizeTransition = null; return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } + + if (isRemovePipTransition(info)) { + return removePipImmediately(info, startTransaction, finishTransaction, finishCallback); + } return false; } @@ -226,12 +241,87 @@ public class PipTransition extends PipTransitionController { // cache the PiP task token and leash mPipScheduler.setPipTaskToken(mPipTaskToken); + SurfaceControl pipLeash = pipChange.getLeash(); + PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; + Rect srcRectHint = params.getSourceRectHint(); + Rect startBounds = pipChange.getStartAbsBounds(); + Rect destinationBounds = pipChange.getEndAbsBounds(); + + WindowContainerTransaction finishWct = new WindowContainerTransaction(); + + if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) { + final float scale = (float) destinationBounds.width() / srcRectHint.width(); + startTransaction.setWindowCrop(pipLeash, srcRectHint); + startTransaction.setPosition(pipLeash, + destinationBounds.left - srcRectHint.left * scale, + destinationBounds.top - srcRectHint.top * scale); + + // Reset the scale in case we are in the multi-activity case. + // TO_FRONT transition already scales down the task in single-activity case, but + // in multi-activity case, reparenting yields new reset scales coming from pinned task. + startTransaction.setScale(pipLeash, scale, scale); + } else { + final float scaleX = (float) destinationBounds.width() / startBounds.width(); + final float scaleY = (float) destinationBounds.height() / startBounds.height(); + final int overlaySize = PipContentOverlay.PipAppIconOverlay + .getOverlaySize(mPipScheduler.mSwipePipToHomeAppBounds, destinationBounds); + SurfaceControl overlayLeash = mPipScheduler.mSwipePipToHomeOverlay; + + startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) + .setScale(pipLeash, scaleX, scaleY) + .setWindowCrop(pipLeash, startBounds) + .reparent(overlayLeash, pipLeash) + .setLayer(overlayLeash, Integer.MAX_VALUE); + + if (mPipTaskToken != null) { + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), + this::onClientDrawAtTransitionEnd) + .setScale(overlayLeash, 1f, 1f) + .setPosition(overlayLeash, + (destinationBounds.width() - overlaySize) / 2f, + (destinationBounds.height() - overlaySize) / 2f); + finishWct.setBoundsChangeTransaction(mPipTaskToken, tx); + } + } startTransaction.apply(); - finishCallback.onTransitionFinished(null); + + // Note that finishWct should be free of any actual WM state changes; we are using + // it for syncing with the client draw after delayed configuration changes are dispatched. + finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct); return true; } + private void onClientDrawAtTransitionEnd() { + startOverlayFadeoutAnimation(); + } + + // + // Subroutines setting up and starting transitions' animations. + // + + private void startOverlayFadeoutAnimation() { + ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f); + animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.remove(mPipScheduler.mSwipePipToHomeOverlay); + tx.apply(); + mPipScheduler.mSwipePipToHomeOverlay = null; + } + }); + animator.addUpdateListener(animation -> { + float alpha = (float) animation.getAnimatedValue(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setAlpha(mPipScheduler.mSwipePipToHomeOverlay, alpha).apply(); + }); + animator.start(); + } + private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -246,6 +336,7 @@ public class PipTransition extends PipTransitionController { mPipScheduler.setPipTaskToken(mPipTaskToken); startTransaction.apply(); + // TODO: b/275910498 Use a new implementation of the PiP animator here. finishCallback.onTransitionFinished(null); return true; } @@ -273,11 +364,26 @@ public class PipTransition extends PipTransitionController { @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { startTransaction.apply(); + // TODO: b/275910498 Use a new implementation of the PiP animator here. + finishCallback.onTransitionFinished(null); + onExitPip(); + return true; + } + + private boolean removePipImmediately(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); finishCallback.onTransitionFinished(null); onExitPip(); return true; } + // + // Utility methods for checking PiP-related transition info and requests. + // + @Nullable private TransitionInfo.Change getPipChange(TransitionInfo info) { for (TransitionInfo.Change change : info.getChanges()) { @@ -303,6 +409,7 @@ public class PipTransition extends PipTransitionController { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); + wct.deferConfigToTransitionEnd(pipTask.token); return wct; } @@ -334,6 +441,25 @@ public class PipTransition extends PipTransitionController { && info.getChanges().size() == 1; } + private boolean isRemovePipTransition(@NonNull TransitionInfo info) { + if (mPipTaskToken == null) { + // PiP removal makes sense if enter-PiP has cached a valid pinned task token. + return false; + } + TransitionInfo.Change pipChange = info.getChange(mPipTaskToken); + if (pipChange == null) { + // Search for the PiP change by token since the windowing mode might be FULLSCREEN now. + return false; + } + + boolean isPipMovedToBack = info.getType() == TRANSIT_TO_BACK + && pipChange.getMode() == TRANSIT_TO_BACK; + boolean isPipClosed = info.getType() == TRANSIT_CLOSE + && pipChange.getMode() == TRANSIT_CLOSE; + // PiP is being removed if the pinned task is either moved to back or closed. + return isPipMovedToBack || isPipClosed; + } + /** * TODO: b/275910498 Use a new implementation of the PiP animator here. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index e8f58fe2bfad..62d195efb381 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -37,4 +37,9 @@ oneway interface IRecentTasksListener { * Called when a running task vanishes. */ void onRunningTaskVanished(in RunningTaskInfo taskInfo); + + /** + * Called when a running task changes. + */ + void onRunningTaskChanged(in RunningTaskInfo taskInfo); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index 2616b8b08bf1..77b8663861ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -16,7 +16,10 @@ package com.android.wm.shell.recents; -import com.android.wm.shell.common.annotations.ExternalThread; +import android.annotation.Nullable; +import android.graphics.Color; + +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.util.List; @@ -40,4 +43,12 @@ public interface RecentTasks { */ default void addAnimationStateListener(Executor listenerExecutor, Consumer<Boolean> listener) { } + + /** + * Sets a background color on the transition root layered behind the outgoing task. {@code null} + * may be used to clear any previously set colors to avoid showing a background at all. The + * color is always shown at full opacity. + */ + default void setTransitionBackgroundColor(@Nullable Color color) { + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 1c54754e9953..e7d9812e5393 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -19,6 +19,7 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.pm.PackageManager.FEATURE_PC; +import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; @@ -26,10 +27,10 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; -import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Color; import android.os.Bundle; import android.os.RemoteException; import android.util.Slog; @@ -49,11 +50,11 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -86,7 +87,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private final ActivityTaskManager mActivityTaskManager; private RecentsTransitionHandler mTransitionHandler = null; private IRecentTasksListener mListener; - private final boolean mIsDesktopMode; + private final boolean mPcFeatureEnabled; // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) @@ -133,7 +134,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellController = shellController; mShellCommandHandler = shellCommandHandler; mActivityTaskManager = activityTaskManager; - mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); + mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; mDesktopModeTaskRepository = desktopModeTaskRepository; mMainExecutor = mainExecutor; @@ -252,8 +253,10 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRunningTaskVanished(taskInfo); } - public void onTaskWindowingModeChanged(TaskInfo taskInfo) { + /** Notify listeners that the windowing mode of the given Task was updated. */ + public void onTaskWindowingModeChanged(ActivityManager.RunningTaskInfo taskInfo) { notifyRecentTasksChanged(); + notifyRunningTaskChanged(taskInfo); } @Override @@ -278,7 +281,9 @@ public class RecentTasksController implements TaskStackListenerCallback, * Notify the running task listener that a task appeared on desktop environment. */ private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { - if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + if (mListener == null + || !shouldEnableRunningTasksForDesktopMode() + || taskInfo.realActivity == null) { return; } try { @@ -292,7 +297,9 @@ public class RecentTasksController implements TaskStackListenerCallback, * Notify the running task listener that a task was removed on desktop environment. */ private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + if (mListener == null + || !shouldEnableRunningTasksForDesktopMode() + || taskInfo.realActivity == null) { return; } try { @@ -302,6 +309,27 @@ public class RecentTasksController implements TaskStackListenerCallback, } } + /** + * Notify the running task listener that a task was changed on desktop environment. + */ + private void notifyRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null + || !shouldEnableRunningTasksForDesktopMode() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskChanged(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskChanged", e); + } + } + + private boolean shouldEnableRunningTasksForDesktopMode() { + return mPcFeatureEnabled + || (DesktopModeStatus.isEnabled() && enableDesktopWindowingTaskbarRunningApps()); + } + @VisibleForTesting void registerRecentTasksListener(IRecentTasksListener listener) { mListener = listener; @@ -332,6 +360,8 @@ public class RecentTasksController implements TaskStackListenerCallback, ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>(); + int mostRecentFreeformTaskIndex = Integer.MAX_VALUE; + // Pull out the pairs as we iterate back in the list ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>(); for (int i = 0; i < rawList.size(); i++) { @@ -344,6 +374,9 @@ public class RecentTasksController implements TaskStackListenerCallback, if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent() && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { // Freeform tasks will be added as a separate entry + if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) { + mostRecentFreeformTaskIndex = recentTasks.size(); + } freeformTasks.add(taskInfo); continue; } @@ -362,7 +395,7 @@ public class RecentTasksController implements TaskStackListenerCallback, // Add a special entry for freeform tasks if (!freeformTasks.isEmpty()) { - recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks( + recentTasks.add(mostRecentFreeformTaskIndex, GroupedRecentTaskInfo.forFreeformTasks( freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); } @@ -444,6 +477,16 @@ public class RecentTasksController implements TaskStackListenerCallback, }); }); } + + @Override + public void setTransitionBackgroundColor(@Nullable Color color) { + mMainExecutor.execute(() -> { + if (mTransitionHandler == null) { + return; + } + mTransitionHandler.setTransitionBackgroundColor(color); + }); + } } @@ -471,6 +514,11 @@ public class RecentTasksController implements TaskStackListenerCallback, public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { mListener.call(l -> l.onRunningTaskVanished(taskInfo)); } + + @Override + public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onRunningTaskChanged(taskInfo)); + } }; public IRecentTasksImpl(RecentTasksController controller) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index b5ea1b1b43ea..c625b69deac0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -35,6 +36,7 @@ import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; import android.content.Intent; +import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -55,10 +57,13 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; @@ -90,6 +95,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>(); private final HomeTransitionObserver mHomeTransitionObserver; + private @Nullable Color mBackgroundColor; public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, @Nullable RecentTasksController recentTasksController, @@ -121,6 +127,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mStateListeners.add(listener); } + /** + * Sets a background color on the transition root layered behind the outgoing task. {@code null} + * may be used to clear any previously set colors to avoid showing a background at all. The + * color is always shown at full opacity. + */ + public void setTransitionBackgroundColor(@Nullable Color color) { + mBackgroundColor = color; + } + @VisibleForTesting public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener) { @@ -418,6 +433,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.start", mInstanceId); if (mListener == null || mTransition == null) { + Slog.e(TAG, "Missing listener or transition, hasListener=" + (mListener != null) + + " hasTransition=" + (mTransition != null)); cleanUp(); return false; } @@ -465,6 +482,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { final int belowLayers = info.getChanges().size(); final int middleLayers = info.getChanges().size() * 2; final int aboveLayers = info.getChanges().size() * 3; + + // Add a background color to each transition root in this transition. + if (mBackgroundColor != null) { + info.getChanges().stream() + .mapToInt((change) -> TransitionUtil.rootIndexFor(change, info)) + .distinct() + .mapToObj((rootIndex) -> info.getRoot(rootIndex).getLeash()) + .forEach((root) -> createBackgroundSurface(t, root, middleLayers)); + } + for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change change = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); @@ -531,21 +558,31 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { // Put into the "below" layer space. t.setLayer(change.getLeash(), layer); mOpeningTasks.add(new TaskState(change, null /* leash */)); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " unhandled root taskId=%d", taskInfo.taskId); } } else if (TransitionUtil.isDividerBar(change)) { final RemoteAnimationTarget target = TransitionUtil.newTarget(change, belowLayers - i, info, t, mLeashMap); // Add this as a app and we will separate them on launcher side by window type. apps.add(target); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " unhandled change taskId=%d", + taskInfo != null ? taskInfo.taskId : -1); } } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "Applying transaction=%d", t.getId()); t.apply(); Bundle b = new Bundle(1 /*capacity*/); b.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, mRecentTasksController.getSplitBoundsForTaskId(closingSplitTaskId)); try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.start: calling onAnimationStart", mInstanceId); + "[%d] RecentsController.start: calling onAnimationStart with %d apps", + mInstanceId, apps.size()); mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]), @@ -1011,13 +1048,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } if (mPipTransaction != null && sendUserLeaveHint) { SurfaceControl pipLeash = null; + TransitionInfo.Change pipChange = null; if (mPipTask != null) { - pipLeash = mInfo.getChange(mPipTask).getLeash(); + pipChange = mInfo.getChange(mPipTask); + pipLeash = pipChange.getLeash(); } else if (mPipTaskId != -1) { // find a task with taskId from #setFinishTaskTransaction() for (TransitionInfo.Change change : mInfo.getChanges()) { if (change.getTaskInfo() != null && change.getTaskInfo().taskId == mPipTaskId) { + pipChange = change; pipLeash = change.getLeash(); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.finishInner:" @@ -1036,6 +1076,28 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.finishInner: PiP transaction %s merged", mPipTransaction); + if (PipUtils.isPip2ExperimentEnabled()) { + // If this path is triggered, we are in auto-enter PiP flow in gesture + // navigation mode, which means "Recents" transition should be followed + // by a TRANSIT_PIP. Hence, we take the WCT was about to be sent + // to Core to be applied during finishTransition(), we modify it to + // factor in PiP changes, and we send it as a direct startWCT for + // a new TRANSIT_PIP type transition. Recents still sends + // finishTransition() to update visibilities, but with finishWCT=null. + TransitionRequestInfo requestInfo = new TransitionRequestInfo( + TRANSIT_PIP, null /* triggerTask */, pipChange.getTaskInfo(), + null /* remote */, null /* displayChange */, 0 /* flags */); + // Use mTransition IBinder token temporarily just to get PipTransition + // to return from its handleRequest(). The actual TRANSIT_PIP will have + // anew token once it arrives into PipTransition#startAnimation(). + Pair<Transitions.TransitionHandler, WindowContainerTransaction> + requestRes = mTransitions.dispatchRequest(mTransition, + requestInfo, null /* skip */); + wct.merge(requestRes.second, true); + mTransitions.startTransition(TRANSIT_PIP, wct, null /* handler */); + // We need to clear the WCT to send finishWCT=null for Recents. + wct.clear(); + } } mPipTaskId = -1; mPipTask = null; @@ -1057,7 +1119,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) { - if (tasks == null || tasks.isEmpty()) { + if (tasks == null) { return false; } for (int i = tasks.size() - 1; i >= 0; --i) { @@ -1068,6 +1130,29 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return true; } + private void createBackgroundSurface(SurfaceControl.Transaction transaction, + SurfaceControl parent, int layer) { + if (mBackgroundColor == null) { + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " adding background color to layer=%d", layer); + final SurfaceControl background = new SurfaceControl.Builder() + .setName("recents_background") + .setColorLayer() + .setOpaque(true) + .setParent(parent) + .build(); + transaction.setColor(background, colorToFloatArray(mBackgroundColor)); + transaction.setLayer(background, layer); + transaction.setAlpha(background, 1F); + transaction.show(background); + } + + private static float[] colorToFloatArray(@NonNull Color color) { + return new float[]{color.red(), color.green(), color.blue()}; + } + private void cleanUpPausingOrClosingTask(TaskState task, WindowContainerTransaction wct, SurfaceControl.Transaction finishTransaction, boolean sendUserLeaveHint) { if (!sendUserLeaveHint && task.isLeaf()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index ad4049320d93..6aad4e2c9da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -18,11 +18,16 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.graphics.Rect; +import android.os.Bundle; +import android.window.RemoteTransition; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.internal.logging.InstanceId; +import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; @@ -72,6 +77,12 @@ public interface SplitScreen { } } + /** Launches a pair of tasks into splitscreen */ + void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, + @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId); + /** Registers listener that gets split screen callback. */ void registerSplitScreenListener(@NonNull SplitScreenListener listener, @NonNull Executor executor); @@ -85,6 +96,9 @@ public interface SplitScreen { /** Called when requested to go to fullscreen from the current active split app. */ void goToFullscreenFromSplit(); + /** Called when splitscreen focused app is changed. */ + void setSplitscreenFocus(boolean leftOrTop); + /** Get a string representation of a stage type */ static String stageTypeToString(@StageType int stage) { switch (stage) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 952e2d4b3b9a..547457b018a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -90,7 +90,6 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.common.split.SplitScreenUtils; @@ -99,6 +98,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.DragAndDropPolicy; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -138,6 +138,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public static final int EXIT_REASON_RECREATE_SPLIT = 10; public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 11; public static final int EXIT_REASON_DESKTOP_MODE = 12; + public static final int EXIT_REASON_FULLSCREEN_REQUEST = 13; @IntDef(value = { EXIT_REASON_UNKNOWN, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, @@ -151,7 +152,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, EXIT_REASON_CHILD_TASK_ENTER_PIP, EXIT_REASON_RECREATE_SPLIT, EXIT_REASON_FULLSCREEN_SHORTCUT, - EXIT_REASON_DESKTOP_MODE + EXIT_REASON_DESKTOP_MODE, + EXIT_REASON_FULLSCREEN_REQUEST }) @Retention(RetentionPolicy.SOURCE) @interface ExitReason{} @@ -436,7 +438,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { - mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + if (ENABLE_SHELL_TRANSITIONS) { + mStageCoordinator.dismissSplitScreen(toTopTaskId, exitReason); + } else { + mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + } } @Override @@ -481,6 +487,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } } + public void setSplitscreenFocus(boolean leftOrTop) { + if (mStageCoordinator.isSplitActive()) { + mStageCoordinator.grantFocusToPosition(leftOrTop); + } + } + /** Move the specified task to fullscreen, regardless of focus state. */ public void moveTaskToFullscreen(int taskId, int exitReason) { mStageCoordinator.moveTaskToFullscreen(taskId, exitReason); @@ -494,6 +506,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.getActivateSplitPosition(taskInfo); } + /** Start two tasks in parallel as a splitscreen pair. */ + public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, + @PersistentSnapPosition int snapPosition, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + mStageCoordinator.startTasks(taskId1, options1, taskId2, options2, splitPosition, + snapPosition, remoteTransition, instanceId); + } + /** * Move a task to split select * @param taskInfo the task being moved to split select @@ -1044,6 +1065,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return "RECREATE_SPLIT"; case EXIT_REASON_DESKTOP_MODE: return "DESKTOP_MODE"; + case EXIT_REASON_FULLSCREEN_REQUEST: + return "FULLSCREEN_REQUEST"; default: return "unknown reason, reason int = " + exitReason; } @@ -1106,6 +1129,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, }; @Override + public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, int splitPosition, int snapPosition, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + mMainExecutor.execute(() -> SplitScreenController.this.startTasks( + taskId1, options1, taskId2, options2, splitPosition, snapPosition, + remoteTransition, instanceId)); + } + + @Override public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { if (mExecutors.containsKey(listener)) return; @@ -1142,6 +1174,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void goToFullscreenFromSplit() { mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit); } + + @Override + public void setSplitscreenFocus(boolean leftOrTop) { + mMainExecutor.execute( + () -> SplitScreenController.this.setSplitscreenFocus(leftOrTop)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java index 7f16c5e3592e..af11ebc515d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -17,6 +17,7 @@ package com.android.wm.shell.splitscreen; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -45,6 +46,8 @@ public class SplitScreenShellCommandHandler implements return runSetSideStagePosition(args, pw); case "switchSplitPosition": return runSwitchSplitPosition(); + case "exitSplitScreen": + return runExitSplitScreen(args, pw); default: pw.println("Invalid command: " + args[0]); return false; @@ -91,6 +94,17 @@ public class SplitScreenShellCommandHandler implements return true; } + private boolean runExitSplitScreen(String[] args, PrintWriter pw) { + if (args.length < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments"); + return false; + } + final int taskId = Integer.parseInt(args[1]); + mController.exitSplitScreen(taskId, EXIT_REASON_UNKNOWN); + return true; + } + @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>"); @@ -101,5 +115,7 @@ public class SplitScreenShellCommandHandler implements pw.println(prefix + " Sets the position of the side-stage."); pw.println(prefix + "switchSplitPosition"); pw.println(prefix + " Reverses the split."); + pw.println(prefix + "exitSplitScreen <taskId>"); + pw.println(prefix + " Exits split screen and leaves the provided split task on top."); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 9dd4c193a006..fadc9706af6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -29,6 +29,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -58,6 +59,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; @@ -1450,6 +1452,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mExitSplitScreenOnHide = exitSplitScreenOnHide; } + /** Exits split screen with legacy transition */ void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: topTaskId=%d reason=%s active=%b", toTopTaskId, exitReasonToString(exitReason), mMainStage.isActive()); @@ -1469,6 +1472,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, applyExitSplitScreen(childrenToTop, wct, exitReason); } + /** Exits split screen with legacy transition */ private void exitSplitScreen(@Nullable StageTaskListener childrenToTop, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: mainStageToTop=%b reason=%s active=%b", @@ -1546,6 +1550,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + void dismissSplitScreen(int toTopTaskId, @ExitReason int exitReason) { + if (!mMainStage.isActive()) return; + final int stage = getStageOfTask(toTopTaskId); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(stage, wct); + mSplitTransitions.startDismissTransition(wct, this, stage, exitReason); + } + /** * Overridden by child classes. */ @@ -1581,6 +1593,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + protected void grantFocusToPosition(boolean leftOrTop) { + int stageToFocus; + if (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + stageToFocus = leftOrTop ? getMainStagePosition() : getSideStagePosition(); + } else { + stageToFocus = leftOrTop ? getSideStagePosition() : getMainStagePosition(); + } + grantFocusToStage(stageToFocus); + } + private void clearRequestIfPresented() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented"); if (mSideStageListener.mVisible && mSideStageListener.mHasChildren @@ -1611,6 +1633,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // User has used a keyboard shortcut to go back to fullscreen from split case EXIT_REASON_DESKTOP_MODE: // One of the children enters desktop mode + case EXIT_REASON_UNKNOWN: + // Unknown reason return true; default: return false; @@ -2592,6 +2616,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering); + } else if (inFullscreen && isSplitScreenVisible()) { + // If the trigger task is in fullscreen and in split, exit split and place + // task on top + final int stageType = getStageOfTask(triggerTask.taskId); + prepareExitSplitScreen(stageType, out); + mSplitTransitions.setDismissTransition(transition, stageType, + EXIT_REASON_FULLSCREEN_REQUEST); } } else if (isOpening && inFullscreen) { final int activityType = triggerTask.getActivityType(); @@ -2776,7 +2807,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskInfo.taskId + " before startAnimation()."); record.addRecord(stage, true, taskInfo.taskId); } - } else if (isClosingType(change.getMode())) { + } else if (change.getMode() == TRANSIT_CLOSE) { if (stage.containsTask(taskInfo.taskId)) { record.addRecord(stage, false, taskInfo.taskId); Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index da2965c05ee4..2b12a22f907d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -251,9 +251,14 @@ public class SplashscreenContentDrawer { final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null ? windowInfo.targetActivityInfo : taskInfo.topActivityInfo; + final boolean isEdgeToEdgeEnforced = PhoneWindow.isEdgeToEdgeEnforced( + activityInfo.applicationInfo, false /* local */, a); + if (isEdgeToEdgeEnforced) { + params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; + } params.layoutInDisplayCutoutMode = a.getInt( R.styleable.Window_windowLayoutInDisplayCutoutMode, - PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */, a) + isEdgeToEdgeEnforced ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : params.layoutInDisplayCutoutMode); params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java index 31fc98b713ab..da3aa4adc42c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java @@ -461,25 +461,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { a.recycle(); } - // Reset the system bar color which set by splash screen, make it align to the app. - void clearSystemBarColor() { - if (mRootView == null || !mRootView.isAttachedToWindow()) { - return; - } - if (mRootView.getLayoutParams() instanceof WindowManager.LayoutParams) { - final WindowManager.LayoutParams lp = - (WindowManager.LayoutParams) mRootView.getLayoutParams(); - if (mDrawsSystemBarBackgrounds) { - lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } else { - lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } - mRootView.setLayoutParams(lp); - } - mRootView.getWindowInsetsController().setSystemBarsAppearance( - mSystemBarAppearance, LIGHT_BARS_MASK); - } - @Override public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { if (mRootView == null) { @@ -491,7 +472,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { removeWindowInner(mRootView, false); return true; } - clearSystemBarColor(); if (immediately || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { removeWindowInner(mRootView, false); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index e2be1533118a..3353c7bd81c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -18,10 +18,12 @@ package com.android.wm.shell.startingsurface; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NONE; import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL; import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION; import android.annotation.CallSuper; +import android.annotation.NonNull; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.Context; @@ -45,8 +47,8 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ShellSplashscreenThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; /** * A class which able to draw splash screen or snapshot as the starting window for a task. @@ -269,21 +271,18 @@ public class StartingSurfaceDrawer { @Override public final boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { - if (immediately) { + if (immediately + // Show the latest content as soon as possible for unlocking to home. + || mActivityType == ACTIVITY_TYPE_HOME + || info.deferRemoveMode == DEFER_MODE_NONE) { removeImmediately(); - } else { - scheduleRemove(info.deferRemoveForImeMode); - return false; + return true; } - return true; + scheduleRemove(info.deferRemoveMode); + return false; } void scheduleRemove(@StartingWindowRemovalInfo.DeferMode int deferRemoveForImeMode) { - // Show the latest content as soon as possible for unlocking to home. - if (mActivityType == ACTIVITY_TYPE_HOME) { - removeImmediately(); - return; - } mRemoveExecutor.removeCallbacks(mScheduledRunnable); final long delayRemovalTime; switch (deferRemoveForImeMode) { @@ -306,7 +305,7 @@ public class StartingSurfaceDrawer { @CallSuper protected void removeImmediately() { mRemoveExecutor.removeCallbacks(mScheduledRunnable); - mRecordManager.onRecordRemoved(mTaskId); + mRecordManager.onRecordRemoved(this, mTaskId); } } @@ -327,6 +326,11 @@ public class StartingSurfaceDrawer { } void addRecord(int taskId, StartingWindowRecord record) { + final StartingWindowRecord original = mStartingWindowRecords.get(taskId); + if (original != null) { + mTmpRemovalInfo.taskId = taskId; + original.removeIfPossible(mTmpRemovalInfo, true /* immediately */); + } mStartingWindowRecords.put(taskId, record); } @@ -346,8 +350,11 @@ public class StartingSurfaceDrawer { removeWindow(mTmpRemovalInfo, true/* immediately */); } - void onRecordRemoved(int taskId) { - mStartingWindowRecords.remove(taskId); + void onRecordRemoved(@NonNull StartingWindowRecord record, int taskId) { + final StartingWindowRecord currentRecord = mStartingWindowRecords.get(taskId); + if (currentRecord == record) { + mStartingWindowRecords.remove(taskId); + } } StartingWindowRecord getRecord(int taskId) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 1a0c011205fb..ceac40d9ba95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import android.annotation.BinderThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.graphics.Paint; @@ -42,6 +43,7 @@ import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.SnapshotDrawerUtils; import android.window.StartingWindowInfo; @@ -214,7 +216,7 @@ public class TaskSnapshotWindow { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, - boolean dragResizing) { + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) { final TaskSnapshotWindow snapshot = mOuter.get(); if (snapshot == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java new file mode 100644 index 000000000000..a94f80241d4f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import android.graphics.Rect; + +/** + * Callbacks for when the Display IME changes. + */ +public interface DisplayImeChangeListener { + /** + * Called when the ime bounds change. + */ + default void onImeBoundsChanged(int displayId, Rect bounds) {} + + /** + * Called when the IME visibility change. + */ + default void onImeVisibilityChanged(int displayId, boolean isShowing) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java index a7843e218a8a..5ced1fb41a41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -30,21 +30,28 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Bundle; import android.util.ArrayMap; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.SurfaceControlRegistry; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.io.PrintWriter; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; import java.util.function.Supplier; /** @@ -57,6 +64,7 @@ public class ShellController { private final ShellInit mShellInit; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mMainExecutor; + private final DisplayInsetsController mDisplayInsetsController; private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl(); private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners = @@ -65,6 +73,8 @@ public class ShellController { new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners = new CopyOnWriteArrayList<>(); + private final ConcurrentHashMap<DisplayImeChangeListener, Executor> mDisplayImeChangeListeners = + new ConcurrentHashMap<>(); private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers = new ArrayMap<>(); @@ -73,20 +83,53 @@ public class ShellController { private Configuration mLastConfiguration; + private OnInsetsChangedListener mInsetsChangeListener = new OnInsetsChangedListener() { + private InsetsState mInsetsState = new InsetsState(); + + @Override + public void insetsChanged(InsetsState insetsState) { + if (mInsetsState == insetsState) { + return; + } + + InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME); + boolean wasVisible = (oldSource != null && oldSource.isVisible()); + Rect oldFrame = wasVisible ? oldSource.getFrame() : null; + + InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME); + boolean isVisible = (newSource != null && newSource.isVisible()); + Rect newFrame = isVisible ? newSource.getFrame() : null; + + if (wasVisible != isVisible) { + onImeVisibilityChanged(isVisible); + } + + if (newFrame != null && !newFrame.equals(oldFrame)) { + onImeBoundsChanged(newFrame); + } + + mInsetsState = insetsState; + } + }; + public ShellController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, + DisplayInsetsController displayInsetsController, ShellExecutor mainExecutor) { mContext = context; mShellInit = shellInit; mShellCommandHandler = shellCommandHandler; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } private void onInit() { mShellCommandHandler.addDumpCallback(this::dump, this); + mDisplayInsetsController.addInsetsChangedListener( + mContext.getDisplayId(), mInsetsChangeListener); } /** @@ -259,6 +302,25 @@ public class ShellController { } } + @VisibleForTesting + void onImeBoundsChanged(Rect bounds) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime bounds changed"); + mDisplayImeChangeListeners.forEach( + (DisplayImeChangeListener listener, Executor executor) -> + executor.execute(() -> listener.onImeBoundsChanged( + mContext.getDisplayId(), bounds))); + } + + @VisibleForTesting + void onImeVisibilityChanged(boolean isShowing) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime visibility changed: isShowing=%b", + isShowing); + mDisplayImeChangeListeners.forEach( + (DisplayImeChangeListener listener, Executor executor) -> + executor.execute(() -> listener.onImeVisibilityChanged( + mContext.getDisplayId(), isShowing))); + } + private void handleInit() { SurfaceControlRegistry.createProcessInstance(mContext); mShellInit.init(); @@ -329,6 +391,19 @@ public class ShellController { } @Override + public void addDisplayImeChangeListener(DisplayImeChangeListener listener, + Executor executor) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Adding new DisplayImeChangeListener"); + mDisplayImeChangeListeners.put(listener, executor); + } + + @Override + public void removeDisplayImeChangeListener(DisplayImeChangeListener listener) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Removing DisplayImeChangeListener"); + mDisplayImeChangeListeners.remove(listener); + } + + @Override public boolean handleCommand(String[] args, PrintWriter pw) { try { boolean[] result = new boolean[1]; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java index bc5dd11ef54e..bd1c64a0d182 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java @@ -25,6 +25,7 @@ import androidx.annotation.NonNull; import java.io.PrintWriter; import java.util.List; +import java.util.concurrent.Executor; /** * General interface for notifying the Shell of common SysUI events like configuration or keyguard @@ -65,6 +66,18 @@ public interface ShellInterface { default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {} /** + * Registers a DisplayImeChangeListener to monitor for changes on Ime + * position and visibility. + */ + default void addDisplayImeChangeListener(DisplayImeChangeListener listener, + Executor executor) {} + + /** + * Removes a registered DisplayImeChangeListener. + */ + default void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {} + + /** * Handles a shell command. */ default boolean handleCommand(final String[] args, PrintWriter pw) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java index a7e4b0119480..f0a2315d7deb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java @@ -19,7 +19,7 @@ package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java index 7eed5883043d..e4fcff0c372a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java @@ -22,7 +22,7 @@ import android.content.Context; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index cb2944c120e0..b1a1e5999aa9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; import static com.android.wm.shell.transition.Transitions.TransitionObserver; import android.annotation.NonNull; @@ -32,6 +33,7 @@ import android.window.TransitionInfo; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.TransitionUtil; /** @@ -59,7 +61,8 @@ public class HomeTransitionObserver implements TransitionObserver, @NonNull SurfaceControl.Transaction finishTransaction) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null + if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP + || taskInfo == null || taskInfo.displayId != DEFAULT_DISPLAY || taskInfo.taskId == -1 || !taskInfo.isRunning) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java index 61e11e877b90..89b0e25b306b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -16,6 +16,8 @@ package com.android.wm.shell.transition; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS; + import android.annotation.NonNull; import android.os.RemoteException; import android.view.IRemoteAnimationFinishedCallback; @@ -26,6 +28,8 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IWindowContainerTransactionCallback; +import com.android.internal.protolog.common.ProtoLog; + /** * Utilities and interfaces for transition-like usage on top of the legacy app-transition and * synctransaction tools. @@ -87,9 +91,11 @@ public class LegacyTransitions { @Override public void onTransactionReady(int id, SurfaceControl.Transaction t) throws RemoteException { + ProtoLog.v(WM_SHELL_TRANSITIONS, + "LegacyTransitions.onTransactionReady(): syncId=%d", id); mSyncId = id; mTransaction = t; - checkApply(); + checkApply(true /* log */); } } @@ -103,20 +109,29 @@ public class LegacyTransitions { mWallpapers = wallpapers; mNonApps = nonApps; mFinishCallback = finishedCallback; - checkApply(); + checkApply(false /* log */); } @Override public void onAnimationCancelled() throws RemoteException { mCancelled = true; mApps = mWallpapers = mNonApps = null; - checkApply(); + checkApply(false /* log */); } } - private void checkApply() throws RemoteException { - if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return; + private void checkApply(boolean log) throws RemoteException { + if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) { + if (log) { + ProtoLog.v(WM_SHELL_TRANSITIONS, "\tSkipping hasFinishedCb=%b canceled=%b", + mFinishCallback != null, mCancelled); + } + return; + } + if (log) { + ProtoLog.v(WM_SHELL_TRANSITIONS, "\tapply"); + } mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers, mNonApps, mFinishCallback, mTransaction); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 4ea71490798c..5b402a5a7d53 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -212,6 +212,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { switch (mType) { case TYPE_RECENTS_DURING_DESKTOP: case TYPE_RECENTS_DURING_SPLIT: + case TYPE_RECENTS_DURING_KEYGUARD: mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); break; default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 274183dd9e2e..437a00e4a160 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -76,10 +76,13 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.IHomeTransitionListener; +import com.android.wm.shell.shared.IShellTransitions; +import com.android.wm.shell.shared.ShellTransitions; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -495,6 +498,7 @@ public class Transitions implements RemoteCallable<Transitions>, if (mode == TRANSIT_TO_FRONT) { // When the window is moved to front, make sure the crop is updated to prevent it // from using the old crop. + t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); t.setWindowCrop(leash, change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } @@ -506,6 +510,8 @@ public class Transitions implements RemoteCallable<Transitions>, t.setMatrix(leash, 1, 0, 0, 1); t.setAlpha(leash, 1.f); t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); } continue; } @@ -1405,6 +1411,8 @@ public class Transitions implements RemoteCallable<Transitions>, public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo, SurfaceControl.Transaction t, SurfaceControl.Transaction finishT) throws RemoteException { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady(transaction=%d)", + t.getId()); mMainExecutor.execute(() -> Transitions.this.onTransitionReady( iBinder, transitionInfo, t, finishT)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt index 7a50814f0275..564e716c7378 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt @@ -31,42 +31,42 @@ class KtProtoLog { companion object { /** @see [com.android.internal.protolog.common.ProtoLog.d] */ fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.d(group.tag, String.format(messageString, *args)) } } /** @see [com.android.internal.protolog.common.ProtoLog.v] */ fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.v(group.tag, String.format(messageString, *args)) } } /** @see [com.android.internal.protolog.common.ProtoLog.i] */ fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.i(group.tag, String.format(messageString, *args)) } } /** @see [com.android.internal.protolog.common.ProtoLog.w] */ fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.w(group.tag, String.format(messageString, *args)) } } /** @see [com.android.internal.protolog.common.ProtoLog.e] */ fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.e(group.tag, String.format(messageString, *args)) } } /** @see [com.android.internal.protolog.common.ProtoLog.wtf] */ fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (ProtoLog.isEnabled(group)) { + if (group.isLogToLogcat) { Log.wtf(group.tag, String.format(messageString, *args)) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index b2eeea7048bc..87dc3915082f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -19,21 +19,30 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.PackageManager.FEATURE_PC; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; +import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; +import android.content.ContentResolver; import android.content.Context; +import android.graphics.Rect; import android.os.Handler; +import android.provider.Settings; import android.util.SparseArray; import android.view.Choreographer; +import android.view.Display; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.window.DisplayAreaInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; @@ -51,6 +60,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final Handler mMainHandler; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; private TaskOperations mTaskOperations; @@ -63,6 +73,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { Choreographer mainChoreographer, ShellTaskOrganizer taskOrganizer, DisplayController displayController, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, Transitions transitions) { mContext = context; @@ -70,6 +81,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSyncQueue = syncQueue; mTransitions = transitions; if (!Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -156,10 +168,33 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { - return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD - && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode() - == WINDOWING_MODE_FREEFORM); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + return true; + } + if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { + return false; + } + final DisplayAreaInfo rootDisplayAreaInfo = + mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId); + if (rootDisplayAreaInfo != null) { + return rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FREEFORM; + } + + // It is possible that the rootDisplayAreaInfo is null when a task appears soon enough after + // a new display shows up, because TDA may appear after task appears in WM shell. Instead of + // fixing the synchronization issues, let's use other signals to "guess" the answer. It is + // OK in this context because no other captions other than the legacy developer option + // freeform and Kingyo/CF PC may use this class. WM shell should have full control over the + // condition where captions should show up in all new cases such as desktop mode, for which + // we should use different window decor view models. Ultimately Kingyo/CF PC may need to + // spin up their own window decor view model when they start to care about multiple + // displays. + if (isPc()) { + return true; + } + return taskInfo.displayId != Display.DEFAULT_DISPLAY + && forcesDesktopModeOnExternalDisplays(); } private void createWindowDecoration( @@ -186,7 +221,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { final FluidResizeTaskPositioner taskPositioner = new FluidResizeTaskPositioner(mTaskOrganizer, mTransitions, windowDecoration, - mDisplayController, 0 /* disallowedAreaForEndBoundsHeight */); + mDisplayController); final CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); @@ -229,7 +264,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mTaskOperations.minimizeTask(mTaskToken); } else if (id == R.id.maximize_window) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - mTaskOperations.maximizeTask(taskInfo); + final DisplayAreaInfo rootDisplayAreaInfo = + mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId); + mTaskOperations.maximizeTask(taskInfo, + rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode()); } } @@ -286,8 +324,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); - mDragPositioningCallback.onDragPositioningEnd( + final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(newTaskBounds, + mWindowDecorByTaskId.get(mTaskId).calculateValidDragArea()); + if (newTaskBounds != taskInfo.configuration.windowConfiguration.getBounds()) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(taskInfo.token, newTaskBounds); + mTransitions.startTransition(TRANSIT_CHANGE, wct, null); + } final boolean wasDragging = mIsDragging; mIsDragging = false; return wasDragging; @@ -296,4 +341,17 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return true; } } + + /** + * Returns if this device is a PC. + */ + private boolean isPc() { + return mContext.getPackageManager().hasSystemFeature(FEATURE_PC); + } + + private boolean forcesDesktopModeOnExternalDisplays() { + final ContentResolver resolver = mContext.getContentResolver(); + return Settings.Global.getInt(resolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0; + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 91e9601c6a27..43fd32ba1750 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -16,16 +16,23 @@ package com.android.wm.shell.windowdecor; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; + +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; +import android.util.Size; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; @@ -87,13 +94,16 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } @Override + @NonNull Rect calculateValidDragArea() { + final Context displayContext = mDisplayController.getDisplayContext(mTaskInfo.displayId); + if (displayContext == null) return new Rect(); final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(), R.dimen.caption_left_buttons_width); // On a smaller screen, don't require as much empty space on screen, as offscreen // drags will be restricted too much. - final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.displayId) + final int requiredEmptySpaceId = displayContext .getResources().getConfiguration().smallestScreenWidthDp >= 600 ? R.dimen.freeform_required_visible_empty_space_in_header : R.dimen.small_screen_required_visible_empty_space_in_header; @@ -218,7 +228,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mHandler, mChoreographer, mDisplay.getDisplayId(), - 0 /* taskCornerRadius */, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -230,12 +239,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - final int resize_handle = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_handle); - final int resize_corner = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_corner); - mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); + final Resources res = mResult.mRootView.getResources(); + mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, + new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), + getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index fa3d8a61fd04..777ab9c17218 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -25,17 +25,16 @@ import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; +import static android.view.MotionEvent.ACTION_HOVER_MOVE; +import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowInsets.Type.statusBars; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION; -import static com.android.wm.shell.windowdecor.MoveToDesktopAnimator.DRAG_FREEFORM_SCALE; +import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; @@ -71,6 +70,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -81,8 +81,10 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; @@ -119,9 +121,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; - private final Optional<DesktopTasksController> mDesktopTasksController; + private final DesktopTasksController mDesktopTasksController; private final InputManager mInputManager; - private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -129,8 +130,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final ExclusionRegionListener mExclusionRegionListener = new ExclusionRegionListenerImpl(); - private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId = - new SparseArray<>(); + private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId; private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); private final InputMonitorFactory mInputMonitorFactory; private TaskOperations mTaskOperations; @@ -197,7 +197,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + new SparseArray<>()); } @VisibleForTesting @@ -219,7 +220,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -231,7 +233,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDisplayInsetsController = displayInsetsController; mSyncQueue = syncQueue; mTransitions = transitions; - mDesktopTasksController = desktopTasksController; + mDesktopTasksController = desktopTasksController.get(); mShellCommandHandler = shellCommandHandler; mWindowManager = windowManager; mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; @@ -239,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTransactionFactory = transactionFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mInputManager = mContext.getSystemService(InputManager.class); + mWindowDecorByTaskId = windowDecorByTaskId; shellInit.addInitCallback(this::onInit, this); } @@ -248,8 +251,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mShellCommandHandler.addDumpCallback(this::dump, this); mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(), new DesktopModeOnInsetsChangedListener()); - mDesktopTasksController.ifPresent(c -> c.setOnTaskResizeAnimationListener( - new DeskopModeOnTaskResizeAnimationListener())); + mDesktopTasksController.setOnTaskResizeAnimationListener( + new DeskopModeOnTaskResizeAnimationListener()); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, mContext.getDisplayId()); @@ -273,7 +276,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null && DesktopModeStatus.isEnabled() && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { - mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo)); + mDesktopTasksController.moveToSplit(decor.mTaskInfo); } } } @@ -340,8 +343,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void destroyWindowDecoration(RunningTaskInfo taskInfo) { - final DesktopModeWindowDecoration decoration = - mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (decoration == null) return; decoration.close(); @@ -349,11 +351,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (mEventReceiversByDisplay.contains(displayId)) { removeTaskFromEventReceiver(displayId); } + // Remove the decoration from the cache last because WindowDecoration#close could still + // issue CANCEL MotionEvents to touch listeners before its view host is released. + // See b/327664694. + mWindowDecorByTaskId.remove(taskInfo.taskId); } private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, - View.OnGenericMotionListener , DragDetector.MotionEventHandler { + View.OnGenericMotionListener, DragDetector.MotionEventHandler { private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150; private final int mTaskId; @@ -402,7 +408,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(getOtherSplitTask(mTaskId).taskId, SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { - mTaskOperations.closeTask(mTaskToken); + WindowContainerTransaction wct = new WindowContainerTransaction(); + mDesktopTasksController.onDesktopWindowClose(wct, mTaskId); + mTaskOperations.closeTask(mTaskToken, wct); } } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(); @@ -414,13 +422,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.closeHandleMenu(); } } else if (id == R.id.desktop_button) { - if (mDesktopTasksController.isPresent()) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // App sometimes draws before the insets from WindowDecoration#relayout have - // been added, so they must be added here - mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); - mDesktopTasksController.get().moveToDesktop(mTaskId, wct); - } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // App sometimes draws before the insets from WindowDecoration#relayout have + // been added, so they must be added here + mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); + mDesktopTasksController.moveToDesktop(mTaskId, wct); decoration.closeHandleMenu(); } else if (id == R.id.fullscreen_button) { decoration.closeHandleMenu(); @@ -428,42 +434,31 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(mTaskId, SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { - mDesktopTasksController.ifPresent(c -> - c.moveToFullscreen(mTaskId)); + mDesktopTasksController.moveToFullscreen(mTaskId); } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); - mDesktopTasksController.ifPresent(c -> { - c.requestSplit(decoration.mTaskInfo); - }); + mDesktopTasksController.requestSplit(decoration.mTaskInfo); } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); - } else if (id == R.id.select_button) { - if (DesktopModeStatus.IS_DISPLAY_CHANGE_ENABLED) { - // TODO(b/278084491): dev option to enable display switching - // remove when select is implemented - mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId)); - } } else if (id == R.id.maximize_window) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); - mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo)); + mDesktopTasksController.toggleDesktopTaskSize(taskInfo); } else if (id == R.id.maximize_menu_maximize_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo)); + mDesktopTasksController.toggleDesktopTaskSize(taskInfo); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } else if (id == R.id.maximize_menu_snap_left_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen( - taskInfo, SnapPosition.LEFT)); + mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } else if (id == R.id.maximize_menu_snap_right_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen( - taskInfo, SnapPosition.RIGHT)); + mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } @@ -495,6 +490,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { (int) e.getRawX(), (int) e.getRawY()); final boolean isTransparentCaption = TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo); + // MotionEvent's coordinates are relative to view, we want location in window + // to offset position relative to caption as a whole. + int[] viewLocation = new int[2]; + v.getLocationInWindow(viewLocation); + final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + new Point(viewLocation[0], viewLocation[1])); // The caption window may be a spy window when the caption background is // transparent, which means events will fall through to the app window. Make // sure to cancel these events if they do not happen in the intersection of the @@ -502,11 +503,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // the drag-move or other caption gestures should take priority outside those // regions. mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion - && downInExclusionRegion && isTransparentCaption); + && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; } if (!mShouldPilferCaptionEvents) { - // The event will be handled by a window below. + // The event will be handled by a window below or pilfered by resize handler. return false; } // Otherwise pilfer so that windows below receive cancellations for this gesture, and @@ -553,8 +554,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // Re-hovering over any of the maximize menu views should keep the menu open by // cancelling any attempts to close the menu. mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); + if (id != R.id.maximize_window) { + decoration.onMaximizeMenuHoverEnter(id, ev); + } } return true; + } else if (ev.getAction() == ACTION_HOVER_MOVE + && MaximizeMenu.Companion.isMaximizeMenuView(id)) { + decoration.onMaximizeMenuHoverMove(id, ev); } else if (ev.getAction() == ACTION_HOVER_EXIT) { if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { decoration.onMaximizeWindowHoverExit(); @@ -564,6 +571,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // menu view to another. mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); + } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) { + decoration.onMaximizeMenuHoverExit(id, ev); } return true; } @@ -572,7 +581,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void moveTaskToFront(RunningTaskInfo taskInfo) { if (!taskInfo.isFocused) { - mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo)); + mDesktopTasksController.moveTaskToFront(taskInfo); } } @@ -606,7 +615,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // prevent the button's ripple effect from showing. return !touchingButton; } - case MotionEvent.ACTION_MOVE: { + case ACTION_MOVE: { // If a decor's resize drag zone is active, don't also try to reposition it. if (decoration.isHandlingDragResize()) break; decoration.closeMaximizeMenu(); @@ -616,10 +625,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final int dragPointerIdx = e.findPointerIndex(mDragPointerId); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo, + mDesktopTasksController.onDragPositioningMove(taskInfo, decoration.mTaskSurface, e.getRawX(dragPointerIdx), - newTaskBounds)); + newTaskBounds); mIsDragging = true; return true; } @@ -641,10 +650,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx))); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo, - position, + mDesktopTasksController.onDragPositioningEnd(taskInfo, position, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), - newTaskBounds)); + newTaskBounds, decoration.calculateValidDragArea()); if (touchingButton && !mHasLongClicked) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing @@ -672,10 +680,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } - mDesktopTasksController.ifPresent(c -> { - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - c.toggleDesktopTaskSize(decoration.mTaskInfo); - }); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); return true; } } @@ -793,7 +799,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) { return; } - + relevantDecor.updateHoverAndPressStatus(ev); final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (!mTransitionDragActive) { @@ -810,74 +816,87 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ private void handleCaptionThroughStatusBar(MotionEvent ev, DesktopModeWindowDecoration relevantDecor) { + if (relevantDecor == null) { + if (ev.getActionMasked() == ACTION_UP) { + mMoveToDesktopAnimator = null; + mTransitionDragActive = false; + } + return; + } switch (ev.getActionMasked()) { + case MotionEvent.ACTION_HOVER_EXIT: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_ENTER: { + relevantDecor.updateHoverAndPressStatus(ev); + break; + } case MotionEvent.ACTION_DOWN: { // Begin drag through status bar if applicable. - if (relevantDecor != null) { - mDragToDesktopAnimationStartBounds.set( - relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); - boolean dragFromStatusBarAllowed = false; - if (DesktopModeStatus.isEnabled()) { - // In proto2 any full screen or multi-window task can be dragged to - // freeform. - final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); - dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN - || windowingMode == WINDOWING_MODE_MULTI_WINDOW; - } + relevantDecor.checkTouchEvent(ev); + relevantDecor.updateHoverAndPressStatus(ev); + mDragToDesktopAnimationStartBounds.set( + relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); + boolean dragFromStatusBarAllowed = false; + if (DesktopModeStatus.isEnabled()) { + // In proto2 any full screen or multi-window task can be dragged to + // freeform. + final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); + dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN + || windowingMode == WINDOWING_MODE_MULTI_WINDOW; + } - if (dragFromStatusBarAllowed - && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) { - mTransitionDragActive = true; - } + if (dragFromStatusBarAllowed + && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) { + mTransitionDragActive = true; } break; } case MotionEvent.ACTION_UP: { - if (relevantDecor == null) { - mMoveToDesktopAnimator = null; - mTransitionDragActive = false; - return; - } if (mTransitionDragActive) { + mDesktopTasksController.updateVisualIndicator(relevantDecor.mTaskInfo, + relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()); mTransitionDragActive = false; - final int statusBarHeight = getStatusBarHeight( - relevantDecor.mTaskInfo.displayId); - if (ev.getRawY() > 2 * statusBarHeight) { - if (DesktopModeStatus.isEnabled()) { - animateToDesktop(relevantDecor, ev); - } - mMoveToDesktopAnimator = null; - return; - } else if (mMoveToDesktopAnimator != null) { - mDesktopTasksController.ifPresent( - c -> c.cancelDragToDesktop(relevantDecor.mTaskInfo)); + if (mMoveToDesktopAnimator != null) { + // Though this isn't a hover event, we need to update handle's hover state + // as it likely will change. + relevantDecor.updateHoverAndPressStatus(ev); + mDesktopTasksController.onDragPositioningEndThroughStatusBar( + new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo); mMoveToDesktopAnimator = null; return; + } else { + // In cases where we create an indicator but do not start the + // move-to-desktop animation, we need to dismiss it. + mDesktopTasksController.releaseVisualIndicator(); } } - relevantDecor.checkClickEvent(ev); + relevantDecor.checkTouchEvent(ev); break; } - case MotionEvent.ACTION_MOVE: { + case ACTION_MOVE: { if (relevantDecor == null) { return; } if (mTransitionDragActive) { - mDesktopTasksController.ifPresent( - c -> c.updateVisualIndicator( + // Do not create an indicator at all if we're not past transition height. + DisplayLayout layout = mDisplayController + .getDisplayLayout(relevantDecor.mTaskInfo.displayId); + if (ev.getRawY() < 2 * layout.stableInsets().top + && mMoveToDesktopAnimator == null) { + return; + } + final DesktopModeVisualIndicator.IndicatorType indicatorType = + mDesktopTasksController.updateVisualIndicator( relevantDecor.mTaskInfo, - relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY())); - final int statusBarHeight = getStatusBarHeight( - relevantDecor.mTaskInfo.displayId); - if (ev.getRawY() > statusBarHeight) { + relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()); + if (indicatorType != TO_FULLSCREEN_INDICATOR) { if (mMoveToDesktopAnimator == null) { mMoveToDesktopAnimator = new MoveToDesktopAnimator( mContext, mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); - mDesktopTasksController.ifPresent( - c -> c.startDragToDesktop(relevantDecor.mTaskInfo, - mMoveToDesktopAnimator)); + mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo, + mMoveToDesktopAnimator); } } if (mMoveToDesktopAnimator != null) { @@ -894,70 +913,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - /** - * Gets bounds of a scaled window centered relative to the screen bounds - * @param scale the amount to scale to relative to the Screen Bounds - */ - private Rect calculateFreeformBounds(int displayId, float scale) { - // TODO(b/319819547): Account for app constraints so apps do not become letterboxed - final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); - final int screenWidth = displayLayout.width(); - final int screenHeight = displayLayout.height(); - - final float adjustmentPercentage = (1f - scale) / 2; - return new Rect((int) (screenWidth * adjustmentPercentage), - (int) (screenHeight * adjustmentPercentage), - (int) (screenWidth * (adjustmentPercentage + scale)), - (int) (screenHeight * (adjustmentPercentage + scale))); - } - - /** - * Blocks relayout until transition is finished and transitions to Desktop - */ - private void animateToDesktop(DesktopModeWindowDecoration relevantDecor, - MotionEvent ev) { - centerAndMoveToDesktopWithAnimation(relevantDecor, ev); - } - - /** - * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode. - * @param relevantDecor the window decor of the task to be animated - * @param ev the motion event that triggers the animation - */ - private void centerAndMoveToDesktopWithAnimation(DesktopModeWindowDecoration relevantDecor, - MotionEvent ev) { - ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); - animator.setDuration(FREEFORM_ANIMATION_DURATION); - final SurfaceControl sc = relevantDecor.mTaskSurface; - final Rect endBounds = calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE); - final Transaction t = mTransactionFactory.get(); - final float diffX = endBounds.centerX() - ev.getRawX(); - final float diffY = endBounds.top - ev.getRawY(); - final float startingX = ev.getRawX() - DRAG_FREEFORM_SCALE - * mDragToDesktopAnimationStartBounds.width() / 2; - - animator.addUpdateListener(animation -> { - final float animatorValue = (float) animation.getAnimatedValue(); - final float x = startingX + diffX * animatorValue; - final float y = ev.getRawY() + diffY * animatorValue; - t.setPosition(sc, x, y); - t.apply(); - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mDesktopTasksController.ifPresent( - c -> { - c.onDragPositioningEndThroughStatusBar(relevantDecor.mTaskInfo, - calculateFreeformBounds(ev.getDisplayId(), - DesktopTasksController - .DESKTOP_MODE_INITIAL_BOUNDS_SCALE)); - }); - } - }); - animator.start(); - } - @Nullable private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) { final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); @@ -1048,12 +1003,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && taskInfo.isFocused) { return false; } + if (Flags.enableDesktopWindowingModalsPolicy() + && isSingleTopActivityTranslucent(taskInfo)) { + return false; + } return DesktopModeStatus.isEnabled() + && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop() - && mDisplayController.getDisplayContext(taskInfo.displayId) - .getResources().getConfiguration().smallestScreenWidthDp >= 600; + && DesktopModeStatus.canEnterDesktopMode(mContext); } private void createWindowDecoration( @@ -1081,18 +1040,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { windowDecoration.createResizeVeil(); final DragPositioningCallback dragPositioningCallback; - final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize( - R.dimen.desktop_mode_transition_area_height); if (!DesktopModeStatus.isVeiledResizeEnabled()) { dragPositioningCallback = new FluidResizeTaskPositioner( mTaskOrganizer, mTransitions, windowDecoration, mDisplayController, - mDragStartListener, mTransactionFactory, transitionAreaHeight); + mDragStartListener, mTransactionFactory); windowDecoration.setTaskDragResizer( (FluidResizeTaskPositioner) dragPositioningCallback); } else { dragPositioningCallback = new VeiledResizeTaskPositioner( mTaskOrganizer, windowDecoration, mDisplayController, - mDragStartListener, mTransitions, transitionAreaHeight); + mDragStartListener, mTransitions); windowDecoration.setTaskDragResizer( (VeiledResizeTaskPositioner) dragPositioningCallback); } @@ -1181,12 +1138,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void onExclusionRegionChanged(int taskId, Region region) { - mDesktopTasksController.ifPresent(d -> d.onExclusionRegionChanged(taskId, region)); + mDesktopTasksController.onExclusionRegionChanged(taskId, region); } @Override public void onExclusionRegionDismissed(int taskId) { - mDesktopTasksController.ifPresent(d -> d.removeExclusionRegionForTask(taskId)); + mDesktopTasksController.removeExclusionRegionForTask(taskId); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 39803e2afd34..da1699cd6e33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -19,12 +19,20 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.windowingModeToString; +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -36,6 +44,8 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.util.Log; +import android.util.Size; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -270,7 +280,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler, mChoreographer, mDisplay.getDisplayId(), - mRelayoutParams.mCornerRadius, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -282,15 +291,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - final int resize_handle = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_handle); - final int resize_corner = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_corner); - // If either task geometry or position have changed, update this task's // exclusion region listener + final Resources res = mResult.mRootView.getResources(); if (mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop) + new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, + new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), + getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop) || !mTaskInfo.positionInParent.equals(mPositionInParent)) { updateExclusionRegion(); } @@ -318,11 +325,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); - if (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor - && TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { - // App is requesting to customize the caption bar. Allow input to fall through to the - // windows below so that the app can respond to input events on their custom content. - relayoutParams.mAllowCaptionInputFallthrough = true; + if (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor) { + // If the app is requesting to customize the caption bar, allow input to fall through + // to the windows below so that the app can respond to input events on their custom + // content. + relayoutParams.mAllowCaptionInputFallthrough = + TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo); // Report occluding elements as bounding rects to the insets system so that apps can // draw in the empty space in the center: // First, the "app chip" section of the caption bar (+ some extra margins). @@ -399,7 +407,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int menuHeight = loadDimensionPixelSize( resources, R.dimen.desktop_mode_maximize_menu_height); - float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0]); + float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth + - maximizeWindowButton.getWidth()) / 2)); float menuTop = (mPositionInParent.y + captionHeight); final float menuRight = menuLeft + menuWidth; final float menuBottom = menuTop + menuHeight; @@ -419,20 +428,29 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mHandleMenu != null; } + boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); + } + boolean isHandlingDragResize() { return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize(); } private void loadAppInfo() { + final ActivityInfo activityInfo = mTaskInfo.topActivityInfo; + if (activityInfo == null) { + Log.e(TAG, "Top activity info not found in task"); + return; + } PackageManager pm = mContext.getApplicationContext().getPackageManager(); final IconProvider provider = new IconProvider(mContext); - mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo); + mAppIconDrawable = provider.getIcon(activityInfo); final Resources resources = mContext.getResources(); final BaseIconFactory factory = new BaseIconFactory(mContext, resources.getDisplayMetrics().densityDpi, resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius)); mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT); - final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo; + final ApplicationInfo applicationInfo = activityInfo.applicationInfo; mAppName = pm.getApplicationLabel(applicationInfo); } @@ -449,8 +467,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * until a resize event calls showResizeVeil below. */ void createResizeVeil() { - mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, - mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier); + mResizeVeil = new ResizeVeil(mContext, mDisplayController, mAppIconDrawable, mTaskInfo, + mTaskSurface, mSurfaceControlTransactionSupplier); } /** @@ -498,6 +516,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Determine valid drag area for this task based on elements in the app chip. */ @Override + @NonNull Rect calculateValidDragArea() { final int appTextWidth = ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder).getAppNameTextWidth(); @@ -706,27 +725,50 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Check a passed MotionEvent if a click has occurred on any button on this caption + * Check a passed MotionEvent if it has occurred on any button related to this decor. * Note this should only be called when a regular onClick is not possible * (i.e. the button was clicked through status bar layer) * * @param ev the MotionEvent to compare */ - void checkClickEvent(MotionEvent ev) { + void checkTouchEvent(MotionEvent ev) { if (mResult.mRootView == null) return; - if (!isHandleMenuActive()) { - // Click if point in caption handle view - final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); - final View handle = caption.findViewById(R.id.caption_handle); - if (checkTouchEventInFocusedCaptionHandle(ev)) { - mOnCaptionButtonClickListener.onClick(handle); - } - } else { - mHandleMenu.checkClickEvent(ev); + final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final View handle = caption.findViewById(R.id.caption_handle); + final boolean inHandle = !isHandleMenuActive() + && checkTouchEventInFocusedCaptionHandle(ev); + final int action = ev.getActionMasked(); + if (action == ACTION_UP && inHandle) { + handle.performClick(); + } + if (isHandleMenuActive()) { + mHandleMenu.checkMotionEvent(ev); closeHandleMenuIfNeeded(ev); } } + /** + * Updates hover and pressed status of views in this decoration. Should only be called + * when status cannot be updated normally (i.e. the button is hovered through status + * bar layer). + * @param ev the MotionEvent to compare against. + */ + void updateHoverAndPressStatus(MotionEvent ev) { + if (mResult.mRootView == null) return; + final View handle = mResult.mRootView.findViewById(R.id.caption_handle); + final boolean inHandle = !isHandleMenuActive() + && checkTouchEventInFocusedCaptionHandle(ev); + final int action = ev.getActionMasked(); + // The comparison against ACTION_UP is needed for the cancel drag to desktop case. + handle.setHovered(inHandle && action != ACTION_UP); + // We want handle to remain pressed if the pointer moves outside of it during a drag. + handle.setPressed((inHandle && action == ACTION_DOWN) + || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL)); + if (isHandleMenuActive()) { + mHandleMenu.checkMotionEvent(ev); + } + } + private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -764,7 +806,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private Region getGlobalExclusionRegion() { Region exclusionRegion; - if (mTaskInfo.isResizeable) { + if (mDragResizeListener != null && mTaskInfo.isResizeable) { exclusionRegion = mDragResizeListener.getCornersRegion(); } else { exclusionRegion = new Region(); @@ -801,16 +843,34 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setAnimatingTaskResize(animatingTaskResize); } + /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeWindowHoverExit() { ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverExit(); } + /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ void onMaximizeWindowHoverEnter() { ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverEnter(); } + /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */ + void onMaximizeMenuHoverEnter(int id, MotionEvent ev) { + mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev); + } + + /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */ + void onMaximizeMenuHoverMove(int id, MotionEvent ev) { + mMaximizeMenu.onMaximizeMenuHoverMove(id, ev); + } + + /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */ + void onMaximizeMenuHoverExit(int id, MotionEvent ev) { + mMaximizeMenu.onMaximizeMenuHoverExit(id, ev); + } + + @Override public String toString() { return "{" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java index 8ce2d6d6d092..421ffd929fb2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java @@ -23,6 +23,9 @@ import android.graphics.Rect; * Callback called when receiving drag-resize or drag-move related input events. */ public interface DragPositioningCallback { + /** + * Indicates the direction of resizing. May be combined together to indicate a diagonal drag. + */ @IntDef(flag = true, value = { CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM }) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index 5afbd54088d1..82c399ad8152 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -131,7 +131,7 @@ public class DragPositioningCallbackUtility { t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top); } - private static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, + static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, float x, float y) { final float deltaX = x - repositionStartPoint.x; final float deltaY = y - repositionStartPoint.y; @@ -140,49 +140,32 @@ public class DragPositioningCallbackUtility { } /** - * Calculates the new position of the top edge of the task and returns true if it is below the - * disallowed area. - * - * @param disallowedAreaForEndBoundsHeight the height of the area that where the task positioner - * should not finalize the bounds using WCT#setBounds - * @param taskBoundsAtDragStart the bounds of the task on the first drag input event - * @param repositionStartPoint initial input coordinate - * @param y the y position of the motion event - * @return true if the top of the task is below the disallowed area + * If task bounds are outside of provided drag area, snap the bounds to be just inside the + * drag area. + * @param repositionTaskBounds bounds determined by task positioner + * @param validDragArea the area that task must be positioned inside + * @return whether bounds were modified */ - static boolean isBelowDisallowedArea(int disallowedAreaForEndBoundsHeight, - Rect taskBoundsAtDragStart, PointF repositionStartPoint, float y) { - final float deltaY = y - repositionStartPoint.y; - final float topPosition = taskBoundsAtDragStart.top + deltaY; - return topPosition > disallowedAreaForEndBoundsHeight; - } - - /** - * Updates repositionTaskBounds to the final bounds of the task after the drag is finished. If - * the bounds are outside of the valid drag area, the task is shifted back onto the edge of the - * valid drag area. - */ - static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, - PointF repositionStartPoint, float x, float y, Rect validDragArea) { - updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, - x, y); - snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea); - } - - private static void snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { + public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { // If we were never supplied a valid drag area, do not restrict movement. // Otherwise, we restrict deltas to keep task position inside the Rect. - if (validDragArea.width() == 0) return; + if (validDragArea.width() == 0) return false; + boolean result = false; if (repositionTaskBounds.left < validDragArea.left) { repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0); + result = true; } else if (repositionTaskBounds.left > validDragArea.right) { repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0); + result = true; } if (repositionTaskBounds.top < validDragArea.top) { repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top); + result = true; } else if (repositionTaskBounds.top > validDragArea.bottom) { repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top); + result = true; } + return result; } private static float getMinWidth(DisplayController displayController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index e83e5d1ef5a5..9624d46678bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -30,7 +30,9 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import android.annotation.NonNull; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; @@ -38,6 +40,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.util.Size; import android.view.Choreographer; import android.view.IWindowSession; import android.view.InputChannel; @@ -54,6 +57,7 @@ import android.window.InputTransferToken; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -65,40 +69,20 @@ import java.util.function.Supplier; class DragResizeInputListener implements AutoCloseable { private static final String TAG = "DragResizeInputListener"; private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); - private final Context mContext; - private final Handler mHandler; - private final Choreographer mChoreographer; - private final InputManager mInputManager; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; private final int mDisplayId; private final IBinder mClientToken; - private final InputTransferToken mInputTransferToken; private final SurfaceControl mDecorationSurface; private final InputChannel mInputChannel; private final TaskResizeInputEventReceiver mInputEventReceiver; - private final DragPositioningCallback mCallback; private final SurfaceControl mInputSinkSurface; private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; private final DisplayController mDisplayController; - - private int mTaskWidth; - private int mTaskHeight; - private int mResizeHandleThickness; - private int mCornerSize; - private int mTaskCornerRadius; - - private Rect mLeftTopCornerBounds; - private Rect mRightTopCornerBounds; - private Rect mLeftBottomCornerBounds; - private Rect mRightBottomCornerBounds; - - private int mDragPointerId = -1; - private DragDetector mDragDetector; private final Region mTouchRegion = new Region(); DragResizeInputListener( @@ -106,23 +90,17 @@ class DragResizeInputListener implements AutoCloseable { Handler handler, Choreographer choreographer, int displayId, - int taskCornerRadius, SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController) { - mInputManager = context.getSystemService(InputManager.class); - mContext = context; - mHandler = handler; - mChoreographer = choreographer; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayId = displayId; - mTaskCornerRadius = taskCornerRadius; mDecorationSurface = decorationSurface; mDisplayController = displayController; mClientToken = new Binder(); - mInputTransferToken = new InputTransferToken(); + final InputTransferToken inputTransferToken = new InputTransferToken(); mInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( @@ -135,18 +113,19 @@ class DragResizeInputListener implements AutoCloseable { INPUT_FEATURE_SPY, TYPE_APPLICATION, null /* windowToken */, - mInputTransferToken, + inputTransferToken, TAG + " of " + decorationSurface.toString(), mInputChannel); } catch (RemoteException e) { e.rethrowFromSystemServer(); } - mInputEventReceiver = new TaskResizeInputEventReceiver( - mInputChannel, mHandler, mChoreographer); - mCallback = callback; - mDragDetector = new DragDetector(mInputEventReceiver); - mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); + mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback, + handler, choreographer, () -> { + final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); + return new Size(layout.width(), layout.height()); + }, this::updateSinkInputChannel); + mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); mInputSinkSurface = surfaceControlBuilderSupplier.get() .setName("TaskInputSink of " + decorationSurface) @@ -170,7 +149,7 @@ class DragResizeInputListener implements AutoCloseable { INPUT_FEATURE_NO_INPUT_CHANNEL, TYPE_INPUT_CONSUMER, null /* windowToken */, - mInputTransferToken, + inputTransferToken, "TaskInputSink of " + decorationSurface, mSinkInputChannel); } catch (RemoteException e) { @@ -181,86 +160,26 @@ class DragResizeInputListener implements AutoCloseable { /** * Updates the geometry (the touch region) of this drag resize handler. * - * @param taskWidth The width of the task. - * @param taskHeight The height of the task. - * @param resizeHandleThickness The thickness of the resize handle in pixels. - * @param cornerSize The size of the resize handle centered in each corner. - * @param touchSlop The distance in pixels user has to drag with touch for it to register as - * a resize action. + * @param incomingGeometry The geometry update to apply for this task's drag resize regions. + * @param touchSlop The distance in pixels user has to drag with touch for it to register + * as a resize action. * @return whether the geometry has changed or not */ - boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize, - int touchSlop) { - if (mTaskWidth == taskWidth && mTaskHeight == taskHeight - && mResizeHandleThickness == resizeHandleThickness - && mCornerSize == cornerSize) { + boolean setGeometry(@NonNull DragResizeWindowGeometry incomingGeometry, int touchSlop) { + DragResizeWindowGeometry geometry = mInputEventReceiver.getGeometry(); + if (incomingGeometry.equals(geometry)) { + // Geometry hasn't changed size so skip all updates. return false; + } else { + geometry = incomingGeometry; } - - mTaskWidth = taskWidth; - mTaskHeight = taskHeight; - mResizeHandleThickness = resizeHandleThickness; - mCornerSize = cornerSize; - mDragDetector.setTouchSlop(touchSlop); + mInputEventReceiver.setTouchSlop(touchSlop); mTouchRegion.setEmpty(); - final Rect topInputBounds = new Rect( - -mResizeHandleThickness, - -mResizeHandleThickness, - mTaskWidth + mResizeHandleThickness, - 0); - mTouchRegion.union(topInputBounds); - - final Rect leftInputBounds = new Rect( - -mResizeHandleThickness, - 0, - 0, - mTaskHeight); - mTouchRegion.union(leftInputBounds); - - final Rect rightInputBounds = new Rect( - mTaskWidth, - 0, - mTaskWidth + mResizeHandleThickness, - mTaskHeight); - mTouchRegion.union(rightInputBounds); - - final Rect bottomInputBounds = new Rect( - -mResizeHandleThickness, - mTaskHeight, - mTaskWidth + mResizeHandleThickness, - mTaskHeight + mResizeHandleThickness); - mTouchRegion.union(bottomInputBounds); - - // Set up touch areas in each corner. - int cornerRadius = mCornerSize / 2; - mLeftTopCornerBounds = new Rect( - -cornerRadius, - -cornerRadius, - cornerRadius, - cornerRadius); - mTouchRegion.union(mLeftTopCornerBounds); - - mRightTopCornerBounds = new Rect( - mTaskWidth - cornerRadius, - -cornerRadius, - mTaskWidth + cornerRadius, - cornerRadius); - mTouchRegion.union(mRightTopCornerBounds); - - mLeftBottomCornerBounds = new Rect( - -cornerRadius, - mTaskHeight - cornerRadius, - cornerRadius, - mTaskHeight + cornerRadius); - mTouchRegion.union(mLeftBottomCornerBounds); - - mRightBottomCornerBounds = new Rect( - mTaskWidth - cornerRadius, - mTaskHeight - cornerRadius, - mTaskWidth + cornerRadius, - mTaskHeight + cornerRadius); - mTouchRegion.union(mRightBottomCornerBounds); + // Apply the geometry to the touch region. + geometry.union(mTouchRegion); + mInputEventReceiver.setGeometry(geometry); + mInputEventReceiver.setTouchRegion(mTouchRegion); try { mWindowSession.updateInputChannel( @@ -275,8 +194,9 @@ class DragResizeInputListener implements AutoCloseable { e.rethrowFromSystemServer(); } + final Size taskSize = geometry.getTaskSize(); mSurfaceControlTransactionSupplier.get() - .setWindowCrop(mInputSinkSurface, mTaskWidth, mTaskHeight) + .setWindowCrop(mInputSinkSurface, taskSize.getWidth(), taskSize.getHeight()) .apply(); // The touch region of the TaskInputSink should be the touch region of this // DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent @@ -289,21 +209,16 @@ class DragResizeInputListener implements AutoCloseable { // issue. However, were there touchscreen-only a region out of the task bounds, mouse // gestures will become no-op in that region, even though the mouse gestures may appear to // be performed on the input window behind the resize handle. - mTouchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE); + mTouchRegion.op(0, 0, taskSize.getWidth(), taskSize.getHeight(), Region.Op.DIFFERENCE); updateSinkInputChannel(mTouchRegion); return true; } /** - * Generate a Region that encapsulates all 4 corner handles + * Generate a Region that encapsulates all 4 corner handles and window edges. */ - Region getCornersRegion() { - Region region = new Region(); - region.union(mLeftTopCornerBounds); - region.union(mLeftBottomCornerBounds); - region.union(mRightTopCornerBounds); - region.union(mRightBottomCornerBounds); - return region; + @NonNull Region getCornersRegion() { + return mInputEventReceiver.getCornersRegion(); } private void updateSinkInputChannel(Region region) { @@ -321,6 +236,10 @@ class DragResizeInputListener implements AutoCloseable { } } + boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return mInputEventReceiver.shouldHandleEvent(e, offset); + } + boolean isHandlingDragResize() { return mInputEventReceiver.isHandlingEvents(); } @@ -346,19 +265,37 @@ class DragResizeInputListener implements AutoCloseable { .apply(); } - private class TaskResizeInputEventReceiver extends InputEventReceiver - implements DragDetector.MotionEventHandler { - private final Choreographer mChoreographer; - private final Runnable mConsumeBatchEventRunnable; + private static class TaskResizeInputEventReceiver extends InputEventReceiver implements + DragDetector.MotionEventHandler { + @NonNull private final Context mContext; + private final InputManager mInputManager; + @NonNull private final InputChannel mInputChannel; + @NonNull private final DragPositioningCallback mCallback; + @NonNull private final Choreographer mChoreographer; + @NonNull private final Runnable mConsumeBatchEventRunnable; + @NonNull private final DragDetector mDragDetector; + @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier; + @NonNull private final Consumer<Region> mTouchRegionConsumer; + private final Rect mTmpRect = new Rect(); private boolean mConsumeBatchEventScheduled; + private DragResizeWindowGeometry mDragResizeWindowGeometry; + private Region mTouchRegion; private boolean mShouldHandleEvents; private int mLastCursorType = PointerIcon.TYPE_DEFAULT; private Rect mDragStartTaskBounds; - private final Rect mTmpRect = new Rect(); - - private TaskResizeInputEventReceiver( - InputChannel inputChannel, Handler handler, Choreographer choreographer) { + private int mDragPointerId = -1; + + private TaskResizeInputEventReceiver(@NonNull Context context, + @NonNull InputChannel inputChannel, + @NonNull DragPositioningCallback callback, @NonNull Handler handler, + @NonNull Choreographer choreographer, + @NonNull Supplier<Size> displayLayoutSizeSupplier, + @NonNull Consumer<Region> touchRegionConsumer) { super(inputChannel, handler.getLooper()); + mContext = context; + mInputManager = context.getSystemService(InputManager.class); + mInputChannel = inputChannel; + mCallback = callback; mChoreographer = choreographer; mConsumeBatchEventRunnable = () -> { @@ -371,6 +308,48 @@ class DragResizeInputListener implements AutoCloseable { scheduleConsumeBatchEvent(); } }; + + mDragDetector = new DragDetector(this); + mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; + mTouchRegionConsumer = touchRegionConsumer; + } + + /** + * Returns the geometry of the areas to drag resize. + */ + DragResizeWindowGeometry getGeometry() { + return mDragResizeWindowGeometry; + } + + /** + * Updates the geometry of the areas to drag resize. + */ + void setGeometry(@NonNull DragResizeWindowGeometry dragResizeWindowGeometry) { + mDragResizeWindowGeometry = dragResizeWindowGeometry; + } + + /** + * Sets how much slop to allow for touches. + */ + void setTouchSlop(int touchSlop) { + mDragDetector.setTouchSlop(touchSlop); + } + + /** + * Updates the region accepting input for drag resizing the task. + */ + void setTouchRegion(@NonNull Region touchRegion) { + mTouchRegion = touchRegion; + } + + /** + * Returns the union of all regions that can be touched for drag resizing; the corners and + * window edges. + */ + @NonNull Region getCornersRegion() { + Region region = new Region(); + mDragResizeWindowGeometry.union(region); + return region; } @Override @@ -408,21 +387,18 @@ class DragResizeInputListener implements AutoCloseable { boolean result = false; // Check if this is a touch event vs mouse event. // Touch events are tracked in four corners. Other events are tracked in resize edges. - boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + boolean isTouch = isTouchEvent(e); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - float x = e.getX(0); - float y = e.getY(0); - if (isTouch) { - mShouldHandleEvents = isInCornerBounds(x, y); - } else { - mShouldHandleEvents = isInResizeHandleBounds(x, y); - } + mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, isTouch, + new Point() /* offset */); if (mShouldHandleEvents) { mDragPointerId = e.getPointerId(0); + float x = e.getX(0); + float y = e.getY(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); - int ctrlType = calculateCtrlType(isTouch, x, y); + int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, rawX, rawY); // Increase the input sink region to cover the whole screen; this is to @@ -447,7 +423,6 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - mInputManager.pilferPointers(mInputChannel.getToken()); if (mShouldHandleEvents) { int dragPointerIndex = e.findPointerIndex(mDragPointerId); final Rect taskBounds = mCallback.onDragPositioningEnd( @@ -455,7 +430,7 @@ class DragResizeInputListener implements AutoCloseable { // If taskBounds has changed, setGeometry will be called and update the // sink region. Otherwise, we should revert it here. if (taskBounds.equals(mDragStartTaskBounds)) { - updateSinkInputChannel(mTouchRegion); + mTouchRegionConsumer.accept(mTouchRegion); } } mShouldHandleEvents = false; @@ -480,125 +455,20 @@ class DragResizeInputListener implements AutoCloseable { private void updateInputSinkRegionForDrag(Rect taskBounds) { mTmpRect.set(taskBounds); - final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); - final Region dragTouchRegion = new Region(-taskBounds.left, - -taskBounds.top, - -taskBounds.left + layout.width(), - -taskBounds.top + layout.height()); + final Size displayLayoutSize = mDisplayLayoutSizeSupplier.get(); + final Region dragTouchRegion = new Region(-taskBounds.left, -taskBounds.top, + -taskBounds.left + displayLayoutSize.getWidth(), + -taskBounds.top + displayLayoutSize.getHeight()); // Remove the localized task bounds from the touch region. mTmpRect.offsetTo(0, 0); dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE); - updateSinkInputChannel(dragTouchRegion); - } - - private boolean isInCornerBounds(float xf, float yf) { - return calculateCornersCtrlType(xf, yf) != 0; - } - - private boolean isInResizeHandleBounds(float x, float y) { - return calculateResizeHandlesCtrlType(x, y) != 0; - } - - @DragPositioningCallback.CtrlType - private int calculateCtrlType(boolean isTouch, float x, float y) { - if (isTouch) { - return calculateCornersCtrlType(x, y); - } - return calculateResizeHandlesCtrlType(x, y); - } - - @DragPositioningCallback.CtrlType - private int calculateResizeHandlesCtrlType(float x, float y) { - int ctrlType = 0; - // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with - // sides will use the bounds specified in setGeometry and not go into task bounds. - if (x < mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_LEFT; - } - if (x > mTaskWidth - mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_RIGHT; - } - if (y < mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_TOP; - } - if (y > mTaskHeight - mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_BOTTOM; - } - // Check distances from the center if it's in one of four corners. - if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 - && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { - return checkDistanceFromCenter(ctrlType, x, y); - } - // Otherwise, we should make sure we don't resize tasks inside task bounds. - return (x < 0 || y < 0 || x >= mTaskWidth || y >= mTaskHeight) ? ctrlType : 0; - } - - // If corner input is not within appropriate distance of corner radius, do not use it. - // If input is not on a corner or is within valid distance, return ctrlType. - @DragPositioningCallback.CtrlType - private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, - float x, float y) { - int centerX; - int centerY; - - // Determine center of rounded corner circle; this is simply the corner if radius is 0. - switch (ctrlType) { - case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: { - centerX = mTaskCornerRadius; - centerY = mTaskCornerRadius; - break; - } - case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: { - centerX = mTaskCornerRadius; - centerY = mTaskHeight - mTaskCornerRadius; - break; - } - case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: { - centerX = mTaskWidth - mTaskCornerRadius; - centerY = mTaskCornerRadius; - break; - } - case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: { - centerX = mTaskWidth - mTaskCornerRadius; - centerY = mTaskHeight - mTaskCornerRadius; - break; - } - default: { - throw new IllegalArgumentException("ctrlType should be complex, but it's 0x" - + Integer.toHexString(ctrlType)); - } - } - double distanceFromCenter = Math.hypot(x - centerX, y - centerY); - - if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness - && distanceFromCenter >= mTaskCornerRadius) { - return ctrlType; - } - return 0; - } - - @DragPositioningCallback.CtrlType - private int calculateCornersCtrlType(float x, float y) { - int xi = (int) x; - int yi = (int) y; - if (mLeftTopCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; - } - if (mLeftBottomCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; - } - if (mRightTopCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; - } - if (mRightBottomCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; - } - return 0; + mTouchRegionConsumer.accept(dragTouchRegion); } private void updateCursorType(int displayId, int deviceId, int pointerId, float x, float y) { - @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); + @DragPositioningCallback.CtrlType int ctrlType = + mDragResizeWindowGeometry.calculateCtrlType(/* isTouch= */ false, x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -638,5 +508,13 @@ class DragResizeInputListener implements AutoCloseable { mLastCursorType = cursorType; } } + + private boolean shouldHandleEvent(MotionEvent e, Point offset) { + return mDragResizeWindowGeometry.shouldHandleEvent(e, offset); + } + + private boolean isTouchEvent(MotionEvent e) { + return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java new file mode 100644 index 000000000000..eafb56995db7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; + +import static com.android.window.flags.Flags.enableWindowingEdgeDragResize; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; + +import android.annotation.NonNull; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.Size; +import android.view.MotionEvent; + +import com.android.wm.shell.R; + +import java.util.Objects; + +/** + * Geometry for a drag resize region for a particular window. + */ +final class DragResizeWindowGeometry { + private final int mTaskCornerRadius; + private final Size mTaskSize; + // The size of the handle applied to the edges of the window, for the user to drag resize. + private final int mResizeHandleThickness; + // The task corners to permit drag resizing with a course input, such as touch. + + private final @NonNull TaskCorners mLargeTaskCorners; + // The task corners to permit drag resizing with a fine input, such as stylus or cursor. + private final @NonNull TaskCorners mFineTaskCorners; + // The bounds for each edge drag region, which can resize the task in one direction. + private final @NonNull Rect mTopEdgeBounds; + private final @NonNull Rect mLeftEdgeBounds; + private final @NonNull Rect mRightEdgeBounds; + private final @NonNull Rect mBottomEdgeBounds; + + DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, + int resizeHandleThickness, int fineCornerSize, int largeCornerSize) { + mTaskCornerRadius = taskCornerRadius; + mTaskSize = taskSize; + mResizeHandleThickness = resizeHandleThickness; + + mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize); + mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize); + + // Save touch areas for each edge. + mTopEdgeBounds = new Rect( + -mResizeHandleThickness, + -mResizeHandleThickness, + mTaskSize.getWidth() + mResizeHandleThickness, + 0); + mLeftEdgeBounds = new Rect( + -mResizeHandleThickness, + 0, + 0, + mTaskSize.getHeight()); + mRightEdgeBounds = new Rect( + mTaskSize.getWidth(), + 0, + mTaskSize.getWidth() + mResizeHandleThickness, + mTaskSize.getHeight()); + mBottomEdgeBounds = new Rect( + -mResizeHandleThickness, + mTaskSize.getHeight(), + mTaskSize.getWidth() + mResizeHandleThickness, + mTaskSize.getHeight() + mResizeHandleThickness); + } + + /** + * Returns the resource value to use for the resize handle on the edge of the window. + */ + static int getResizeEdgeHandleSize(@NonNull Resources res) { + return enableWindowingEdgeDragResize() + ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle) + : res.getDimensionPixelSize(R.dimen.freeform_resize_handle); + } + + /** + * Returns the resource value to use for course input, such as touch, that benefits from a large + * square on each of the window's corners. + */ + static int getLargeResizeCornerSize(@NonNull Resources res) { + return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large); + } + + /** + * Returns the resource value to use for fine input, such as stylus, that can use a smaller + * square on each of the window's corners. + */ + static int getFineResizeCornerSize(@NonNull Resources res) { + return res.getDimensionPixelSize(R.dimen.freeform_resize_corner); + } + + /** + * Returns the size of the task this geometry is calculated for. + */ + @NonNull Size getTaskSize() { + // Safe to return directly since size is immutable. + return mTaskSize; + } + + /** + * Returns the union of all regions that can be touched for drag resizing; the corners window + * and window edges. + */ + void union(@NonNull Region region) { + // Apply the edge resize regions. + region.union(mTopEdgeBounds); + region.union(mLeftEdgeBounds); + region.union(mRightEdgeBounds); + region.union(mBottomEdgeBounds); + + if (enableWindowingEdgeDragResize()) { + // Apply the corners as well for the larger corners, to ensure we capture all possible + // touches. + mLargeTaskCorners.union(region); + } else { + // Only apply fine corners for the legacy approach. + mFineTaskCorners.union(region); + } + } + + /** + * Returns if this MotionEvent should be handled, based on its source and position. + */ + boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return shouldHandleEvent(e, isTouchEvent(e), offset); + } + + /** + * Returns if this MotionEvent should be handled, based on its source and position. + */ + boolean shouldHandleEvent(@NonNull MotionEvent e, boolean isTouch, @NonNull Point offset) { + final float x = e.getX(0) + offset.x; + final float y = e.getY(0) + offset.y; + + if (enableWindowingEdgeDragResize()) { + // First check if touch falls within a corner. + // Large corner bounds are used for course input like touch, otherwise fine bounds. + boolean result = isTouch + ? isInCornerBounds(mLargeTaskCorners, x, y) + : isInCornerBounds(mFineTaskCorners, x, y); + // Check if touch falls within the edge resize handle, since edge resizing can apply + // for any input source. + if (!result) { + result = isInEdgeResizeBounds(x, y); + } + return result; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isTouch + ? isInCornerBounds(mFineTaskCorners, x, y) + : isInEdgeResizeBounds(x, y); + } + } + + private boolean isTouchEvent(@NonNull MotionEvent e) { + return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + } + + private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) { + return corners.calculateCornersCtrlType(xf, yf) != 0; + } + + private boolean isInEdgeResizeBounds(float x, float y) { + return calculateEdgeResizeCtrlType(x, y) != 0; + } + + /** + * Returns the control type for the drag-resize, based on the touch regions and this + * MotionEvent's coordinates. + */ + @DragPositioningCallback.CtrlType + int calculateCtrlType(boolean isTouch, float x, float y) { + if (enableWindowingEdgeDragResize()) { + // First check if touch falls within a corner. + // Large corner bounds are used for course input like touch, otherwise fine bounds. + int ctrlType = isTouch + ? mLargeTaskCorners.calculateCornersCtrlType(x, y) + : mFineTaskCorners.calculateCornersCtrlType(x, y); + // Check if touch falls within the edge resize handle, since edge resizing can apply + // for any input source. + if (ctrlType == CTRL_TYPE_UNDEFINED) { + ctrlType = calculateEdgeResizeCtrlType(x, y); + } + return ctrlType; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isTouch + ? mFineTaskCorners.calculateCornersCtrlType(x, y) + : calculateEdgeResizeCtrlType(x, y); + } + } + + @DragPositioningCallback.CtrlType + private int calculateEdgeResizeCtrlType(float x, float y) { + int ctrlType = CTRL_TYPE_UNDEFINED; + // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with + // sides will use the bounds specified in setGeometry and not go into task bounds. + if (x < mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_LEFT; + } + if (x > mTaskSize.getWidth() - mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_RIGHT; + } + if (y < mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_TOP; + } + if (y > mTaskSize.getHeight() - mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_BOTTOM; + } + // If the touch is within one of the four corners, check if it is within the bounds of the + // // handle. + if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 + && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { + return checkDistanceFromCenter(ctrlType, x, y); + } + // Otherwise, we should make sure we don't resize tasks inside task bounds. + return (x < 0 || y < 0 || x >= mTaskSize.getWidth() || y >= mTaskSize.getHeight()) + ? ctrlType : CTRL_TYPE_UNDEFINED; + } + + /** + * Return {@code ctrlType} if the corner input is outside the (potentially rounded) corner of + * the task, and within the thickness of the resize handle. Otherwise, return 0. + */ + @DragPositioningCallback.CtrlType + private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x, + float y) { + final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType); + double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y); + + if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness + && distanceFromCenter >= mTaskCornerRadius) { + return ctrlType; + } + return CTRL_TYPE_UNDEFINED; + } + + /** + * Returns center of rounded corner circle; this is simply the corner if radius is 0. + */ + private Point calculateCenterForCornerRadius(@DragPositioningCallback.CtrlType int ctrlType) { + int centerX; + int centerY; + + switch (ctrlType) { + case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: { + centerX = mTaskCornerRadius; + centerY = mTaskCornerRadius; + break; + } + case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: { + centerX = mTaskCornerRadius; + centerY = mTaskSize.getHeight() - mTaskCornerRadius; + break; + } + case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: { + centerX = mTaskSize.getWidth() - mTaskCornerRadius; + centerY = mTaskCornerRadius; + break; + } + case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: { + centerX = mTaskSize.getWidth() - mTaskCornerRadius; + centerY = mTaskSize.getHeight() - mTaskCornerRadius; + break; + } + default: { + throw new IllegalArgumentException( + "ctrlType should be complex, but it's 0x" + Integer.toHexString(ctrlType)); + } + } + return new Point(centerX, centerY); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof DragResizeWindowGeometry other)) return false; + + return this.mTaskCornerRadius == other.mTaskCornerRadius + && this.mTaskSize.equals(other.mTaskSize) + && this.mResizeHandleThickness == other.mResizeHandleThickness + && this.mFineTaskCorners.equals(other.mFineTaskCorners) + && this.mLargeTaskCorners.equals(other.mLargeTaskCorners) + && this.mTopEdgeBounds.equals(other.mTopEdgeBounds) + && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds) + && this.mRightEdgeBounds.equals(other.mRightEdgeBounds) + && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds); + } + + @Override + public int hashCode() { + return Objects.hash( + mTaskCornerRadius, + mTaskSize, + mResizeHandleThickness, + mFineTaskCorners, + mLargeTaskCorners, + mTopEdgeBounds, + mLeftEdgeBounds, + mRightEdgeBounds, + mBottomEdgeBounds); + } + + /** + * Representation of the drag resize regions at the corner of the window. + */ + private static class TaskCorners { + // The size of the square applied to the corners of the window, for the user to drag + // resize. + private final int mCornerSize; + // The square for each corner. + private final @NonNull Rect mLeftTopCornerBounds; + private final @NonNull Rect mRightTopCornerBounds; + private final @NonNull Rect mLeftBottomCornerBounds; + private final @NonNull Rect mRightBottomCornerBounds; + + TaskCorners(@NonNull Size taskSize, int cornerSize) { + mCornerSize = cornerSize; + final int cornerRadius = cornerSize / 2; + mLeftTopCornerBounds = new Rect( + -cornerRadius, + -cornerRadius, + cornerRadius, + cornerRadius); + + mRightTopCornerBounds = new Rect( + taskSize.getWidth() - cornerRadius, + -cornerRadius, + taskSize.getWidth() + cornerRadius, + cornerRadius); + + mLeftBottomCornerBounds = new Rect( + -cornerRadius, + taskSize.getHeight() - cornerRadius, + cornerRadius, + taskSize.getHeight() + cornerRadius); + + mRightBottomCornerBounds = new Rect( + taskSize.getWidth() - cornerRadius, + taskSize.getHeight() - cornerRadius, + taskSize.getWidth() + cornerRadius, + taskSize.getHeight() + cornerRadius); + } + + /** + * Updates the region to include all four corners. + */ + void union(Region region) { + region.union(mLeftTopCornerBounds); + region.union(mRightTopCornerBounds); + region.union(mLeftBottomCornerBounds); + region.union(mRightBottomCornerBounds); + } + + /** + * Returns the control type based on the position of the {@code MotionEvent}'s coordinates. + */ + @DragPositioningCallback.CtrlType + int calculateCornersCtrlType(float x, float y) { + int xi = (int) x; + int yi = (int) y; + if (mLeftTopCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; + } + if (mLeftBottomCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; + } + if (mRightTopCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; + } + if (mRightBottomCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; + } + return 0; + } + + @Override + public String toString() { + return "TaskCorners of size " + mCornerSize + " for the" + + " top left " + mLeftTopCornerBounds + + " top right " + mRightTopCornerBounds + + " bottom left " + mLeftBottomCornerBounds + + " bottom right " + mRightBottomCornerBounds; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof TaskCorners other)) return false; + + return this.mCornerSize == other.mCornerSize + && this.mLeftTopCornerBounds.equals(other.mLeftTopCornerBounds) + && this.mRightTopCornerBounds.equals(other.mRightTopCornerBounds) + && this.mLeftBottomCornerBounds.equals(other.mLeftBottomCornerBounds) + && this.mRightBottomCornerBounds.equals(other.mRightBottomCornerBounds); + } + + @Override + public int hashCode() { + return Objects.hash( + mCornerSize, + mLeftTopCornerBounds, + mRightTopCornerBounds, + mLeftBottomCornerBounds, + mRightBottomCornerBounds); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 6bfc7cdcb33e..76096b0c59f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -60,9 +61,6 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); - // If a task move (not resize) finishes with the positions y less than this value, do not - // finalize the bounds there using WCT#setBounds - private final int mDisallowedAreaForEndBoundsHeight; private boolean mHasDragResized; private boolean mIsResizingOrAnimatingResize; private int mCtrlType; @@ -70,11 +68,9 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, @Surface.Rotation private int mRotation; FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions, - WindowDecoration windowDecoration, DisplayController displayController, - int disallowedAreaForEndBoundsHeight) { + WindowDecoration windowDecoration, DisplayController displayController) { this(taskOrganizer, transitions, windowDecoration, displayController, - dragStartListener -> {}, SurfaceControl.Transaction::new, - disallowedAreaForEndBoundsHeight); + dragStartListener -> {}, SurfaceControl.Transaction::new); } FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, @@ -82,15 +78,13 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, WindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier, - int disallowedAreaForEndBoundsHeight) { + Supplier<SurfaceControl.Transaction> supplier) { mTaskOrganizer = taskOrganizer; mTransitions = transitions; mWindowDecoration = windowDecoration; mDisplayController = displayController; mDragStartListener = dragStartListener; mTransactionSupplier = supplier; - mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight; } @Override @@ -157,14 +151,10 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); } mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); - } else if (mCtrlType == CTRL_TYPE_UNDEFINED - && DragPositioningCallbackUtility.isBelowDisallowedArea( - mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint, - y)) { + } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds, - mTaskBoundsAtDragStart, mRepositionStartPoint, x, y, - mWindowDecoration.calculateValidDragArea()); + DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, + mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } @@ -189,10 +179,11 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt new file mode 100644 index 000000000000..b21c3f522eab --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor + +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageButton + +/** + * [ImageButton] for the handle at the top of fullscreen apps. Has custom hover + * and press handling to grow the handle on hover enter and shrink the handle on + * hover exit and press. + */ +class HandleImageButton (context: Context?, attrs: AttributeSet?) : + ImageButton(context, attrs) { + private val handleAnimator = ValueAnimator() + + override fun onHoverChanged(hovered: Boolean) { + super.onHoverChanged(hovered) + if (hovered) { + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_SCALE) + } else { + if (!isPressed) { + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + } + } + } + + override fun setPressed(pressed: Boolean) { + if (isPressed != pressed) { + super.setPressed(pressed) + if (pressed) { + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_SCALE) + } else { + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + } + } + } + + private fun animateHandle(duration: Long, endScale: Float) { + if (handleAnimator.isRunning) { + handleAnimator.cancel() + } + handleAnimator.duration = duration + handleAnimator.setFloatValues(scaleX, endScale) + handleAnimator.addUpdateListener { animator -> + scaleX = animator.animatedValue as Float + } + handleAnimator.start() + } + + companion object { + /** The duration of animations related to hover state. **/ + private const val HANDLE_HOVER_ANIM_DURATION = 300L + /** The duration of animations related to pressed state. **/ + private const val HANDLE_PRESS_ANIM_DURATION = 200L + /** Ending scale for hover enter. **/ + private const val HANDLE_HOVER_ENTER_SCALE = 1.2f + /** Ending scale for press down. **/ + private const val HANDLE_PRESS_DOWN_SCALE = 0.85f + /** Default scale for handle. **/ + private const val HANDLE_DEFAULT_SCALE = 1f + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index b37dd0d6fd2d..65adcee1567c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -20,6 +20,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; import android.annotation.NonNull; import android.annotation.Nullable; @@ -35,7 +37,6 @@ import android.graphics.PointF; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; -import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -53,6 +54,7 @@ import com.android.wm.shell.R; */ class HandleMenu { private static final String TAG = "HandleMenu"; + private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false; private final Context mContext; private final WindowDecoration mParentDecor; private WindowDecoration.AdditionalWindow mHandleMenuWindow; @@ -140,7 +142,8 @@ class HandleMenu { * Set up interactive elements of handle menu's app info pill. */ private void setupAppInfoPill(View handleMenu) { - final ImageButton collapseBtn = handleMenu.findViewById(R.id.collapse_menu_button); + final HandleMenuImageButton collapseBtn = + handleMenu.findViewById(R.id.collapse_menu_button); final ImageView appIcon = handleMenu.findViewById(R.id.application_icon); final TextView appName = handleMenu.findViewById(R.id.application_name); collapseBtn.setOnClickListener(mOnClickListener); @@ -172,9 +175,9 @@ class HandleMenu { final ColorStateList activeColorStateList = iconColors[1]; final int windowingMode = mTaskInfo.getWindowingMode(); fullscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_FULLSCREEN - ? activeColorStateList : inActiveColorStateList); + ? activeColorStateList : inActiveColorStateList); splitscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_MULTI_WINDOW - ? activeColorStateList : inActiveColorStateList); + ? activeColorStateList : inActiveColorStateList); floatingBtn.setImageTintList(windowingMode == WINDOWING_MODE_PINNED ? activeColorStateList : inActiveColorStateList); desktopBtn.setImageTintList(windowingMode == WINDOWING_MODE_FREEFORM @@ -185,11 +188,9 @@ class HandleMenu { * Set up interactive elements & height of handle menu's more actions pill */ private void setupMoreActionsPill(View handleMenu) { - final Button selectBtn = handleMenu.findViewById(R.id.select_button); - selectBtn.setOnClickListener(mOnClickListener); - final Button screenshotBtn = handleMenu.findViewById(R.id.screenshot_button); - // TODO: Remove once implemented. - screenshotBtn.setVisibility(View.GONE); + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE); + } } /** @@ -245,24 +246,31 @@ class HandleMenu { } /** - * Check a passed MotionEvent if a click has occurred on any button on this caption - * Note this should only be called when a regular onClick is not possible + * Check a passed MotionEvent if a click or hover has occurred on any button on this caption + * Note this should only be called when a regular onClick/onHover is not possible * (i.e. the button was clicked through status bar layer) * * @param ev the MotionEvent to compare against. */ - void checkClickEvent(MotionEvent ev) { + void checkMotionEvent(MotionEvent ev) { final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView(); - final ImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); - // Translate the input point from display coordinates to the same space as the collapse - // button, meaning its parent (app info pill view). - final PointF inputPoint = new PointF(ev.getX() - mHandleMenuPosition.x, - ev.getY() - mHandleMenuPosition.y); - if (pointInView(collapse, inputPoint.x, inputPoint.y)) { - mOnClickListener.onClick(collapse); + final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); + final PointF inputPoint = translateInputToLocalSpace(ev); + final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y); + final int action = ev.getActionMasked(); + collapse.setHovered(inputInCollapseButton && action != ACTION_UP); + collapse.setPressed(inputInCollapseButton && action == ACTION_DOWN); + if (action == ACTION_UP && inputInCollapseButton) { + collapse.performClick(); } } + // Translate the input point from display coordinates to the same space as the handle menu. + private PointF translateInputToLocalSpace(MotionEvent ev) { + return new PointF(ev.getX() - mHandleMenuPosition.x, + ev.getY() - mHandleMenuPosition.y); + } + /** * A valid menu input is one of the following: * An input that happens in the menu views. @@ -305,12 +313,15 @@ class HandleMenu { * Determines handle menu height based on if windowing pill should be shown. */ private int getHandleMenuHeight(Resources resources) { - int menuHeight = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_height); + int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height); if (!mShouldShowWindowingPill) { menuHeight -= loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_windowing_pill_height); } + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + menuHeight -= loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_more_actions_pill_height); + } return menuHeight; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt new file mode 100644 index 000000000000..7898567b70e9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.ImageButton + +/** + * A custom [ImageButton] for buttons inside handle menu that intentionally doesn't handle hovers. + * This is due to the hover events being handled by [DesktopModeWindowDecorViewModel] + * in order to take the status bar layer into account. Handling it in both classes results in a + * flicker when the hover moves from outside to inside status bar layer. + */ +class HandleMenuImageButton(context: Context?, attrs: AttributeSet?) : + ImageButton(context, attrs) { + override fun onHoverEvent(motionEvent: MotionEvent): Boolean { + return false + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index b82f7ca47ef3..899b7cc0ea0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -33,7 +33,11 @@ import android.view.View.OnTouchListener import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.Button +import android.widget.FrameLayout +import android.widget.LinearLayout import android.window.TaskConstants +import androidx.core.content.withStyledAttributes +import com.android.internal.R.attr.colorAccentPrimary import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController @@ -70,6 +74,12 @@ class MaximizeMenu( private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width) private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) + private lateinit var snapRightButton: Button + private lateinit var snapLeftButton: Button + private lateinit var maximizeButton: Button + private lateinit var maximizeButtonLayout: FrameLayout + private lateinit var snapButtonsLayout: LinearLayout + /** Position the menu relative to the caption's position. */ fun positionMenu(position: PointF, t: Transaction) { menuPosition.set(position) @@ -150,23 +160,23 @@ class MaximizeMenu( maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener) maximizeMenuView.setOnTouchListener(onTouchListener) - val maximizeButton = maximizeMenuView.requireViewById<Button>( - R.id.maximize_menu_maximize_button - ) + maximizeButtonLayout = maximizeMenuView.requireViewById( + R.id.maximize_menu_maximize_button_layout) + + maximizeButton = maximizeMenuView.requireViewById(R.id.maximize_menu_maximize_button) maximizeButton.setOnClickListener(onClickListener) maximizeButton.setOnGenericMotionListener(onGenericMotionListener) - val snapRightButton = maximizeMenuView.requireViewById<Button>( - R.id.maximize_menu_snap_right_button - ) + snapRightButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_right_button) snapRightButton.setOnClickListener(onClickListener) snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - val snapLeftButton = maximizeMenuView.requireViewById<Button>( - R.id.maximize_menu_snap_left_button - ) + snapLeftButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_left_button) snapLeftButton.setOnClickListener(onClickListener) snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) + + snapButtonsLayout = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_menu_layout) + snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) } /** @@ -190,11 +200,77 @@ class MaximizeMenu( return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false } + fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { + setSnapButtonsColorOnHover(viewId, ev) + } + + fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { + setSnapButtonsColorOnHover(viewId, ev) + } + + fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { + val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapButtonsLayout.width && + ev.y >= 0 && ev.y <= snapButtonsLayout.height + val colorList = decorWindowContext.getColorStateList( + R.color.desktop_mode_maximize_menu_button_color_selector) + + if (id == R.id.maximize_menu_maximize_button) { + maximizeButton.background?.setTintList(colorList) + maximizeButtonLayout.setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background) + } else if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { + // After exiting the snap menu layout area, checks to see that user is not still + // hovering within the snap menu layout bounds which would indicate that the user is + // hovering over a snap button within the snap menu layout rather than having exited. + snapLeftButton.background?.setTintList(colorList) + snapLeftButton.background?.alpha = 255 + snapRightButton.background?.setTintList(colorList) + snapRightButton.background?.alpha = 255 + snapButtonsLayout.setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background) + } + } + + private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { + decorWindowContext.withStyledAttributes(null, intArrayOf(colorAccentPrimary), 0, 0) { + val materialColor = getColor(0, 0) + val snapMenuCenter = snapButtonsLayout.width / 2 + if (viewId == R.id.maximize_menu_maximize_button) { + // Highlight snap maximize window button + maximizeButton.background?.setTint(materialColor) + maximizeButtonLayout.setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) + } else if (viewId == R.id.maximize_menu_snap_left_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter)) { + // Highlight snap left button + snapRightButton.background?.setTint(materialColor) + snapLeftButton.background?.setTint(materialColor) + snapButtonsLayout.setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) + snapRightButton.background?.alpha = 102 + snapLeftButton.background?.alpha = 255 + } else if (viewId == R.id.maximize_menu_snap_right_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter)) { + // Highlight snap right button + snapRightButton.background?.setTint(materialColor) + snapLeftButton.background?.setTint(materialColor) + snapButtonsLayout.setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) + snapRightButton.background?.alpha = 255 + snapLeftButton.background?.alpha = 102 + } + } + } + companion object { fun isMaximizeMenuView(@IdRes viewId: Int): Boolean { - return viewId == R.id.maximize_menu || viewId == R.id.maximize_menu_maximize_button || + return viewId == R.id.maximize_menu || + viewId == R.id.maximize_menu_maximize_button || + viewId == R.id.maximize_menu_maximize_button_layout || viewId == R.id.maximize_menu_snap_left_button || - viewId == R.id.maximize_menu_snap_right_button + viewId == R.id.maximize_menu_snap_right_button || + viewId == R.id.maximize_menu_snap_menu_layout || + viewId == R.id.maximize_menu_snap_menu_layout } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index af055230b629..74499c7e429e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -31,17 +31,23 @@ class MoveToDesktopAnimator @JvmOverloads constructor( private val animatedTaskWidth get() = dragToDesktopAnimator.animatedValue as Float * startBounds.width() + val scale: Float + get() = dragToDesktopAnimator.animatedValue as Float + private val mostRecentInput = PointF() private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE) .setDuration(ANIMATION_DURATION.toLong()) .apply { val t = SurfaceControl.Transaction() val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) - addUpdateListener { animation -> - val animatorValue = animation.animatedValue as Float - t.setScale(taskSurface, animatorValue, animatorValue) - .setCornerRadius(taskSurface, cornerRadius) - .apply() + addUpdateListener { + setTaskPosition(mostRecentInput.x, mostRecentInput.y) + t.setScale(taskSurface, scale, scale) + .setCornerRadius(taskSurface, cornerRadius) + .setScale(taskSurface, scale, scale) + .setCornerRadius(taskSurface, cornerRadius) + .setPosition(taskSurface, position.x, position.y) + .apply() } } @@ -77,22 +83,31 @@ class MoveToDesktopAnimator @JvmOverloads constructor( // allow dragging beyond its stage across any region of the display. Because of that, the // rawX/Y are more true to where the gesture is on screen and where the surface should be // positioned. - position.x = ev.rawX - animatedTaskWidth / 2 - position.y = ev.rawY + mostRecentInput.set(ev.rawX, ev.rawY) - if (!allowSurfaceChangesOnMove) { + // If animator is running, allow it to set scale and position at the same time. + if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) { return } - + setTaskPosition(ev.rawX, ev.rawY) val t = transactionFactory() t.setPosition(taskSurface, position.x, position.y) t.apply() } /** - * Ends the animation, setting the scale and position to the final animation value + * Calculates the top left corner of task from input coordinates. + * Top left will be needed for the resulting surface control transaction. + */ + private fun setTaskPosition(x: Float, y: Float) { + position.x = x - animatedTaskWidth / 2 + position.y = y + } + + /** + * Cancels the animation, intended to be used when another animator will take over. */ - fun endAnimator() { - dragToDesktopAnimator.end() + fun cancelAnimator() { + dragToDesktopAnimator.cancel() } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index b0d3b5090ef0..2c4092ac6d2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -20,16 +20,20 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.ColorRes; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.Display; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -37,6 +41,7 @@ import android.widget.ImageView; import android.window.TaskConstants; import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; import java.util.function.Supplier; @@ -44,63 +49,145 @@ import java.util.function.Supplier; * Creates and updates a veil that covers task contents on resize. */ public class ResizeVeil { + private static final String TAG = "ResizeVeil"; private static final int RESIZE_ALPHA_DURATION = 100; + + private static final int VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL; + /** The background is a child of the veil container layer and goes at the bottom. */ + private static final int VEIL_BACKGROUND_LAYER = 0; + /** The icon is a child of the veil container layer and goes in front of the background. */ + private static final int VEIL_ICON_LAYER = 1; + private final Context mContext; - private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; + private final DisplayController mDisplayController; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; + private final SurfaceControlBuilderFactory mSurfaceControlBuilderFactory; + private final WindowDecoration.SurfaceControlViewHostFactory mSurfaceControlViewHostFactory; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); private final Drawable mAppIcon; private ImageView mIconView; + private int mIconSize; private SurfaceControl mParentSurface; + + /** A container surface to host the veil background and icon child surfaces. */ private SurfaceControl mVeilSurface; + /** A color surface for the veil background. */ + private SurfaceControl mBackgroundSurface; + /** A surface that hosts a windowless window with the app icon. */ + private SurfaceControl mIconSurface; + private final RunningTaskInfo mTaskInfo; private SurfaceControlViewHost mViewHost; - private final Display mDisplay; + private Display mDisplay; private ValueAnimator mVeilAnimator; - public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo, - Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display, + private boolean mIsShowing = false; + + private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayAdded(int displayId) { + if (mTaskInfo.displayId != displayId) { + return; + } + mDisplayController.removeDisplayWindowListener(this); + setupResizeVeil(); + } + }; + + public ResizeVeil(Context context, + @NonNull DisplayController displayController, + Drawable appIcon, RunningTaskInfo taskInfo, + SurfaceControl taskSurface, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { + this(context, + displayController, + appIcon, + taskInfo, + taskSurface, + surfaceControlTransactionSupplier, + new SurfaceControlBuilderFactory() {}, + new WindowDecoration.SurfaceControlViewHostFactory() {}); + } + + public ResizeVeil(Context context, + @NonNull DisplayController displayController, + Drawable appIcon, RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + SurfaceControlBuilderFactory surfaceControlBuilderFactory, + WindowDecoration.SurfaceControlViewHostFactory surfaceControlViewHostFactory) { mContext = context; + mDisplayController = displayController; mAppIcon = appIcon; - mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mTaskInfo = taskInfo; - mDisplay = display; + mParentSurface = taskSurface; + mSurfaceControlBuilderFactory = surfaceControlBuilderFactory; + mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; setupResizeVeil(); } - /** * Create the veil in its default invisible state. */ private void setupResizeVeil() { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); - mVeilSurface = builder - .setName("Resize veil of Task= " + mTaskInfo.taskId) + if (!obtainDisplayOrRegisterListener()) { + // Display may not be available yet, skip this until then. + return; + } + mVeilSurface = mSurfaceControlBuilderFactory + .create("Resize veil of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(mParentSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build(); + mBackgroundSurface = mSurfaceControlBuilderFactory + .create("Resize veil background of Task=" + mTaskInfo.taskId, mSurfaceSession) + .setColorLayer() + .setHidden(true) + .setParent(mVeilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build(); + mIconSurface = mSurfaceControlBuilderFactory + .create("Resize veil icon of Task=" + mTaskInfo.taskId) .setContainerLayer() + .setHidden(true) + .setParent(mVeilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") .build(); - View v = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null); - t.setPosition(mVeilSurface, 0, 0) - .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) - .apply(); - Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + mIconSize = mContext.getResources() + .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size); + final View root = LayoutInflater.from(mContext) + .inflate(R.layout.desktop_mode_resize_veil, null /* root */); + mIconView = root.findViewById(R.id.veil_application_icon); + mIconView.setImageDrawable(mAppIcon); + final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(taskBounds.width(), - taskBounds.height(), + new WindowManager.LayoutParams( + mIconSize, + mIconSize, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId); + lp.setTitle("Resize veil icon window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); - WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, - mVeilSurface, null /* hostInputToken */); - mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil"); - mViewHost.setView(v, lp); - mIconView = mViewHost.getView().findViewById(R.id.veil_application_icon); - mIconView.setImageDrawable(mAppIcon); + final WindowlessWindowManager wwm = new WindowlessWindowManager(mTaskInfo.configuration, + mIconSurface, null /* hostInputToken */); + + mViewHost = mSurfaceControlViewHostFactory.create(mContext, mDisplay, wwm, "ResizeVeil"); + mViewHost.setView(root, lp); + } + + private boolean obtainDisplayOrRegisterListener() { + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + if (mDisplay == null) { + mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); + return false; + } + return true; } /** @@ -114,58 +201,95 @@ public class ResizeVeil { */ public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface, Rect taskBounds, boolean fadeIn) { + if (!isReady() || isVisible()) { + t.apply(); + return; + } + mIsShowing = true; + // Parent surface can change, ensure it is up to date. if (!parentSurface.equals(mParentSurface)) { t.reparent(mVeilSurface, parentSurface); mParentSurface = parentSurface; } - int backgroundColorId = getBackgroundColorId(); - mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId)); + t.show(mVeilSurface); + t.setLayer(mVeilSurface, VEIL_CONTAINER_LAYER); + t.setLayer(mIconSurface, VEIL_ICON_LAYER); + t.setLayer(mBackgroundSurface, VEIL_BACKGROUND_LAYER); + t.setColor(mBackgroundSurface, + Color.valueOf(mContext.getColor(getBackgroundColorId())).getComponents()); relayout(taskBounds, t); if (fadeIn) { cancelAnimation(); + final SurfaceControl.Transaction veilAnimT = mSurfaceControlTransactionSupplier.get(); mVeilAnimator = new ValueAnimator(); mVeilAnimator.setFloatValues(0f, 1f); mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { - t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction()); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, mVeilAnimator.getAnimatedFraction()); + veilAnimT.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + veilAnimT.show(mBackgroundSurface) + .setAlpha(mBackgroundSurface, 0) + .apply(); + } + + @Override public void onAnimationEnd(Animator animation) { - t.setAlpha(mVeilSurface, 1); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, 1).apply(); } }); + final SurfaceControl.Transaction iconAnimT = mSurfaceControlTransactionSupplier.get(); final ValueAnimator iconAnimator = new ValueAnimator(); iconAnimator.setFloatValues(0f, 1f); iconAnimator.setDuration(RESIZE_ALPHA_DURATION); iconAnimator.addUpdateListener(animation -> { - mIconView.setAlpha(animation.getAnimatedFraction()); + iconAnimT.setAlpha(mIconSurface, animation.getAnimatedFraction()); + iconAnimT.apply(); }); + iconAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + iconAnimT.show(mIconSurface) + .setAlpha(mIconSurface, 0) + .apply(); + } + + @Override + public void onAnimationEnd(Animator animation) { + iconAnimT.setAlpha(mIconSurface, 1).apply(); + } + }); + // Let the animators show it with the correct alpha value once the animation starts. + t.hide(mIconSurface); + t.hide(mBackgroundSurface); + t.apply(); - t.show(mVeilSurface) - .addTransactionCommittedListener( - mContext.getMainExecutor(), () -> { - mVeilAnimator.start(); - iconAnimator.start(); - }) - .setAlpha(mVeilSurface, 0); + mVeilAnimator.start(); + iconAnimator.start(); } else { - // Show the veil immediately at full opacity. - t.show(mVeilSurface).setAlpha(mVeilSurface, 1); + // Show the veil immediately. + t.show(mIconSurface); + t.show(mBackgroundSurface); + t.setAlpha(mIconSurface, 1); + t.setAlpha(mBackgroundSurface, 1); + t.apply(); } - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } /** * Animate veil's alpha to 1, fading it in. */ public void showVeil(SurfaceControl parentSurface, Rect taskBounds) { + if (!isReady() || isVisible()) { + return; + } SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); showVeil(t, parentSurface, taskBounds, true /* fadeIn */); } @@ -175,8 +299,9 @@ public class ResizeVeil { * @param newBounds bounds to update veil to. */ private void relayout(Rect newBounds, SurfaceControl.Transaction t) { - mViewHost.relayout(newBounds.width(), newBounds.height()); t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); + final PointF iconPosition = calculateAppIconPosition(newBounds); + t.setPosition(mIconSurface, iconPosition.x, iconPosition.y); t.setPosition(mParentSurface, newBounds.left, newBounds.top); t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); } @@ -186,6 +311,9 @@ public class ResizeVeil { * @param newBounds bounds to update veil to. */ public void updateResizeVeil(Rect newBounds) { + if (!isVisible()) { + return; + } SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); updateResizeVeil(t, newBounds); } @@ -199,36 +327,46 @@ public class ResizeVeil { * @param newBounds bounds to update veil to. */ public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) { + if (!isVisible()) { + t.apply(); + return; + } if (mVeilAnimator != null && mVeilAnimator.isStarted()) { mVeilAnimator.removeAllUpdateListeners(); mVeilAnimator.end(); } relayout(newBounds, t); - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); + t.apply(); } /** * Animate veil's alpha to 0, fading it out. */ public void hideVeil() { + if (!isVisible()) { + return; + } cancelAnimation(); mVeilAnimator = new ValueAnimator(); mVeilAnimator.setFloatValues(1, 0); mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mBackgroundSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mIconSurface, 1 - mVeilAnimator.getAnimatedFraction()); t.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.hide(mVeilSurface); + t.hide(mBackgroundSurface); + t.hide(mIconSurface); t.apply(); } }); mVeilAnimator.start(); + mIsShowing = false; } @ColorRes @@ -242,6 +380,11 @@ public class ResizeVeil { } } + private PointF calculateAppIconPosition(Rect parentBounds) { + return new PointF((float) parentBounds.width() / 2 - (float) mIconSize / 2, + (float) parentBounds.height() / 2 - (float) mIconSize / 2); + } + private void cancelAnimation() { if (mVeilAnimator != null) { mVeilAnimator.removeAllUpdateListeners(); @@ -250,21 +393,56 @@ public class ResizeVeil { } /** + * Whether the resize veil is currently visible. + * + * Note: when animating a {@link ResizeVeil#hideVeil()}, the veil is considered visible as soon + * as the animation starts. + */ + private boolean isVisible() { + return mIsShowing; + } + + /** Whether the resize veil is ready to be shown. */ + private boolean isReady() { + return mViewHost != null; + } + + /** * Dispose of veil when it is no longer needed, likely on close of its container decor. */ void dispose() { cancelAnimation(); + mIsShowing = false; mVeilAnimator = null; if (mViewHost != null) { mViewHost.release(); mViewHost = null; } + final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + if (mBackgroundSurface != null) { + t.remove(mBackgroundSurface); + mBackgroundSurface = null; + } + if (mIconSurface != null) { + t.remove(mIconSurface); + mIconSurface = null; + } if (mVeilSurface != null) { - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); t.remove(mVeilSurface); mVeilSurface = null; - t.apply(); + } + t.apply(); + mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); + } + + interface SurfaceControlBuilderFactory { + default SurfaceControl.Builder create(@NonNull String name) { + return new SurfaceControl.Builder().setName(name); + } + default SurfaceControl.Builder create(@NonNull String name, + @NonNull SurfaceSession surfaceSession) { + return new SurfaceControl.Builder(surfaceSession).setName(name); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index d0fcd8651481..53d4e2701849 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -72,7 +72,10 @@ class TaskOperations { } void closeTask(WindowContainerToken taskToken) { - WindowContainerTransaction wct = new WindowContainerTransaction(); + closeTask(taskToken, new WindowContainerTransaction()); + } + + void closeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) { wct.removeTask(taskToken); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitionStarter.startRemoveTransition(wct); @@ -91,14 +94,12 @@ class TaskOperations { } } - void maximizeTask(RunningTaskInfo taskInfo) { + void maximizeTask(RunningTaskInfo taskInfo, int containerWindowingMode) { WindowContainerTransaction wct = new WindowContainerTransaction(); int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM; - int displayWindowingMode = - taskInfo.configuration.windowConfiguration.getDisplayWindowingMode(); wct.setWindowingMode(taskInfo.token, - targetWindowingMode == displayWindowingMode + targetWindowingMode == containerWindowingMode ? WINDOWING_MODE_UNDEFINED : targetWindowingMode); if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { wct.setBounds(taskInfo.token, null); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 5c69d5542227..5fce5d228d71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -54,9 +55,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); - // If a task move (not resize) finishes with the positions y less than this value, do not - // finalize the bounds there using WCT#setBounds - private final int mDisallowedAreaForEndBoundsHeight; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; private int mCtrlType; private boolean mIsResizingOrAnimatingResize; @@ -66,25 +64,22 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Transitions transitions, - int disallowedAreaForEndBoundsHeight) { + Transitions transitions) { this(taskOrganizer, windowDecoration, displayController, dragStartListener, - SurfaceControl.Transaction::new, transitions, disallowedAreaForEndBoundsHeight); + SurfaceControl.Transaction::new, transitions); } public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier, Transitions transitions, - int disallowedAreaForEndBoundsHeight) { + Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) { mDesktopWindowDecoration = windowDecoration; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mDragStartListener = dragStartListener; mTransactionSupplier = supplier; mTransitions = transitions; - mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight; } @Override @@ -151,13 +146,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, // won't be called. resetVeilIfVisible(); } - } else if (DragPositioningCallbackUtility.isBelowDisallowedArea( - mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint, - y)) { + } else { final WindowContainerTransaction wct = new WindowContainerTransaction(); - DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds, - mTaskBoundsAtDragStart, mRepositionStartPoint, x, y, - mDesktopWindowDecoration.calculateValidDragArea()); + DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, + mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } @@ -188,10 +180,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 32c2d1e9b257..36da1ace8408 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -40,7 +40,6 @@ import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; -import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -293,60 +292,56 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER) .show(mCaptionContainerSurface); - if (ViewRootImpl.CAPTION_ON_SHELL) { - outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); - - // Caption insets - if (mIsCaptionVisible) { - // Caption inset is the full width of the task with the |captionHeight| and - // positioned at the top of the task bounds, also in absolute coordinates. - // So just reuse the task bounds and adjust the bottom coordinate. - mCaptionInsetsRect.set(taskBounds); - mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + outResult.mCaptionHeight; - - // Caption bounding rectangles: these are optional, and are used to present finer - // insets than traditional |Insets| to apps about where their content is occluded. - // These are also in absolute coordinates. - final Rect[] boundingRects; - final int numOfElements = params.mOccludingCaptionElements.size(); - if (numOfElements == 0) { - boundingRects = null; - } else { - // The customizable region can at most be equal to the caption bar. + outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); + + // Caption insets + if (mIsCaptionVisible) { + // Caption inset is the full width of the task with the |captionHeight| and + // positioned at the top of the task bounds, also in absolute coordinates. + // So just reuse the task bounds and adjust the bottom coordinate. + mCaptionInsetsRect.set(taskBounds); + mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + outResult.mCaptionHeight; + + // Caption bounding rectangles: these are optional, and are used to present finer + // insets than traditional |Insets| to apps about where their content is occluded. + // These are also in absolute coordinates. + final Rect[] boundingRects; + final int numOfElements = params.mOccludingCaptionElements.size(); + if (numOfElements == 0) { + boundingRects = null; + } else { + // The customizable region can at most be equal to the caption bar. + if (params.mAllowCaptionInputFallthrough) { + outResult.mCustomizableCaptionRegion.set(mCaptionInsetsRect); + } + boundingRects = new Rect[numOfElements]; + for (int i = 0; i < numOfElements; i++) { + final OccludingCaptionElement element = + params.mOccludingCaptionElements.get(i); + final int elementWidthPx = + resources.getDimensionPixelSize(element.mWidthResId); + boundingRects[i] = + calculateBoundingRect(element, elementWidthPx, mCaptionInsetsRect); + // Subtract the regions used by the caption elements, the rest is + // customizable. if (params.mAllowCaptionInputFallthrough) { - outResult.mCustomizableCaptionRegion.set(mCaptionInsetsRect); - } - boundingRects = new Rect[numOfElements]; - for (int i = 0; i < numOfElements; i++) { - final OccludingCaptionElement element = - params.mOccludingCaptionElements.get(i); - final int elementWidthPx = - resources.getDimensionPixelSize(element.mWidthResId); - boundingRects[i] = - calculateBoundingRect(element, elementWidthPx, mCaptionInsetsRect); - // Subtract the regions used by the caption elements, the rest is - // customizable. - if (params.mAllowCaptionInputFallthrough) { - outResult.mCustomizableCaptionRegion.op(boundingRects[i], - Region.Op.DIFFERENCE); - } + outResult.mCustomizableCaptionRegion.op(boundingRects[i], + Region.Op.DIFFERENCE); } } - // Add this caption as an inset source. - wct.addInsetsSource(mTaskInfo.token, - mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect, - boundingRects); - wct.addInsetsSource(mTaskInfo.token, - mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(), - mCaptionInsetsRect, null /* boundingRects */); - } else { - wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, - WindowInsets.Type.captionBar()); - wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, - WindowInsets.Type.mandatorySystemGestures()); } + // Add this caption as an inset source. + wct.addInsetsSource(mTaskInfo.token, + mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect, + boundingRects); + wct.addInsetsSource(mTaskInfo.token, + mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(), + mCaptionInsetsRect, null /* boundingRects */); } else { - startT.hide(mCaptionContainerSurface); + wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, + WindowInsets.Type.captionBar()); + wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, + WindowInsets.Type.mandatorySystemGestures()); } // Task surface itself @@ -594,8 +589,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> */ public void addCaptionInset(WindowContainerTransaction wct) { final int captionHeightId = getCaptionHeightId(mTaskInfo.getWindowingMode()); - if (!ViewRootImpl.CAPTION_ON_SHELL || captionHeightId == Resources.ID_NULL - || !mIsCaptionVisible) { + if (captionHeightId == Resources.ID_NULL || !mIsCaptionVisible) { return; } @@ -674,6 +668,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration"); } + default SurfaceControlViewHost create(Context c, Display d, + WindowlessWindowManager wmm, String callsite) { + return new SurfaceControlViewHost(c, d, wmm, callsite); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt index 5dd96aceaec7..a2293d53618a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt @@ -17,17 +17,25 @@ package com.android.wm.shell.windowdecor.extension import android.app.TaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND val TaskInfo.isTransparentCaptionBarAppearance: Boolean get() { - val appearance = taskDescription?.statusBarAppearance ?: 0 + val appearance = taskDescription?.systemBarsAppearance ?: 0 return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0 } val TaskInfo.isLightCaptionBarAppearance: Boolean get() { - val appearance = taskDescription?.statusBarAppearance ?: 0 + val appearance = taskDescription?.systemBarsAppearance ?: 0 return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 } + +val TaskInfo.isFullscreen: Boolean + get() = windowingMode == WINDOWING_MODE_FULLSCREEN + +val TaskInfo.isFreeform: Boolean + get() = windowingMode == WINDOWING_MODE_FREEFORM diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index 6dcae2776847..96bc4a146ebd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -65,7 +65,7 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 } else { - taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 + taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 } } ?: false } diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt index 3380adac0b3f..e9eabb4162e3 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt @@ -17,10 +17,10 @@ package com.android.wm.shell.flicker.appcompat import android.content.Context -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.FlickerTestData import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.helpers.LetterboxAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.wm.shell.flicker.BaseTest diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt index f08eba5a73a3..16c2d47f9db3 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt @@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.appcompat import android.platform.test.annotations.Postsubmit import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt index 826fc541687e..d85b7718aa56 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt @@ -19,11 +19,11 @@ package com.android.wm.shell.flicker.appcompat import android.platform.test.annotations.Postsubmit import android.tools.Rotation import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt index 26e78bf625ba..164534c14d28 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt @@ -16,17 +16,17 @@ package com.android.wm.shell.flicker.appcompat +import android.graphics.Rect import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice import android.tools.NavBar import android.tools.Rotation -import android.tools.datatypes.Rect import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -260,7 +260,7 @@ class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAp companion object { /** {@inheritDoc} */ - private var startDisplayBounds = Rect.EMPTY + private var startDisplayBounds = Rect() @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt index 2aa84b4e55b8..034d54b185ed 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt @@ -53,7 +53,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) class RepositionFixedPortraitAppTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) { - val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation).bounds + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt index 7ffa23345589..22543aa9f773 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt @@ -16,19 +16,19 @@ package com.android.wm.shell.flicker.appcompat +import android.graphics.Rect import android.os.Build import android.platform.test.annotations.Postsubmit import android.system.helpers.CommandsHelper import android.tools.NavBar import android.tools.Rotation -import android.tools.datatypes.Rect import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.FIND_TIMEOUT +import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.parsers.toFlickerComponent import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice @@ -167,7 +167,7 @@ class RotateImmersiveAppInFullscreenTest(flicker: LegacyFlickerTest) : BaseAppCo } companion object { - private var startDisplayBounds = Rect.EMPTY + private var startDisplayBounds = Rect() const val LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher" /** diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt index 521c0d0aaeb7..2a9b1078afe3 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt @@ -19,11 +19,11 @@ package com.android.wm.shell.flicker.bubble import android.content.Context import android.graphics.Point import android.platform.test.annotations.Presubmit -import android.tools.flicker.subject.layers.LayersTraceSubject -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.subject.layers.LayersTraceSubject +import android.tools.traces.component.ComponentNameMatcher import android.util.DisplayMetrics import android.view.WindowManager import androidx.test.uiautomator.By diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt index e059ac78dc6b..9ef49c1c9e7e 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt @@ -17,10 +17,10 @@ package com.android.wm.shell.flicker.bubble import android.platform.test.annotations.Postsubmit -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import android.view.WindowInsets import android.view.WindowManager import androidx.test.filters.FlakyTest diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index d64bfed382b9..b85d7936efc2 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -33,7 +33,7 @@ import org.junit.runners.Parameterized /** * Test entering pip from an app via auto-enter property when navigating to home. * - * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest` + * To run this test: `atest WMShellFlickerTestsPip1:AutoEnterPipOnGoToHomeTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt index a0edcfb17971..d059211088aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt @@ -17,10 +17,10 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized /** * Test auto entering pip using a source rect hint. * - * To run this test: `atest AutoEnterPipWithSourceRectHintTest` + * To run this test: `atest WMShellFlickerTestsPip1:AutoEnterPipWithSourceRectHintTest` * * Actions: * ``` @@ -66,9 +66,7 @@ class AutoEnterPipWithSourceRectHintTest(flicker: LegacyFlickerTest) : @Test fun pipOverlayNotShown() { val overlay = ComponentNameMatcher.PIP_CONTENT_OVERLAY - flicker.assertLayers { - this.notContains(overlay) - } + flicker.assertLayers { this.notContains(overlay) } } @Presubmit @Test @@ -83,4 +81,4 @@ class AutoEnterPipWithSourceRectHintTest(flicker: LegacyFlickerTest) : // auto enter and sourceRectHint that causes the app to move outside of the display // bounds during the transition. } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt index 031acf4919eb..a5e0550d9c79 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt @@ -17,10 +17,10 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import com.android.wm.shell.flicker.pip.common.ClosePipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test closing a pip window by swiping it to the bottom-center of the screen * - * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest` + * To run this test: `atest WMShellFlickerTestsPip1:ClosePipBySwipingDownTest` * * Actions: * ``` @@ -69,7 +69,8 @@ class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition wmHelper.currentState.layerState .getLayerWithBuffer(barComponent) ?.visibleRegion - ?.height + ?.bounds + ?.height() ?: error("Couldn't find Nav or Task bar layer") // The dismiss button doesn't appear at the complete bottom of the screen, // it appears above the hot seat but `hotseatBarSize` is not available outside diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt index 860307f2bb76..d177624378c1 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized /** * Test closing a pip window via the dismiss button * - * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest` + * To run this test: `atest WMShellFlickerTestsPip1:ClosePipWithDismissButtonTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index c5541613fece..a86803d058f8 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test entering pip from an app via [onUserLeaveHint] and by navigating to home. * - * To run this test: `atest WMShellFlickerTests:EnterPipOnUserLeaveHintTest` + * To run this test: `atest WMShellFlickerTestsPip2:EnterPipOnUserLeaveHintTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 9a1bd267ea1f..a0a61fe2cf72 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -21,12 +21,12 @@ import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper @@ -46,7 +46,7 @@ import org.junit.runners.Parameterized /** * Test entering pip while changing orientation (from app in landscape to pip window in portrait) * - * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest` + * To run this test: `atest WMShellFlickerTestsPip2:EnterPipToOtherOrientation` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt index f97d8d1842b0..d92f55af578f 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt @@ -28,7 +28,7 @@ import org.junit.runners.Parameterized /** * Test entering pip from an app by interacting with the app UI * - * To run this test: `atest WMShellFlickerTests:EnterPipTest` + * To run this test: `atest WMShellFlickerTestsPip2:EnterPipViaAppUiButtonTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt index 47bf41814d17..8c0817d6e287 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt @@ -28,7 +28,7 @@ import org.junit.runners.Parameterized /** * Test expanding a pip window back to full screen via the expand button * - * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest` + * To run this test: `atest WMShellFlickerTestsPip2:ExitPipToAppViaExpandButtonTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt index a356e68d14dd..90a9623056ce 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt @@ -28,7 +28,7 @@ import org.junit.runners.Parameterized /** * Test expanding a pip window back to full screen via an intent * - * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest` + * To run this test: `atest WMShellFlickerTestsPip2:ExitPipToAppViaIntentTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt index 25614ef63ccc..9306c77a1c43 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import com.android.wm.shell.flicker.pip.common.PipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -33,7 +33,7 @@ import org.junit.runners.Parameterized /** * Test expanding a pip window by double-clicking it * - * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest` + * To run this test: `atest WMShellFlickerTestsPip2:ExpandPipOnDoubleClickTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt index b94989d98e97..cb8ee27f29e2 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -38,7 +39,7 @@ import org.junit.runners.Parameterized /** * Test entering pip from an app via auto-enter property when navigating to home from split screen. * - * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest` + * To run this test: `atest WMShellFlickerTestsPip1:FromSplitScreenAutoEnterPipOnGoToHomeTest` * * Actions: * ``` @@ -143,6 +144,10 @@ class FromSplitScreenAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : } } + @FlakyTest(bugId = 293133362) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 1ccc7d8084a6..f2f10aef4fd7 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -39,7 +40,7 @@ import org.junit.runners.Parameterized /** * Test entering pip from an app via auto-enter property when navigating to home from split screen. * - * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest` + * To run this test: `atest WMShellFlickerTestsPip1:FromSplitScreenEnterPipOnUserLeaveHintTest` * * Actions: * ``` @@ -181,11 +182,18 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) - ) + fun getParams() = + LegacyFlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt index 9b746224a1a0..265eb4416a2b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt @@ -32,7 +32,7 @@ import org.junit.runners.Parameterized /** * Test Pip movement with Launcher shelf height change (increase). * - * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` + * To run this test: `atest WMShellFlickerTestsPip3:MovePipDownOnShelfHeightChange` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt index e184cf04e4ae..04fedf4f2550 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt @@ -19,12 +19,12 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.assertions.FlickerTest -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils +import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.wm.shell.flicker.pip.common.PipTransition @@ -34,7 +34,10 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized -/** Test Pip launch. To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */ +/** + * Test Pip launch. To run this test: + * `atest WMShellFlickerTestsPip3:MovePipOnImeVisibilityChangeTest` + */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt index 490ebd190ee8..8d6be64da21d 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt @@ -32,7 +32,7 @@ import org.junit.runners.Parameterized /** * Test Pip movement with Launcher shelf height change (decrease). * - * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * To run this test: `atest WMShellFlickerTestsPip3:MovePipUpOnShelfHeightChangeTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt index 68417066ac0a..16d08e5e9055 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt @@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.flicker.subject.exceptions.IncorrectRegionException import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.flicker.subject.exceptions.IncorrectRegionException import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.pip.common.PipTransition import org.junit.FixMethodOrder diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt index 9a6dacb187ef..ed2a0a718c6c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt @@ -41,8 +41,8 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test exiting Pip with orientation changes. To run this test: `atest - * WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest` + * Test exiting Pip with orientation changes. To run this test: + * `atest WMShellFlickerTestsPip1:SetRequestedOrientationWhilePinned` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt index d2f803ec9352..9109eafacf63 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test Pip Stack in bounds after rotations. * - * To run this test: `atest WMShellFlickerTests:PipRotationTest` + * To run this test: `atest WMShellFlickerTestsPip1:ShowPipAndRotateDisplay` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt index c9f4a6ca75b1..65b60ce1022b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt @@ -18,12 +18,12 @@ package com.android.wm.shell.flicker.pip.apps import android.platform.test.annotations.Postsubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.device.apphelpers.StandardAppHelper import android.tools.flicker.junit.FlickerBuilderProvider import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import com.android.wm.shell.flicker.pip.common.EnterPipTransition import org.junit.Test import org.junit.runners.Parameterized diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt index 88650107e63a..1fc9d9910a15 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt @@ -65,8 +65,8 @@ import org.junit.runners.Parameterized open class MapsEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) { override val standardAppHelper: MapsAppHelper = MapsAppHelper(instrumentation) - override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.ACCESS_FINE_LOCATION) + override val permissions: Array<String> = + arrayOf(Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.ACCESS_FINE_LOCATION) val locationManager: LocationManager = instrumentation.context.getSystemService(Context.LOCATION_SERVICE) as LocationManager diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt index 9b5153875987..3a0eeb67995b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt @@ -18,14 +18,14 @@ package com.android.wm.shell.flicker.pip.apps import android.Manifest import android.platform.test.annotations.Postsubmit -import android.tools.NavBar import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.device.apphelpers.NetflixAppHelper import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.helpers.WindowUtils +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.statusBarLayerPositionAtEnd import org.junit.Assume @@ -62,6 +62,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) { override val standardAppHelper: NetflixAppHelper = NetflixAppHelper(instrumentation) + private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) + private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS) @@ -134,6 +136,31 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit // Netflix plays in immersive fullscreen mode, so taskbar will be gone at some point } + @Postsubmit + @Test + override fun pipWindowRemainInsideVisibleBounds() { + // during the transition we assert the center point is within the display bounds, since it + // might go outside of bounds as we resize from landscape fullscreen to destination bounds, + // and once the animation is over we assert that it's fully within the display bounds, at + // which point the device also performs orientation change from landscape to portrait + flicker.assertWmVisibleRegion(standardAppHelper.packageNameMatcher) { + regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds) + } + } + + @Postsubmit + @Test + override fun pipLayerOrOverlayRemainInsideVisibleBounds() { + // during the transition we assert the center point is within the display bounds, since it + // might go outside of bounds as we resize from landscape fullscreen to destination bounds, + // and once the animation is over we assert that it's fully within the display bounds, at + // which point the device also performs orientation change from landscape to portrait + // since Netflix uses source rect hint, there is no PiP overlay present + flicker.assertLayersVisibleRegion(standardAppHelper.packageNameMatcher) { + regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds) + } + } + companion object { /** * Creates the test configurations. @@ -145,8 +172,7 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0), - supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + supportedRotations = listOf(Rotation.ROTATION_0) ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt index 3ae5937df4d0..35ed8de3a464 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt @@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip.apps import android.Manifest import android.platform.test.annotations.Postsubmit -import android.tools.traces.component.ComponentNameMatcher import android.tools.device.apphelpers.YouTubeAppHelper import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import org.junit.Assume import org.junit.FixMethodOrder diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt new file mode 100644 index 000000000000..879034f32514 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip.apps + +import android.Manifest +import android.platform.test.annotations.Postsubmit +import android.tools.Rotation +import android.tools.device.apphelpers.YouTubeAppHelper +import android.tools.flicker.junit.FlickerParametersRunnerFactory +import android.tools.flicker.legacy.FlickerBuilder +import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.helpers.WindowUtils +import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.statusBarLayerPositionAtEnd +import org.junit.Assume +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from YouTube app by interacting with the app UI + * + * To run this test: `atest WMShellFlickerTests:YouTubeEnterPipTest` + * + * Actions: + * ``` + * Launch YouTube and start playing a video + * Make the video fullscreen, aka immersive mode + * Go home to enter PiP + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.flicker.legacy.runner.TransitionRunner], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class YouTubeEnterPipToOtherOrientationTest(flicker: LegacyFlickerTest) : + YouTubeEnterPipTest(flicker) { + override val standardAppHelper: YouTubeAppHelper = YouTubeAppHelper(instrumentation) + private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) + private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) + + override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS) + + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + standardAppHelper.launchViaIntent( + wmHelper, + YouTubeAppHelper.getYoutubeVideoIntent("HPcEAtoXXLA"), + ComponentNameMatcher(YouTubeAppHelper.PACKAGE_NAME, "") + ) + standardAppHelper.enterFullscreen() + standardAppHelper.waitForVideoPlaying() + } + } + + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { tapl.goHomeFromImmersiveFullscreenApp() } + } + + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isTablet) + // YouTube starts in immersive fullscreen mode, so taskbar bar is not visible at start + flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.TASK_BAR) } + flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) } + } + + @Postsubmit + @Test + override fun pipWindowRemainInsideVisibleBounds() { + // during the transition we assert the center point is within the display bounds, since it + // might go outside of bounds as we resize from landscape fullscreen to destination bounds, + // and once the animation is over we assert that it's fully within the display bounds, at + // which point the device also performs orientation change from landscape to portrait + flicker.assertWmVisibleRegion(standardAppHelper.packageNameMatcher) { + regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds) + } + } + + @Postsubmit + @Test + override fun pipLayerOrOverlayRemainInsideVisibleBounds() { + // during the transition we assert the center point is within the display bounds, since it + // might go outside of bounds as we resize from landscape fullscreen to destination bounds, + // and once the animation is over we assert that it's fully within the display bounds, at + // which point the device also performs orientation change from landscape to portrait + // since YouTube uses source rect hint, there is no PiP overlay present + flicker.assertLayersVisibleRegion(standardAppHelper.packageNameMatcher) { + regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds) + } + } + + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() { + // YouTube plays in immersive fullscreen mode, so taskbar will be gone at some point + } + + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() { + // YouTube starts in immersive fullscreen mode, so status bar is not visible at start + flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.STATUS_BAR) } + flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) } + } + + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() { + // YouTube starts in immersive fullscreen mode, so status bar is not visible at start + flicker.statusBarLayerPositionAtEnd() + } + + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() { + // YouTube plays in immersive fullscreen mode, so taskbar will be gone at some point + } + + companion object { + /** + * Creates the test configurations. + * + * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen + * orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = + LegacyFlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt index dc122590388f..8cb81b46cf4d 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt @@ -18,10 +18,10 @@ package com.android.wm.shell.flicker.pip.common import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher.Companion.LAUNCHER import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher.Companion.LAUNCHER import com.android.server.wm.flicker.helpers.setRotation import org.junit.Test import org.junit.runners.Parameterized diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt index 3d9eae62b499..6dd3a175da65 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt @@ -18,10 +18,10 @@ package com.android.wm.shell.flicker.pip.common import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import org.junit.Test import org.junit.runners.Parameterized diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt index 7b6839dc123f..0742cf9c5887 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt @@ -18,9 +18,9 @@ package com.android.wm.shell.flicker.pip.common import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.helpers.SimpleAppHelper import org.junit.Test import org.junit.runners.Parameterized diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt index f4baf5f75928..c4881e7e17a1 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt @@ -18,9 +18,9 @@ package com.android.wm.shell.flicker.pip.common import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.flicker.subject.region.RegionSubject import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.flicker.subject.region.RegionSubject import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper import com.android.wm.shell.flicker.utils.Direction import org.junit.Test diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt index fd467e32e0dc..99c1ad2aaa4e 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt @@ -20,11 +20,11 @@ import android.app.Instrumentation import android.content.Intent import android.platform.test.annotations.Presubmit import android.tools.Rotation -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import android.tools.helpers.WindowUtils +import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.helpers.PipAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.testapp.ActivityOptions diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt index 89ef91e12758..61710742abb4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -66,8 +64,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt index 433669205834..bcd0f126daef 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt @@ -19,18 +19,17 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.MultiWindowUtils import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -53,6 +52,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun setup() { Assume.assumeTrue(tapl.isTablet) + MultiWindowUtils.executeShellCommand( + instrumentation, + "settings put system notification_cooldown_enabled 0" + ) // Send a notification sendNotificationApp.launchViaIntent(wmHelper) sendNotificationApp.postNotification(wmHelper) @@ -76,9 +79,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) sendNotificationApp.exit(wmHelper) - } - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + MultiWindowUtils.executeShellCommand( + instrumentation, + "settings reset system notification_cooldown_enabled" + ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt index 8c7e63f7471f..3f07be083041 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -30,7 +29,6 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -88,8 +86,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { secondaryApp.exit(wmHelper) tapl.enableBlockTimeout(false) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt index 2072831d7d1b..532801357d60 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -29,7 +28,6 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -76,8 +74,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { secondaryApp.exit(wmHelper) tapl.enableBlockTimeout(false) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt index 09e77ccffba7..be4035d6af7f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -72,8 +70,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index babdae164835..db962e717a3b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -20,7 +20,6 @@ import android.app.Instrumentation import android.graphics.Point import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.helpers.WindowUtils import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -30,7 +29,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -143,7 +141,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { private fun isLandscape(rotation: Rotation): Boolean { val displayBounds = WindowUtils.getDisplayBounds(rotation) - return displayBounds.width > displayBounds.height + return displayBounds.width() > displayBounds.height() } private fun isTablet(): Boolean { @@ -151,8 +149,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { val LARGE_SCREEN_DP_THRESHOLD = 600 return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt index 3e8547961ea0..de26982501a3 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -69,8 +67,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt index 655ae4e29af3..873b0199f0e8 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -68,8 +66,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt index 22082586bb62..15934d0f3944 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -70,8 +68,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt index 2ac63c2afefc..79e69ae084f4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -71,8 +69,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { thirdApp.exit(wmHelper) fourthApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt index 35b122d7bc9e..0f932d46d3d3 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.AndroidLoggerSetupRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before -import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -68,8 +66,4 @@ abstract class UnlockKeyguardToSplitScreen { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } - - companion object { - @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index d74c59ef0879..7f48499b0558 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -17,12 +17,12 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Presubmit -import android.tools.traces.component.ComponentNameMatcher -import android.tools.traces.component.EdgeExtensionComponentMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher +import android.tools.traces.component.EdgeExtensionComponentMatcher import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt index 8724346427f4..a72b3d15eb9e 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt @@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Presubmit import android.tools.NavBar -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.PipAppHelper import com.android.wm.shell.flicker.splitscreen.benchmark.SplitScreenBase diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt index 16d73318bd3a..90453640c91a 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt @@ -19,13 +19,13 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.NavBar -import android.tools.flicker.subject.layers.LayersTraceSubject -import android.tools.flicker.subject.region.RegionSubject -import android.tools.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.flicker.subject.layers.LayersTraceSubject +import android.tools.flicker.subject.region.RegionSubject +import android.tools.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.splitscreen.benchmark.UnlockKeyguardToSplitScreenBenchmark diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt index 9c5a3fe35bfe..7e8e50843b90 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt @@ -16,11 +16,11 @@ package com.android.wm.shell.flicker.splitscreen.benchmark -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt index 38206c396efb..6a6aa1abc9f3 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt @@ -128,7 +128,7 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy private fun isLandscape(rotation: Rotation): Boolean { val displayBounds = WindowUtils.getDisplayBounds(rotation) - return displayBounds.width > displayBounds.height + return displayBounds.width() > displayBounds.height() } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt index a19d232c9a2f..90d2635f6a51 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.flicker import android.app.Instrumentation -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.flicker.utils.ICommonAssertions diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt index 3df0954da2e9..509f4f202b6b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt @@ -18,13 +18,13 @@ package com.android.wm.shell.flicker.utils +import android.graphics.Region import android.tools.Rotation -import android.tools.datatypes.Region +import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.subject.layers.LayerTraceEntrySubject import android.tools.flicker.subject.layers.LayersTraceSubject -import android.tools.traces.component.IComponentMatcher -import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.helpers.WindowUtils +import android.tools.traces.component.IComponentMatcher fun LegacyFlickerTest.appPairsDividerIsVisibleAtEnd() { assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } @@ -263,41 +263,41 @@ fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider( val displayBounds = WindowUtils.getDisplayBounds(rotation) return invoke { val dividerRegion = - layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region + layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region?.bounds ?: error("$SPLIT_SCREEN_DIVIDER_COMPONENT component not found") visibleRegion(component).isNotEmpty() visibleRegion(component) .coversAtMost( - if (displayBounds.width > displayBounds.height) { + if (displayBounds.width() > displayBounds.height()) { if (landscapePosLeft) { - Region.from( + Region( 0, 0, - (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2, - displayBounds.bounds.bottom + (dividerRegion.left + dividerRegion.right) / 2, + displayBounds.bottom ) } else { - Region.from( - (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2, + Region( + (dividerRegion.left + dividerRegion.right) / 2, 0, - displayBounds.bounds.right, - displayBounds.bounds.bottom + displayBounds.right, + displayBounds.bottom ) } } else { if (portraitPosTop) { - Region.from( + Region( 0, 0, - displayBounds.bounds.right, - (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2 + displayBounds.right, + (dividerRegion.top + dividerRegion.bottom) / 2 ) } else { - Region.from( + Region( 0, - (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2, - displayBounds.bounds.right, - displayBounds.bounds.bottom + (dividerRegion.top + dividerRegion.bottom) / 2, + displayBounds.right, + displayBounds.bottom ) } } @@ -420,17 +420,17 @@ fun LegacyFlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd( fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation.isRotated()) { - Region.from( + Region( 0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, - displayBounds.bounds.bottom + displayBounds.bottom ) } else { - Region.from( + Region( 0, 0, - displayBounds.bounds.right, + displayBounds.right, dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset ) } @@ -439,18 +439,18 @@ fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): Region { fun getSecondaryRegion(dividerRegion: Region, rotation: Rotation): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation.isRotated()) { - Region.from( + Region( dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, - displayBounds.bounds.right, - displayBounds.bounds.bottom + displayBounds.right, + displayBounds.bottom ) } else { - Region.from( + Region( 0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.bounds.right, - displayBounds.bounds.bottom + displayBounds.right, + displayBounds.bottom ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt index 50c04354528f..4465a16a8e0f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.flicker.utils import android.platform.test.annotations.Presubmit -import android.tools.traces.component.ComponentNameMatcher import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt index 4e9a9d65dbf9..c4954f90179c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt @@ -20,11 +20,11 @@ import android.app.Instrumentation import android.graphics.Point import android.os.SystemClock import android.tools.Rotation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.component.IComponentMatcher import android.tools.traces.component.IComponentNameMatcher -import android.tools.device.apphelpers.StandardAppHelper -import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.parsers.toFlickerComponent import android.view.InputDevice @@ -179,15 +179,10 @@ object SplitScreenUtils { val displayBounds = wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace ?: error("Display not found") + val swipeXCoordinate = displayBounds.centerX() / 2 // Pull down the notifications - device.swipe( - displayBounds.centerX(), - 5, - displayBounds.centerX(), - displayBounds.bottom, - 50 /* steps */ - ) + device.swipe(swipeXCoordinate, 5, swipeXCoordinate, displayBounds.bottom, 50 /* steps */) SystemClock.sleep(TIMEOUT_MS) // Find the target notification @@ -210,7 +205,7 @@ object SplitScreenUtils { // Drag to split val dragStart = notificationContent.visibleCenter val dragMiddle = Point(dragStart.x + 50, dragStart.y) - val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4) + val dragEnd = Point(displayBounds.width() / 4, displayBounds.width() / 4) val downTime = SystemClock.uptimeMillis() touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart) @@ -317,7 +312,7 @@ object SplitScreenUtils { wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace ?: error("Display not found") val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) - dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200) + dividerBar.drag(Point(displayBounds.width() * 1 / 3, displayBounds.height() * 2 / 3), 200) wmHelper .StateSyncBuilder() diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 32c070305e05..13f95ccea640 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -39,7 +39,7 @@ android_test { static_libs: [ "WindowManager-Shell", "junit", - "flag-junit-base", + "flag-junit", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", @@ -55,6 +55,9 @@ android_test { "platform-test-annotations", "servicestests-utils", "com_android_wm_shell_flags_lib", + "guava-android-testlib", + "com.android.window.flags.window-aconfig-java", + "platform-test-annotations", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index 3672ae386dc4..24f4d92af9d7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -23,8 +23,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration; +import android.content.Intent; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; @@ -38,6 +40,7 @@ public final class TestRunningTaskInfoBuilder { private WindowContainerToken mToken = createMockWCToken(); private int mParentTaskId = INVALID_TASK_ID; + private Intent mBaseIntent = new Intent(); private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; private int mDisplayId = Display.DEFAULT_DISPLAY; @@ -68,6 +71,15 @@ public final class TestRunningTaskInfoBuilder { return this; } + /** + * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default + * an empty intent is assigned + */ + public TestRunningTaskInfoBuilder setBaseIntent(@NonNull Intent intent) { + mBaseIntent = intent; + return this; + } + public TestRunningTaskInfoBuilder setActivityType( @WindowConfiguration.ActivityType int activityType) { mActivityType = activityType; @@ -109,6 +121,7 @@ public final class TestRunningTaskInfoBuilder { public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); info.taskId = sNextTaskId++; + info.baseIntent = mBaseIntent; info.parentTaskId = mParentTaskId; info.displayId = mDisplayId; info.configuration.windowConfiguration.setBounds(mBounds); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 9ded6ea1d187..65169e36a225 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -16,7 +16,7 @@ package com.android.wm.shell.back; -import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK; +import static android.window.BackNavigationInfo.KEY_NAVIGATION_FINISHED; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -61,6 +61,7 @@ import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -68,7 +69,6 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellSharedConstants; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -113,11 +113,15 @@ public class BackAnimationControllerTest extends ShellTestCase { private InputManager mInputManager; @Mock private ShellCommandHandler mShellCommandHandler; + @Mock + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private BackAnimationController mController; private TestableContentResolver mContentResolver; private TestableLooper mTestableLooper; + private CrossActivityBackAnimation mCrossActivityBackAnimation; + private CrossTaskBackAnimation mCrossTaskBackAnimation; private ShellBackAnimationRegistry mShellBackAnimationRegistry; @Before @@ -131,10 +135,11 @@ public class BackAnimationControllerTest extends ShellTestCase { ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); + mCrossActivityBackAnimation = new CrossActivityBackAnimation(mContext, mAnimationBackground, + mRootTaskDisplayAreaOrganizer); + mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground); mShellBackAnimationRegistry = - new ShellBackAnimationRegistry( - new CrossActivityBackAnimation(mContext, mAnimationBackground), - new CrossTaskBackAnimation(mContext, mAnimationBackground), + new ShellBackAnimationRegistry(mCrossActivityBackAnimation, mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null, new CustomizeActivityAnimation(mContext, mAnimationBackground), /* defaultBackToHomeAnimation= */ null); @@ -178,7 +183,9 @@ public class BackAnimationControllerTest extends ShellTestCase { } RemoteAnimationTarget createAnimationTarget() { - SurfaceControl topWindowLeash = new SurfaceControl(); + SurfaceControl topWindowLeash = new SurfaceControl.Builder() + .setName("FakeLeash") + .build(); return new RemoteAnimationTarget(-1, RemoteAnimationTarget.MODE_CLOSING, topWindowLeash, false, new Rect(), new Rect(), -1, new Point(0, 0), new Rect(), new Rect(), new WindowConfiguration(), @@ -405,6 +412,32 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test + public void gestureNotQueued_WhenPreviousGestureIsPostCommitCancelling() + throws RemoteException { + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, + /* enableAnimation = */ true, + /* isAnimationCallback = */ false); + + doStartEvents(0, 100); + simulateRemoteAnimationStart(); + releaseBackGesture(); + + // Check that back cancellation is dispatched. + verify(mAnimatorCallback).onBackCancelled(); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + + reset(mAnimatorCallback); + reset(mBackAnimationRunner); + + // Verify that a new start event is dispatched if a new gesture is started during the + // post-commit cancel phase + triggerBackGesture(); + verify(mAnimatorCallback).onBackStarted(any()); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + } + + @Test public void acceptsGesture_transitionTimeout() throws RemoteException { registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, @@ -527,17 +560,32 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test + public void skipsCancelWithoutStart() throws RemoteException { + final int type = BackNavigationInfo.TYPE_CALLBACK; + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setOnBackNavigationDone(new RemoteCallback(result))); + doMotionEvent(MotionEvent.ACTION_CANCEL, 0); + mShellExecutor.flushAll(); + + verify(mAppCallback, never()).onBackStarted(any()); + verify(mAppCallback, never()).onBackProgressed(any()); + verify(mAppCallback, never()).onBackInvoked(); + verify(mAppCallback, never()).onBackCancelled(); + } + + @Test public void testBackToActivity() throws RemoteException { - final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext, - mAnimationBackground); - verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner()); + verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, + mCrossActivityBackAnimation.getRunner()); } @Test public void testBackToTask() throws RemoteException { - final CrossTaskBackAnimation animation = new CrossTaskBackAnimation(mContext, - mAnimationBackground); - verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_TASK, animation.getRunner()); + verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_TASK, + mCrossTaskBackAnimation.getRunner()); } private void verifySystemBackBehavior(int type, BackAnimationRunner animation) @@ -548,6 +596,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Set up the monitoring objects. doNothing().when(runner).onAnimationStart(anyInt(), any(), any(), any(), any()); + doReturn(false).when(animationRunner).shouldMonitorCUJ(any()); doReturn(runner).when(animationRunner).getRunner(); doReturn(callback).when(animationRunner).getCallback(); @@ -629,7 +678,7 @@ public class BackAnimationControllerTest extends ShellTestCase { @Override public void onResult(@Nullable Bundle result) { mBackNavigationDone = true; - mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK); + mTriggerBack = result.getBoolean(KEY_NAVIGATION_FINISHED); } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 7e26577e96d4..8932e60048e6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -134,6 +134,31 @@ public class BackProgressAnimatorTest { assertEquals(0, cancelCallbackCalled.getCount()); } + @Test + public void testCancelFinishCallbackNotInvokedWhenRemoved() throws InterruptedException { + // Give the animator some progress. + final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + + // call onBackCancelled (which animates progress to 0 before invoking the finishCallback) + CountDownLatch finishCallbackCalled = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> mProgressAnimator.onBackCancelled(finishCallbackCalled::countDown)); + + // remove onBackCancelled finishCallback (while progress is still animating to 0) + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> mProgressAnimator.removeOnBackCancelledFinishCallback()); + + // call reset (which triggers the finishCallback invocation, if one is present) + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset()); + + // verify that finishCallback is not invoked + assertEquals(1, finishCallbackCalled.getCount()); + } + private void onGestureProgress(BackEvent backEvent) { if (mTargetProgress == backEvent.getProgress()) { mReceivedBackEvent = backEvent; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index fa0aba5a6ee9..6be411dd81d0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -49,6 +49,8 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.bubbles.BubbleData.TimeSource; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.google.common.collect.ImmutableList; @@ -1207,6 +1209,32 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + @Test + public void test_getInitialStateForBubbleBar_includesInitialBubblesAndPosition() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + + BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar(); + assertThat(update.currentBubbleList).hasSize(2); + assertThat(update.currentBubbleList.get(0).getKey()).isEqualTo(mEntryA2.getKey()); + assertThat(update.currentBubbleList.get(1).getKey()).isEqualTo(mEntryA1.getKey()); + assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + } + + @Test + public void setSelectedBubbleAndExpandStack() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index ae39fbcb4eed..4a4c5e860bb2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -37,6 +37,7 @@ import com.android.wm.shell.WindowManagerShellWrapper import com.android.wm.shell.bubbles.bar.BubbleBarLayerView import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue @@ -94,7 +95,8 @@ class BubbleViewInfoTest : ShellTestCase() { val windowManager = context.getSystemService(WindowManager::class.java) val shellInit = ShellInit(mainExecutor) val shellCommandHandler = ShellCommandHandler() - val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor) + val shellController = ShellController(context, shellInit, shellCommandHandler, + mock<DisplayInsetsController>(), mainExecutor) bubblePositioner = BubblePositioner(context, windowManager) val bubbleData = BubbleData( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt index 2f5fe11634a4..bec91e910cf7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt @@ -32,9 +32,12 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.eq +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -77,7 +80,7 @@ class MultiInstanceHelperTest : ShellTestCase() { @Test fun supportsMultiInstanceSplit_inStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -85,7 +88,7 @@ class MultiInstanceHelperTest : ShellTestCase() { @Test fun supportsMultiInstanceSplit_notInStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY) assertEquals(false, helper.supportsMultiInstanceSplit(component)) } @@ -104,7 +107,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect activity property to override application property assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -123,7 +126,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect activity property to override application property assertEquals(false, helper.supportsMultiInstanceSplit(component)) } @@ -141,7 +144,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect fall through to app property assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -158,10 +161,30 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenThrow(PackageManager.NameNotFoundException()) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) assertEquals(false, helper.supportsMultiInstanceSplit(component)) } + @Test + @Throws(PackageManager.NameNotFoundException::class) + fun checkNoMultiInstancePropertyFlag_ignoreProperty() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + val pm = mock<PackageManager>() + val activityProp = PackageManager.Property("", true, "", "") + whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component))) + .thenReturn(activityProp) + val appProp = PackageManager.Property("", true, "", "") + whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName))) + .thenReturn(appProp) + + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false) + // Expect we only check the static list and not the property + assertEquals(false, helper.supportsMultiInstanceSplit(component)) + verify(pm, never()).getProperty(any(), any<ComponentName>()) + } + companion object { val TEST_PACKAGE = "com.android.wm.shell.common" val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake"; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt new file mode 100644 index 000000000000..27e0b196f0be --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.bubbles + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.bubbles.BubbleBarLocation.DEFAULT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BubbleBarLocationTest : ShellTestCase() { + + @Test + fun isOnLeft_rtlEnabled_defaultsToLeft() { + assertThat(DEFAULT.isOnLeft(isRtl = true)).isTrue() + } + + @Test + fun isOnLeft_rtlDisabled_defaultsToRight() { + assertThat(DEFAULT.isOnLeft(isRtl = false)).isFalse() + } + + @Test + fun isOnLeft_left_trueForAllLanguageDirections() { + assertThat(LEFT.isOnLeft(isRtl = false)).isTrue() + assertThat(LEFT.isOnLeft(isRtl = true)).isTrue() + } + + @Test + fun isOnLeft_right_falseForAllLanguageDirections() { + assertThat(RIGHT.isOnLeft(isRtl = false)).isFalse() + assertThat(RIGHT.isOnLeft(isRtl = true)).isFalse() + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt index a4fb3504f31d..8bb182de7668 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt @@ -22,7 +22,7 @@ import android.view.View import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt new file mode 100644 index 000000000000..4cd2a366f5eb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for {@link AppCompatUtils}. + * + * Build/Install/Run: + * atest WMShellUnitTests:AppCompatUtilsTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class AppCompatUtilsTest : ShellTestCase() { + + @Test + fun testIsSingleTopActivityTranslucent() { + assertTrue(isSingleTopActivityTranslucent( + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityTransparent = true + numActivities = 1 + })) + assertFalse(isSingleTopActivityTranslucent( + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityTransparent = true + numActivities = 0 + })) + assertFalse(isSingleTopActivityTranslucent( + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityTransparent = false + numActivities = 1 + })) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt new file mode 100644 index 000000000000..65117f7e9eea --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS +import android.view.WindowManager.TRANSIT_NONE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_SLEEP +import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.view.WindowManager.TRANSIT_WAKE +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeLoggerTransitionObserverTest { + + @JvmField + @Rule + val extendedMockitoRule = ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeEventLogger::class.java) + .mockStatic(DesktopModeStatus::class.java).build()!! + + @Mock + lateinit var testExecutor: ShellExecutor + @Mock + private lateinit var mockShellInit: ShellInit + @Mock + private lateinit var transitions: Transitions + + private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver + private lateinit var shellInit: ShellInit + private lateinit var desktopModeEventLogger: DesktopModeEventLogger + + @Before + fun setup() { + Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true) + shellInit = Mockito.spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) + + transitionObserver = DesktopModeLoggerTransitionObserver( + mockShellInit, transitions, desktopModeEventLogger) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + val initRunnableCaptor = ArgumentCaptor.forClass( + Runnable::class.java) + verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), + same(transitionObserver)) + initRunnableCaptor.value.run() + } else { + transitionObserver.onInit() + } + } + + @Test + fun testRegistersObserverAtInit() { + verify(transitions) + .registerObserver(same( + transitionObserver)) + } + + @Test + fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + verify(desktopModeEventLogger, never()).logTaskAdded(any(), any()) + } + + @Test + fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_FREEFORM_INTENT)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + // task change is finalised when drag ends + val transitionInfo = TransitionInfoBuilder( + Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_DRAG)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_MENU_BUTTON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() { + val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + taskInfo.isVisibleRequested = true + val change = createChange(TRANSIT_CHANGE, taskInfo) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.APP_HANDLE_MENU_BUTTON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun taskToFront_screenWake_logSessionStartedAndTaskAdded() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0) + .addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), + eq(EnterReason.SCREEN_ON)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + } + + @Test + fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.SCREEN_OFF)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE) + .addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.DRAG_TO_EXIT)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // task closing + val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.TASK_FINISHED)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun sessionExitByRecents_cancelledAnimation_sessionRestored() { + val sessionId = 1 + // add a freeform task to an existing session + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition sent freeform window to back + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change) + .build() + callOnTransitionReady(transitionInfo1) + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), + eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + + val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() + callOnTransitionReady(transitionInfo2) + + verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any()) + verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any()) + } + + @Test + fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { + val sessionId = 1 + // add an existing freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task added + val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + } + + @Test + fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { + val sessionId = 1 + // add two existing freeform tasks + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task added + val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, never()).logSessionExit(any(), any()) + } + + /** + * Simulate calling the onTransitionReady() method + */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val transition = mock(IBinder::class.java) + val startT = mock( + SurfaceControl.Transaction::class.java) + val finishT = mock( + SurfaceControl.Transaction::class.java) + + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) + } + + companion object { + fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowMode + + return taskInfo + } + + fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { + val change = Change( + WindowContainerToken(mock( + IWindowContainerToken::class.java)), + mock(SurfaceControl::class.java)) + change.mode = mode + change.taskInfo = taskInfo + return change + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 445f74a52b0d..b2b54acf4585 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.desktopmode +import android.graphics.Rect import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor @@ -117,6 +119,57 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun isOnlyActiveTask_noActiveTasks() { + // Not an active task + assertThat(repo.isOnlyActiveTask(1)).isFalse() + } + + @Test + fun isOnlyActiveTask_singleActiveTask() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + // The only active task + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isTrue() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test + fun isOnlyActiveTask_multipleActiveTasks() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + repo.addActiveTask(DEFAULT_DISPLAY, 2) + // Not the only task + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isFalse() + // Not the only task + assertThat(repo.isActiveTask(2)).isTrue() + assertThat(repo.isOnlyActiveTask(2)).isFalse() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test + fun isOnlyActiveTask_multipleDisplays() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + repo.addActiveTask(DEFAULT_DISPLAY, 2) + repo.addActiveTask(SECOND_DISPLAY, 3) + // Not the only task on DEFAULT_DISPLAY + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isFalse() + // Not the only task on DEFAULT_DISPLAY + assertThat(repo.isActiveTask(2)).isTrue() + assertThat(repo.isOnlyActiveTask(2)).isFalse() + // The only active task on SECOND_DISPLAY + assertThat(repo.isActiveTask(3)).isTrue() + assertThat(repo.isOnlyActiveTask(3)).isTrue() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test fun addListener_notifiesVisibleFreeformTask() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) val listener = TestVisibilityListener() @@ -237,6 +290,27 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) } + /** + * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY. + * This tests that task is removed from the last parent display when it vanishes. + */ + @Test + fun updateVisibleFreeformTasks_removeVisibleTasksRemovesTaskWithInvalidDisplay() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + executor.flushAll() + + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) + repo.updateVisibleFreeformTasks(INVALID_DISPLAY, taskId = 1, visible = false) + executor.flushAll() + + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) + } + @Test fun getVisibleTaskCount() { // No tasks, count is 0 @@ -384,6 +458,31 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(listener.stashedOnSecondaryDisplay).isTrue() } + @Test + fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { + val taskId = 1 + repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) + repo.removeFreeformTask(taskId) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() + } + + @Test + fun saveBoundsBeforeMaximize_boundsSavedByTaskId() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + repo.saveBoundsBeforeMaximize(taskId, bounds) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isEqualTo(bounds) + } + + @Test + fun removeBoundsBeforeMaximize_returnsNullAfterBoundsRemoved() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + repo.saveBoundsBeforeMaximize(taskId, bounds) + repo.removeBoundsBeforeMaximize(taskId) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() + } + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index f8ce4ee8e1ce..bd39aa6ace42 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -56,31 +56,29 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { context, taskSurface, taskDisplayAreaOrganizer) whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) + whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) } @Test fun testFullscreenRegionCalculation() { val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_transition_area_height) + R.dimen.desktop_mode_fullscreen_from_desktop_height) val fromFreeformWidth = mContext.resources.getDimensionPixelSize( R.dimen.desktop_mode_fullscreen_from_desktop_width ) - val fromFreeformHeight = mContext.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_height - ) var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect( DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, -50, DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, - fromFreeformHeight)) + transitionHeight)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) } @Test @@ -135,5 +133,12 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private const val TRANSITION_AREA_WIDTH = 32 private const val CAPTION_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private const val NAVBAR_HEIGHT = 50 + private val STABLE_INSETS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top + CAPTION_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 35c803b78674..64f604119a8b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -23,34 +23,49 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.Intent +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect import android.os.Binder +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DisplayAreaInfo import android.window.RemoteTransition import android.window.TransitionRequestInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.window.flags.Flags import com.android.wm.shell.MockToken import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.transition.TestRemoteTransition import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.split.SplitScreenConstants import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask @@ -63,16 +78,17 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.OneShotRemoteHandler +import com.android.wm.shell.transition.TestRemoteTransition import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE import com.android.wm.shell.transition.Transitions.TransitionHandler -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -84,32 +100,46 @@ import org.mockito.Mockito import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.capture +import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever +/** + * Test class for {@link DesktopTasksController} + * + * Usage: atest WMShellUnitTests:DesktopTasksControllerTest + */ @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopTasksControllerTest : ShellTestCase() { + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellCommandHandler: ShellCommandHandler @Mock lateinit var shellController: ShellController @Mock lateinit var displayController: DisplayController + @Mock lateinit var displayLayout: DisplayLayout @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler - @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler: + @Mock lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler @Mock lateinit var launchAdjacentController: LaunchAdjacentController - @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration @Mock lateinit var splitScreenController: SplitScreenController @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler @Mock lateinit var dragAndDropController: DragAndDropController @Mock lateinit var multiInstanceHelper: MultiInstanceHelper + @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -118,12 +148,14 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private val shellExecutor = TestShellExecutor() + // Mock running tasks are registered here so we can get the list from mock shell task organizer private val runningTasks = mutableListOf<RunningTaskInfo>() @Before fun setUp() { - mockitoSession = mockitoSession().mockStatic(DesktopModeStatus::class.java).startMocking() + mockitoSession = mockitoSession().strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java).startMocking() whenever(DesktopModeStatus.isEnabled()).thenReturn(true) shellInit = Mockito.spy(ShellInit(testExecutor)) @@ -131,6 +163,14 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + + val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -156,9 +196,10 @@ class DesktopTasksControllerTest : ShellTestCase() { transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, - mToggleResizeDesktopTaskTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, @@ -189,7 +230,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_allAppsInvisible_bringsToFront() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -208,7 +250,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_appsAlreadyVisible_bringsToFront() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -227,7 +289,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_someAppsInvisible_reordersAll() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -246,7 +328,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_noActiveTasks_reorderHomeToTop() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) @@ -258,7 +360,18 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -277,6 +390,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Expect order to be from bottom: wallpaper intent, task + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -306,9 +438,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() { + fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -316,9 +449,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() { + fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -332,7 +466,59 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_otherFreeformTasksBroughtToFront() { + fun moveToDesktop_deviceNotSupported_doesNothing() { + val task = setUpFullscreenTask() + + // Simulate non compatible device + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + controller.moveToDesktop(task) + verifyWCTNotExecuted() + } + + @Test + fun moveToDesktop_topActivityTranslucent_doesNothing() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = setUpFullscreenTask().apply { + isTopActivityTransparent = true + numActivities = 1 + } + + controller.moveToDesktop(task) + verifyWCTNotExecuted() + } + + @Test + fun moveToDesktop_deviceNotSupported_deviceRestrictionsOverridden_taskIsMovedToDesktop() { + val task = setUpFullscreenTask() + + // Simulate non compatible device + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + // Simulate enforce device restrictions system property overridden to false + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) + + controller.moveToDesktop(task) + + val wct = getLatestMoveToDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() { + val task = setUpFullscreenTask() + + controller.moveToDesktop(task) + + val wct = getLatestMoveToDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() @@ -350,6 +536,26 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask) + + with(getLatestMoveToDesktopWct()) { + // Operations should include wallpaper intent, freeform task, fullscreen task + assertThat(hierarchyOps).hasSize(3) + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + assertReorderAt(index = 1, freeformTask) + assertReorderAt(index = 2, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { setUpHomeTask(displayId = DEFAULT_DISPLAY) val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -378,7 +584,9 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(), + verify(splitScreenController).prepareExitSplitScreen( + any(), + anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) ) } @@ -390,15 +598,18 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController, never()).prepareExitSplitScreen(any(), anyInt(), + verify(splitScreenController, never()).prepareExitSplitScreen( + any(), + anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) ) } @Test - fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() { + fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -406,9 +617,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() { + fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -510,6 +722,48 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onDesktopWindowClose_noActiveTasks() { + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, 1 /* taskId */) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task1.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -553,6 +807,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) @@ -599,7 +854,7 @@ class DesktopTasksControllerTest : ShellTestCase() { createTransition(freeformTask2, type = TRANSIT_TO_FRONT) ) assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -609,7 +864,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -621,10 +876,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) @@ -698,6 +954,68 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = setUpFreeformTask().apply { + isTopActivityTransparent = true + numActivities = 1 + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_noToken() { + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() { + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + assertThat(result).isNotNull() + // Creates remove wallpaper transaction + result!!.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasks() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task1 = setUpFreeformTask() + setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test fun stashDesktopApps_stateUpdates() { whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) @@ -741,7 +1059,7 @@ class DesktopTasksControllerTest : ShellTestCase() { verify(launchAdjacentController).launchAdjacentEnabled = true } @Test - fun enterDesktop_fullscreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { val task1 = setUpFullscreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -750,7 +1068,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = false task3.isFocused = false - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) @@ -758,7 +1076,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun enterDesktop_splitScreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -771,12 +1089,14 @@ class DesktopTasksControllerTest : ShellTestCase() { task4.parentTaskId = task1.taskId - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(), + verify(splitScreenController).prepareExitSplitScreen( + any(), + anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) ) } @@ -795,11 +1115,117 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = getLatestExitDesktopWct() assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { + val task = setUpFreeformTask() + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + controller.onDragPositioningMove(task, mockSurface, 200f, + Rect(100, -100, 500, 1000)) + + controller.onDragPositioningEnd(task, + Point(100, -100), /* position */ + PointF(200f, -200f), /* inputCoordinate */ + Rect(100, -100, 500, 1000), /* taskBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */ + ) + val rectAfterEnd = Rect(100, 50, 500, 1150) + verify(transitions).startTransition( + eq(TRANSIT_CHANGE), Mockito.argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }, eq(null)) + } + + fun enterSplit_freeformTaskIsMovedToSplit() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterSplit(DEFAULT_DISPLAY, false) + + verify(splitScreenController).requestEnterSplitSelect( + task2, + any(), + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, + task2.configuration.windowConfiguration.bounds + ) + } + + @Test + fun toggleBounds_togglesToStableBounds() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize(task) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds) + .isEqualTo(STABLE_BOUNDS) + } + + @Test + fun toggleBounds_lastBoundsBeforeMaximizeSaved() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize(task) + assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)) + .isEqualTo(bounds) } - private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createFreeformTask(displayId) + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds) + .isEqualTo(boundsBeforeMaximize) + } + + @Test + fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert last bounds before maximize removed after use + assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + } + + private val desktopWallpaperIntent: Intent + get() = Intent(context, DesktopWallpaperActivity::class.java) + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + bounds: Rect? = null + ): RunningTaskInfo { + val task = createFreeformTask(displayId, bounds) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) desktopModeTaskRepository.addActiveTask(displayId, task.taskId) desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId) @@ -816,6 +1242,8 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFullscreenTask(displayId) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) return task @@ -823,6 +1251,8 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createSplitScreenTask(displayId) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) @@ -862,6 +1292,18 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } + private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) + .startTransition(capture(arg)) + } else { + verify(shellTaskOrganizer).applyTransaction(capture(arg)) + } + return arg.value + } + private fun getLatestMoveToDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) if (ENABLE_SHELL_TRANSITIONS) { @@ -900,13 +1342,18 @@ class DesktopTasksControllerTest : ShellTestCase() { companion object { const val SECOND_DISPLAY = 2 + private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) } } -private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { +private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { assertWithMessage("WCT does not have a hierarchy operation at index $index") .that(hierarchyOps.size) .isGreaterThan(index) +} + +private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { + assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) @@ -917,3 +1364,17 @@ private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: Runni assertReorderAt(i, tasks[i]) } } + +private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) +} + +private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index 2f6f3207137d..52da7fb811d0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.graphics.Rect import android.view.Display.DEFAULT_DISPLAY import com.android.wm.shell.MockToken import com.android.wm.shell.TestRunningTaskInfoBuilder @@ -31,13 +32,17 @@ class DesktopTestHelpers { /** Create a task that has windowing mode set to [WINDOWING_MODE_FREEFORM] */ @JvmStatic @JvmOverloads - fun createFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + fun createFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + bounds: Rect? = null + ): RunningTaskInfo { return TestRunningTaskInfoBuilder() .setDisplayId(displayId) .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FREEFORM) .setLastActiveTime(100) + .apply { bounds?.let { setBounds(it) }} .build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 98e90d60b3b6..2ade3fba9b08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -190,7 +190,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { handler.cancelDragToDesktopTransition() // Cancel animation should run since it had already started. - verify(dragAnimator).endAnimator() + verify(dragAnimator).cancelAnimator() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 3384509f1da9..d38fc6cb6418 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -129,7 +129,7 @@ public class PipControllerTest extends ShellTestCase { }).when(mMockExecutor).execute(any()); mShellInit = spy(new ShellInit(mMockExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mMockShellCommandHandler, - mMockExecutor)); + mMockDisplayInsetsController, mMockExecutor)); mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, mShellController, mMockDisplayController, mMockPipAnimationController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 10e9e11e9004..40b59c1ddc31 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -35,6 +35,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -45,19 +46,25 @@ import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Rect; import android.os.Bundle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; @@ -69,6 +76,7 @@ import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -96,6 +104,13 @@ public class RecentTasksControllerTest extends ShellTestCase { private DesktopModeTaskRepository mDesktopModeTaskRepository; @Mock private ActivityTaskManager mActivityTaskManager; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private IRecentTasksListener mRecentTasksListener; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; @@ -110,7 +125,7 @@ public class RecentTasksControllerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, Optional.of(mDesktopModeTaskRepository), mMainExecutor); @@ -299,6 +314,54 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() { + StaticMockitoSession mockitoSession = mockitoSession().mockStatic( + DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isEnabled()).thenReturn(true); + + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5); + setRawList(t1, t2, t3, t4, t5); + + SplitBounds pair1Bounds = + new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50); + mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds); + + when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true); + when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true); + + ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( + MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); + + // 2 split screen tasks grouped, 2 freeform tasks grouped, 3 total recents entries + assertEquals(3, recentTasks.size()); + GroupedRecentTaskInfo splitGroup = recentTasks.get(0); + GroupedRecentTaskInfo freeformGroup = recentTasks.get(1); + GroupedRecentTaskInfo singleGroup = recentTasks.get(2); + + // Check that groups have expected types + assertEquals(GroupedRecentTaskInfo.TYPE_SPLIT, splitGroup.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup.getType()); + + // Check freeform group entries + assertEquals(t3, freeformGroup.getTaskInfoList().get(0)); + assertEquals(t5, freeformGroup.getTaskInfoList().get(1)); + + // Check split group entries + assertEquals(t1, splitGroup.getTaskInfoList().get(0)); + assertEquals(t2, splitGroup.getTaskInfoList().get(1)); + + // Check single entry + assertEquals(t4, singleGroup.getTaskInfo1()); + + mockitoSession.finishMocking(); + } + + @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() { StaticMockitoSession mockitoSession = mockitoSession().mockStatic( DesktopModeStatus.class).startMocking(); @@ -375,6 +438,85 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void onTaskAdded_desktopModeRunningAppsEnabled_triggersOnRunningTaskAppeared() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskAdded(taskInfo); + + verify(mRecentTasksListener).onRunningTaskAppeared(taskInfo); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS) + public void onTaskAdded_desktopModeRunningAppsDisabled_doesNotTriggerOnRunningTaskAppeared() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskAdded(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskAppeared(any()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void taskWindowingModeChanged_desktopRunningAppsEnabled_triggersOnRunningTaskChanged() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskWindowingModeChanged(taskInfo); + + verify(mRecentTasksListener).onRunningTaskChanged(taskInfo); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS) + public void + taskWindowingModeChanged_desktopRunningAppsDisabled_doesNotTriggerOnRunningTaskChanged() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskWindowingModeChanged(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskChanged(any()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void onTaskRemoved_desktopModeRunningAppsEnabled_triggersOnRunningTaskVanished() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskRemoved(taskInfo); + + verify(mRecentTasksListener).onRunningTaskVanished(taskInfo); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS) + public void onTaskRemoved_desktopModeRunningAppsDisabled_doesNotTriggerOnRunningTaskVanished() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskRemoved(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskVanished(any()); + } + + @Test public void getNullSplitBoundsNonSplitTask() { SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3); assertNull("splitBounds should be null for non-split task", sb); @@ -420,6 +562,7 @@ public class RecentTasksControllerTest extends ShellTestCase { private ActivityManager.RunningTaskInfo makeRunningTaskInfo(int taskId) { ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); info.taskId = taskId; + info.realActivity = new ComponentName("testPackage", "testClass"); return info; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt index e7274918fa2b..3fb66be2f91c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.animation +package com.android.wm.shell.shared.animation import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -27,11 +27,11 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.animation.PhysicsAnimator.EndListener -import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener -import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames -import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames -import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames +import com.android.wm.shell.shared.animation.PhysicsAnimator.EndListener +import com.android.wm.shell.shared.animation.PhysicsAnimator.UpdateListener +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames import org.junit.After import org.junit.Assert import org.junit.Assert.assertEquals diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 315d97ed333b..3c387f0d7c34 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -123,7 +123,7 @@ public class SplitScreenControllerTests extends ShellTestCase { assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); MockitoAnnotations.initMocks(this); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController, mDisplayImeController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java index 012c40811811..ff76a2f13527 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -40,6 +40,7 @@ import com.android.internal.util.function.TriConsumer; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -65,6 +66,7 @@ public class StartingWindowControllerTests extends ShellTestCase { private @Mock Context mContext; private @Mock DisplayManager mDisplayManager; + private @Mock DisplayInsetsController mDisplayInsetsController; private @Mock ShellCommandHandler mShellCommandHandler; private @Mock ShellTaskOrganizer mTaskOrganizer; private @Mock ShellExecutor mMainExecutor; @@ -83,7 +85,7 @@ public class StartingWindowControllerTests extends ShellTestCase { doReturn(super.mContext.getResources()).when(mContext).getResources(); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, - mMainExecutor)); + mDisplayInsetsController, mMainExecutor)); mController = new StartingWindowController(mContext, mShellInit, mShellController, mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); mShellInit.init(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java index 7c520c34b29d..6292018ba35d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import android.content.Context; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -35,8 +36,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ExternalInterfaceBinder; -import com.android.wm.shell.common.ShellExecutor; import org.junit.After; import org.junit.Before; @@ -63,12 +64,15 @@ public class ShellControllerTest extends ShellTestCase { private ShellCommandHandler mShellCommandHandler; @Mock private Context mTestUserContext; + @Mock + private DisplayInsetsController mDisplayInsetsController; private TestShellExecutor mExecutor; private ShellController mController; private TestConfigurationChangeListener mConfigChangeListener; private TestKeyguardChangeListener mKeyguardChangeListener; private TestUserChangeListener mUserChangeListener; + private TestDisplayImeChangeListener mDisplayImeChangeListener; @Before @@ -77,8 +81,10 @@ public class ShellControllerTest extends ShellTestCase { mKeyguardChangeListener = new TestKeyguardChangeListener(); mConfigChangeListener = new TestConfigurationChangeListener(); mUserChangeListener = new TestUserChangeListener(); + mDisplayImeChangeListener = new TestDisplayImeChangeListener(); mExecutor = new TestShellExecutor(); - mController = new ShellController(mContext, mShellInit, mShellCommandHandler, mExecutor); + mController = new ShellController(mContext, mShellInit, mShellCommandHandler, + mDisplayInsetsController, mExecutor); mController.onConfigurationChanged(getConfigurationCopy()); } @@ -130,6 +136,45 @@ public class ShellControllerTest extends ShellTestCase { } @Test + public void testAddDisplayImeChangeListener_ensureCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + + final Rect bounds = new Rect(10, 20, 30, 40); + mController.onImeBoundsChanged(bounds); + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + + assertTrue(mDisplayImeChangeListener.boundsChanged == 1); + assertTrue(bounds.equals(mDisplayImeChangeListener.lastBounds)); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 1); + assertTrue(mDisplayImeChangeListener.lastVisibility); + } + + @Test + public void testDoubleAddDisplayImeChangeListener_ensureSingleCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 1); + } + + @Test + public void testAddRemoveDisplayImeChangeListener_ensureNoCallback() { + mController.asShell().addDisplayImeChangeListener( + mDisplayImeChangeListener, mExecutor); + mController.asShell().removeDisplayImeChangeListener(mDisplayImeChangeListener); + + mController.onImeVisibilityChanged(true); + mExecutor.flushAll(); + assertTrue(mDisplayImeChangeListener.visibilityChanged == 0); + } + + @Test public void testAddUserChangeListener_ensureCallback() { mController.addUserChangeListener(mUserChangeListener); @@ -457,4 +502,23 @@ public class ShellControllerTest extends ShellTestCase { lastUserProfiles = profiles; } } + + private static class TestDisplayImeChangeListener implements DisplayImeChangeListener { + public int boundsChanged = 0; + public Rect lastBounds; + public int visibilityChanged = 0; + public boolean lastVisibility = false; + + @Override + public void onImeBoundsChanged(int displayId, Rect bounds) { + boundsChanged++; + lastBounds = bounds; + } + + @Override + public void onImeVisibilityChanged(int displayId, boolean isShowing) { + visibilityChanged++; + lastVisibility = isShowing; + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 66efa02de764..0db10ef65a74 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -24,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; @@ -51,6 +52,7 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -166,6 +168,25 @@ public class HomeTransitionObserverTest extends ShellTestCase { } @Test + public void testStartDragToDesktopDoesNotTriggerCallback() throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + when(info.getType()).thenReturn(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP); + + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); + } + + @Test public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException { TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index e9da25813510..2366917a0158 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -83,6 +83,7 @@ import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.IWindowContainerToken; import android.window.RemoteTransition; +import android.window.RemoteTransitionStub; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -280,7 +281,7 @@ public class ShellTransitionTests extends ShellTestCase { final boolean[] remoteCalled = new boolean[]{false}; final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -288,16 +289,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, @@ -450,7 +441,7 @@ public class ShellTransitionTests extends ShellTestCase { transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -458,16 +449,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; TransitionFilter filter = new TransitionFilter(); @@ -500,7 +481,7 @@ public class ShellTransitionTests extends ShellTestCase { final boolean[] remoteCalled = new boolean[]{false}; final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -508,16 +489,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; final int transitType = TRANSIT_FIRST_CUSTOM + 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java index 87330d2dc877..184e8955d08c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java @@ -20,6 +20,7 @@ import android.os.RemoteException; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransitionStub; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; @@ -29,7 +30,7 @@ import android.window.WindowContainerTransaction; * {@link #startAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction, * IRemoteTransitionFinishedCallback)} being called. */ -public class TestRemoteTransition extends IRemoteTransition.Stub { +public class TestRemoteTransition extends RemoteTransitionStub { private boolean mCalled = false; private boolean mConsumed = false; final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction(); @@ -44,12 +45,6 @@ public class TestRemoteTransition extends IRemoteTransition.Stub { } @Override - public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { mConsumed = true; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 917fd715f71f..8e9619dc1430 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -27,8 +27,11 @@ import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.os.Handler +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.util.SparseArray import android.view.Choreographer import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager @@ -41,6 +44,10 @@ import android.view.SurfaceView import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -50,6 +57,7 @@ import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopModeStatus import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.sysui.KeyguardChangeListener import com.android.wm.shell.sysui.ShellCommandHandler @@ -57,10 +65,14 @@ import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import java.util.Optional +import java.util.function.Supplier import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -69,16 +81,22 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq +import org.mockito.kotlin.spy import org.mockito.kotlin.whenever -import java.util.Optional -import java.util.function.Supplier - +import org.mockito.quality.Strictness -/** Tests of [DesktopModeWindowDecorViewModel] */ +/** + * Tests of [DesktopModeWindowDecorViewModel] + * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests + */ @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper class DesktopModeWindowDecorViewModelTests : ShellTestCase() { + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + @Mock private lateinit var mockDesktopModeWindowDecorFactory: DesktopModeWindowDecoration.Factory @Mock private lateinit var mockMainHandler: Handler @@ -102,6 +120,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() } + private val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>()) private lateinit var shellInit: ShellInit private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener @@ -110,6 +129,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Before fun setUp() { shellInit = ShellInit(mockShellExecutor) + windowDecorByTaskIdSpy.clear() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( mContext, mockShellExecutor, @@ -128,7 +148,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + windowDecorByTaskIdSpy ) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -272,6 +293,19 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testDescorationIsNotCreatedForTopTranslucentActivities() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { + isTopActivityTransparent = true + numActivities = 1 + } + onTaskOpening(task) + + verify(mockDesktopModeWindowDecorFactory, never()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } + + @Test fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -332,6 +366,89 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(decoration, times(1)).relayout(task) } + @Test + fun testDestroyWindowDecoration_closesBeforeCleanup() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + val inOrder = Mockito.inOrder(decoration, windowDecorByTaskIdSpy) + + onTaskOpening(task) + desktopModeWindowDecorViewModel.destroyWindowDecoration(task) + + inOrder.verify(decoration).close() + inOrder.verify(windowDecorByTaskIdSpy).remove(task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() { + val mockitoSession: StaticMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + try { + // Simulate default enforce device restrictions system property + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) + + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + // Simulate device that doesn't support desktop mode + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + onTaskOpening(task) + verify(mockDesktopModeWindowDecorFactory, never()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } finally { + mockitoSession.finishMocking() + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() { + val mockitoSession: StaticMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + try { + // Simulate enforce device restrictions system property overridden to false + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) + // Simulate device that doesn't support desktop mode + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + setUpMockDecorationsForTasks(task) + + onTaskOpening(task) + verify(mockDesktopModeWindowDecorFactory) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } finally { + mockitoSession.finishMocking() + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() { + val mockitoSession: StaticMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + try { + // Simulate default enforce device restrictions system property + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) + + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + setUpMockDecorationsForTasks(task) + + onTaskOpening(task) + verify(mockDesktopModeWindowDecorFactory) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } finally { + mockitoSession.finishMocking() + } + } + private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) { desktopModeWindowDecorViewModel.onTaskOpening( task, @@ -368,7 +485,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration { val decoration = mock(DesktopModeWindowDecoration::class.java) - whenever(mockDesktopModeWindowDecorFactory.create( + whenever( + mockDesktopModeWindowDecorFactory.create( any(), any(), any(), eq(task), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task @@ -387,7 +505,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400, - /*densityDpi=*/320, + /*densityDpi=*/ 320, surfaceView.holder.surface, DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 9e62bd254ac5..f9b5882f7ad5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -173,10 +173,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test - public void updateRelayoutParams_freeformAndTransparent_allowsInputFallthrough() { + public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); - taskInfo.taskDescription.setStatusBarAppearance( + taskInfo.taskDescription.setSystemBarsAppearance( APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); final RelayoutParams relayoutParams = new RelayoutParams(); @@ -191,10 +191,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test - public void updateRelayoutParams_freeformButOpaque_disallowsInputFallthrough() { + public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); - taskInfo.taskDescription.setStatusBarAppearance(0); + taskInfo.taskDescription.setSystemBarsAppearance(0); final RelayoutParams relayoutParams = new RelayoutParams(); DesktopModeWindowDecoration.updateRelayoutParams( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index e60be7186b1e..e6fabcfec58a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -189,8 +189,9 @@ class DragPositioningCallbackUtilityTest { DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100) - DragPositioningCallbackUtility.onDragEnd(repositionTaskBounds, STARTING_BOUNDS, - startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat(), + DragPositioningCallbackUtility.updateTaskBounds(repositionTaskBounds, STARTING_BOUNDS, + startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat()) + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea) assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left) assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java new file mode 100644 index 000000000000..82e5a1cd25ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; + +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Region; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.testing.AndroidTestingRunner; +import android.util.Size; + +import androidx.test.filters.SmallTest; + +import com.android.window.flags.Flags; + +import com.google.common.testing.EqualsTester; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link DragResizeWindowGeometry}. + * + * Build/Install/Run: + * atest WMShellUnitTests:DragResizeWindowGeometryTests + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DragResizeWindowGeometryTests { + private static final Size TASK_SIZE = new Size(500, 1000); + private static final int TASK_CORNER_RADIUS = 10; + private static final int EDGE_RESIZE_THICKNESS = 15; + private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10; + private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10; + private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry( + TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE); + // Points in the edge resize handle. Note that coordinates start from the top left. + private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, + -EDGE_RESIZE_THICKNESS / 2); + private static final Point LEFT_EDGE_POINT = new Point(-EDGE_RESIZE_THICKNESS / 2, + TASK_SIZE.getHeight() / 2); + private static final Point RIGHT_EDGE_POINT = new Point( + TASK_SIZE.getWidth() + EDGE_RESIZE_THICKNESS / 2, TASK_SIZE.getHeight() / 2); + private static final Point BOTTOM_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, + TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + /** + * Check that both groups of objects satisfy equals/hashcode within each group, and that each + * group is distinct from the next. + */ + @Test + public void testEqualsAndHash() { + new EqualsTester() + .addEqualityGroup( + GEOMETRY, + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + .addEqualityGroup( + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + .addEqualityGroup( + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5)) + .testEquals(); + } + + @Test + public void testGetTaskSize() { + assertThat(GEOMETRY.getTaskSize()).isEqualTo(TASK_SIZE); + } + + @Test + public void testRegionUnionContainsEdges() { + Region region = new Region(); + GEOMETRY.union(region); + assertThat(region.isComplex()).isTrue(); + // Region excludes task area. Note that coordinates start from top left. + assertThat(region.contains(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() / 2)).isFalse(); + // Region includes edges outside the task window. + verifyVerticalEdge(region, LEFT_EDGE_POINT); + verifyHorizontalEdge(region, TOP_EDGE_POINT); + verifyVerticalEdge(region, RIGHT_EDGE_POINT); + verifyHorizontalEdge(region, BOTTOM_EDGE_POINT); + } + + private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) { + assertThat(region.contains(point.x, point.y)).isTrue(); + // Horizontally along the edge is still contained. + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue(); + // Vertically along the edge is not contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isFalse(); + } + + private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) { + assertThat(region.contains(point.x, point.y)).isTrue(); + // Horizontally along the edge is not contained. + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse(); + // Vertically along the edge is contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue(); + } + + /** + * Validate that with the flag enabled, the corner resize regions are the largest size, to + * capture all eligible input regardless of source (touch or cursor). + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() { + Region region = new Region(); + GEOMETRY.union(region); + final int cornerRadius = LARGE_CORNER_SIZE / 2; + + new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + } + + /** + * Validate that with the flag disabled, the corner resize regions are the original smaller + * size. + */ + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() { + Region region = new Region(); + GEOMETRY.union(region); + final int cornerRadius = FINE_CORNER_SIZE / 2; + + new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeEnabled_edges() { + // The input source (touch or cursor) shouldn't impact the edge resize size. + validateCtrlTypeForEdges(/* isTouch= */ false); + validateCtrlTypeForEdges(/* isTouch= */ true); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeDisabled_edges() { + // Edge resizing is not supported when the flag is disabled. + validateCtrlTypeForEdges(/* isTouch= */ false); + validateCtrlTypeForEdges(/* isTouch= */ false); + } + + private void validateCtrlTypeForEdges(boolean isTouch) { + assertThat(GEOMETRY.calculateCtrlType(isTouch, LEFT_EDGE_POINT.x, + LEFT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_LEFT); + assertThat(GEOMETRY.calculateCtrlType(isTouch, TOP_EDGE_POINT.x, + TOP_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_TOP); + assertThat(GEOMETRY.calculateCtrlType(isTouch, RIGHT_EDGE_POINT.x, + RIGHT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_RIGHT); + assertThat(GEOMETRY.calculateCtrlType(isTouch, BOTTOM_EDGE_POINT.x, + BOTTOM_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_BOTTOM); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeEnabled_corners() { + final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + + // When the flag is enabled, points within fine corners should pass regardless of touch or + // not. Points outside fine corners should not pass when using a course input (non-touch). + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + + // When the flag is enabled, points near the large corners should only pass when the point + // is within the corner for large touch inputs. + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, + false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, + false); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeDisabled_corners() { + final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + + // When the flag is disabled, points within fine corners should pass only when touch. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + + // When the flag is disabled, points near the large corners should never pass. + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, + false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, + false); + } + + /** + * Class for creating points for testing the drag resize corners. + * + * <p>Creates points that are both just within the bounds of each corner, and just outside. + */ + private static final class TestPoints { + private final Point mTopLeftPoint; + private final Point mTopLeftPointOutside; + private final Point mTopRightPoint; + private final Point mTopRightPointOutside; + private final Point mBottomLeftPoint; + private final Point mBottomLeftPointOutside; + private final Point mBottomRightPoint; + private final Point mBottomRightPointOutside; + + TestPoints(@NonNull Size taskSize, int cornerRadius) { + // Point just inside corner square is included. + mTopLeftPoint = new Point(-cornerRadius + 1, -cornerRadius + 1); + // Point just outside corner square is excluded. + mTopLeftPointOutside = new Point(mTopLeftPoint.x - 5, mTopLeftPoint.y - 5); + + mTopRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1, -cornerRadius + 1); + mTopRightPointOutside = new Point(mTopRightPoint.x + 5, mTopRightPoint.y - 5); + + mBottomLeftPoint = new Point(-cornerRadius + 1, + taskSize.getHeight() + cornerRadius - 1); + mBottomLeftPointOutside = new Point(mBottomLeftPoint.x - 5, mBottomLeftPoint.y + 5); + + mBottomRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1, + taskSize.getHeight() + cornerRadius - 1); + mBottomRightPointOutside = new Point(mBottomRightPoint.x + 5, mBottomRightPoint.y + 5); + } + + /** + * Validates that all test points are either within or without the given region. + */ + public void validateRegion(@NonNull Region region) { + // Point just inside corner square is included. + assertThat(region.contains(mTopLeftPoint.x, mTopLeftPoint.y)).isTrue(); + // Point just outside corner square is excluded. + assertThat(region.contains(mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isFalse(); + + assertThat(region.contains(mTopRightPoint.x, mTopRightPoint.y)).isTrue(); + assertThat( + region.contains(mTopRightPointOutside.x, mTopRightPointOutside.y)).isFalse(); + + assertThat(region.contains(mBottomLeftPoint.x, mBottomLeftPoint.y)).isTrue(); + assertThat(region.contains(mBottomLeftPointOutside.x, + mBottomLeftPointOutside.y)).isFalse(); + + assertThat(region.contains(mBottomRightPoint.x, mBottomRightPoint.y)).isTrue(); + assertThat(region.contains(mBottomRightPointOutside.x, + mBottomRightPointOutside.y)).isFalse(); + } + + /** + * Validates that all test points within this drag corner size give the correct + * {@code @DragPositioningCallback.CtrlType}. + */ + public void validateCtrlTypeForInnerPoints(@NonNull DragResizeWindowGeometry geometry, + boolean isTouch, boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPoint.x, + mTopLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mTopRightPoint.x, + mTopRightPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPoint.x, + mBottomLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPoint.x, + mBottomRightPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + } + + /** + * Validates that all test points outside this drag corner size give the correct + * {@code @DragPositioningCallback.CtrlType}. + */ + public void validateCtrlTypeForOutsidePoints(@NonNull DragResizeWindowGeometry geometry, + boolean isTouch, boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPointOutside.x, + mTopLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mTopRightPointOutside.x, + mTopRightPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPointOutside.x, + mBottomLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPointOutside.x, + mBottomRightPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index de6903d9a06a..9174556d091b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -2,6 +2,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -11,6 +12,7 @@ import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl import android.view.WindowManager +import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING @@ -41,6 +43,8 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn import java.util.function.Supplier +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever /** @@ -125,8 +129,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration, mockDisplayController, mockDragStartListener, - mockTransactionFactory, - DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT + mockTransactionFactory ) } @@ -577,28 +580,29 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.right.toFloat() + 5 - val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockTransitions, never()).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - }}, eq(taskPositioner)) + fun testStartAnimation_useEndRelOffset() { + val mockTransitionInfo = mock(TransitionInfo::class.java) + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(SurfaceControl.Transaction::class.java) + val finishTransaction = mock(SurfaceControl.Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(finishTransaction) + + taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction, + finishTransaction, { _ -> }) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset } private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { @@ -656,70 +660,6 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_taskPositionedInStableBounds() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.left.toFloat() - val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - // Verify task's top bound is set to stable bounds top since dragged outside stable bounds - // but not in disallowed end bounds area. - verify(mockTransitions).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - STABLE_BOUNDS_LANDSCAPE.top - }}, eq(taskPositioner)) - } - - @Test - fun testDragResize_drag_taskPositionedInValidDragArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = VALID_DRAG_AREA.left - 500f - val newY = VALID_DRAG_AREA.bottom + 500f - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - verify(mockTransitions).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - VALID_DRAG_AREA.bottom && - change.configuration.windowConfiguration.bounds.left == - VALID_DRAG_AREA.left - }}, eq(taskPositioner)) - } - - @Test fun testDragResize_drag_updatesStableBoundsOnRotate() { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt new file mode 100644 index 000000000000..847c2dd77d0a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.WindowlessWindowManager +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever + + +/** + * Tests for [ResizeVeil]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ResizeVeilTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ResizeVeilTest : ShellTestCase() { + + @Mock + private lateinit var mockDisplayController: DisplayController + @Mock + private lateinit var mockAppIcon: Drawable + @Mock + private lateinit var mockDisplay: Display + @Mock + private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost + @Mock + private lateinit var mockSurfaceControlBuilderFactory: ResizeVeil.SurfaceControlBuilderFactory + @Mock + private lateinit var mockSurfaceControlViewHostFactory: SurfaceControlViewHostFactory + @Spy + private val spyResizeVeilSurfaceBuilder = SurfaceControl.Builder() + @Mock + private lateinit var mockResizeVeilSurface: SurfaceControl + @Spy + private val spyBackgroundSurfaceBuilder = SurfaceControl.Builder() + @Mock + private lateinit var mockBackgroundSurface: SurfaceControl + @Spy + private val spyIconSurfaceBuilder = SurfaceControl.Builder() + @Mock + private lateinit var mockIconSurface: SurfaceControl + @Mock + private lateinit var mockTransaction: SurfaceControl.Transaction + + private val taskInfo = TestRunningTaskInfoBuilder().build() + + @Before + fun setUp() { + whenever(mockSurfaceControlViewHostFactory.create(any(), any(), any(), any())) + .thenReturn(mockSurfaceControlViewHost) + whenever(mockSurfaceControlBuilderFactory + .create("Resize veil of Task=" + taskInfo.taskId)) + .thenReturn(spyResizeVeilSurfaceBuilder) + doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build() + whenever(mockSurfaceControlBuilderFactory + .create(eq("Resize veil background of Task=" + taskInfo.taskId), any())) + .thenReturn(spyBackgroundSurfaceBuilder) + doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build() + whenever(mockSurfaceControlBuilderFactory + .create("Resize veil icon of Task=" + taskInfo.taskId)) + .thenReturn(spyIconSurfaceBuilder) + doReturn(mockIconSurface).whenever(spyIconSurfaceBuilder).build() + } + + @Test + fun init_displayAvailable_viewHostCreated() { + createResizeVeil(withDisplayAvailable = true) + + verify(mockSurfaceControlViewHostFactory) + .create(any(), eq(mockDisplay), any(), eq("ResizeVeil")) + } + + @Test + fun init_displayUnavailable_viewHostNotCreatedUntilDisplayAppears() { + createResizeVeil(withDisplayAvailable = false) + + verify(mockSurfaceControlViewHostFactory, never()) + .create(any(), eq(mockDisplay), any<WindowlessWindowManager>(), eq("ResizeVeil")) + val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java) + verify(mockDisplayController).addDisplayWindowListener(captor.capture()) + + whenever(mockDisplayController.getDisplay(taskInfo.displayId)).thenReturn(mockDisplay) + captor.value.onDisplayAdded(taskInfo.displayId) + + verify(mockSurfaceControlViewHostFactory) + .create(any(), eq(mockDisplay), any(), eq("ResizeVeil")) + verify(mockDisplayController).removeDisplayWindowListener(any()) + } + + @Test + fun dispose_removesDisplayWindowListener() { + createResizeVeil().dispose() + + verify(mockDisplayController).removeDisplayWindowListener(any()) + } + + @Test + fun showVeil() { + val veil = createResizeVeil() + val tx = mock<SurfaceControl.Transaction>() + + veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + + verify(tx).show(mockResizeVeilSurface) + verify(tx).show(mockBackgroundSurface) + verify(tx).show(mockIconSurface) + verify(tx).apply() + } + + @Test + fun showVeil_displayUnavailable_doesNotShow() { + val veil = createResizeVeil(withDisplayAvailable = false) + val tx = mock<SurfaceControl.Transaction>() + + veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + + verify(tx, never()).show(mockResizeVeilSurface) + verify(tx, never()).show(mockBackgroundSurface) + verify(tx, never()).show(mockIconSurface) + verify(tx).apply() + } + + @Test + fun showVeil_alreadyVisible_doesNotShowAgain() { + val veil = createResizeVeil() + val tx = mock<SurfaceControl.Transaction>() + + veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + + verify(tx, times(1)).show(mockResizeVeilSurface) + verify(tx, times(1)).show(mockBackgroundSurface) + verify(tx, times(1)).show(mockIconSurface) + verify(tx, times(2)).apply() + } + + @Test + fun showVeil_reparentsVeilToNewParent() { + val veil = createResizeVeil(parent = mock()) + val tx = mock<SurfaceControl.Transaction>() + + val newParent = mock<SurfaceControl>() + veil.showVeil(tx, newParent, Rect(0, 0, 100, 100), false /* fadeIn */) + + verify(tx).reparent(mockResizeVeilSurface, newParent) + } + + @Test + fun hideVeil_alreadyHidden_doesNothing() { + val veil = createResizeVeil() + + veil.hideVeil() + + verifyZeroInteractions(mockTransaction) + } + + private fun createResizeVeil( + withDisplayAvailable: Boolean = true, + parent: SurfaceControl = mock() + ): ResizeVeil { + whenever(mockDisplayController.getDisplay(taskInfo.displayId)) + .thenReturn(if (withDisplayAvailable) mockDisplay else null) + return ResizeVeil( + context, + mockDisplayController, + mockAppIcon, + taskInfo, + parent, + { mockTransaction }, + mockSurfaceControlBuilderFactory, + mockSurfaceControlViewHostFactory + ) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 86253f35a51d..a9f44929fc64 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -25,6 +26,7 @@ import android.view.Surface.ROTATION_0 import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.WindowContainerToken @@ -39,6 +41,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import java.util.function.Supplier import junit.framework.Assert import org.junit.Before import org.junit.Test @@ -47,13 +50,13 @@ import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import java.util.function.Supplier import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations /** * Tests for [VeiledResizeTaskPositioner]. @@ -138,8 +141,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDisplayController, mockDragStartListener, mockTransactionFactory, - mockTransitions, - DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT + mockTransitions ) } @@ -355,68 +357,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_taskPositionedInStableBounds() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.left.toFloat() - val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - // Verify task's top bound is set to stable bounds top since dragged outside stable bounds - // but not in disallowed end bounds area. - verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - STABLE_BOUNDS_LANDSCAPE.top }}, - eq(taskPositioner)) - } - - @Test - fun testDragResize_drag_taskPositionedInValidDragArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = VALID_DRAG_AREA.left - 500f - val newY = VALID_DRAG_AREA.bottom + 500f - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - VALID_DRAG_AREA.bottom && - change.configuration.windowConfiguration.bounds.left == - VALID_DRAG_AREA.left }}, - eq(taskPositioner)) - } - - @Test fun testDragResize_drag_updatesStableBoundsOnRotate() { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), @@ -502,6 +442,40 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { Assert.assertFalse(taskPositioner.isResizingOrAnimating) } + @Test + fun testStartAnimation_useEndRelOffset() { + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(Transaction::class.java) + val finishTransaction = mock(Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(finishTransaction) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + startTransaction, + finishTransaction, + mockFinishCallback + ) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + private fun performDrag( startX: Float, startY: Float, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 228b25ccb1ba..546493746da6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -61,7 +61,6 @@ import android.view.InsetsState; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; -import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowManager.LayoutParams; import android.window.SurfaceSyncGroup; @@ -252,16 +251,14 @@ public class WindowDecorationTests extends ShellTestCase { argThat(lp -> lp.height == 64 && lp.width == 300 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); - if (ViewRootImpl.CAPTION_ON_SHELL) { - verify(mMockView).setTaskFocusState(true); - verify(mMockWindowContainerTransaction).addInsetsSource( - eq(taskInfo.token), - any(), - eq(0 /* index */), - eq(WindowInsets.Type.captionBar()), - eq(new Rect(100, 300, 400, 364)), - any()); - } + verify(mMockView).setTaskFocusState(true); + verify(mMockWindowContainerTransaction).addInsetsSource( + eq(taskInfo.token), + any(), + eq(0 /* index */), + eq(WindowInsets.Type.captionBar()), + eq(new Rect(100, 300, 400, 364)), + any()); verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp index d7b5914130ee..839c7b6fef37 100644 --- a/libs/androidfw/ZipFileRO.cpp +++ b/libs/androidfw/ZipFileRO.cpp @@ -119,30 +119,41 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const * appear to be bogus. */ bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32) const +{ + return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen, + pCrc32, nullptr); +} + +bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, - uint32_t* pModWhen, uint32_t* pCrc32) const + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const { const _ZipEntryRO* zipEntry = reinterpret_cast<_ZipEntryRO*>(entry); const ZipEntry& ze = zipEntry->entry; - if (pMethod != NULL) { + if (pMethod != nullptr) { *pMethod = ze.method; } - if (pUncompLen != NULL) { + if (pUncompLen != nullptr) { *pUncompLen = ze.uncompressed_length; } - if (pCompLen != NULL) { + if (pCompLen != nullptr) { *pCompLen = ze.compressed_length; } - if (pOffset != NULL) { + if (pOffset != nullptr) { *pOffset = ze.offset; } - if (pModWhen != NULL) { + if (pModWhen != nullptr) { *pModWhen = ze.mod_time; } - if (pCrc32 != NULL) { + if (pCrc32 != nullptr) { *pCrc32 = ze.crc32; } + if (pExtraFieldSize != nullptr) { + *pExtraFieldSize = ze.extra_field_size; + } return true; } @@ -304,9 +315,13 @@ bool ZipFileRO::uncompressEntry(ZipEntryRO entry, int fd) const _ZipEntryRO *zipEntry = reinterpret_cast<_ZipEntryRO*>(entry); const int32_t error = ExtractEntryToFile(mHandle, &(zipEntry->entry), fd); if (error) { - ALOGW("ExtractToMemory failed with %s", ErrorCodeString(error)); + ALOGW("ExtractToFile failed with %s", ErrorCodeString(error)); return false; } return true; } + +const char* ZipFileRO::getZipFileName() { + return mFileName; +} diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp new file mode 100644 index 000000000000..4b008a7b4815 --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp @@ -0,0 +1,51 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_libs_androidfw_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_libs_androidfw_license"], +} + +cc_fuzz { + name: "resxmlparser_fuzzer", + srcs: [ + "resxmlparser_fuzzer.cpp", + ], + host_supported: true, + + static_libs: ["libgmock"], + target: { + android: { + shared_libs: [ + "libandroidfw", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + host: { + static_libs: [ + "libandroidfw", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + darwin: { + // libbinder is not supported on mac + enabled: false, + }, + }, + + include_dirs: [ + "system/incremental_delivery/incfs/util/include/", + ], + + corpus: ["testdata/*"], + dictionary: "xmlparser_fuzzer.dict", +} diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp new file mode 100644 index 000000000000..829a39617012 --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <memory> +#include <cstdint> +#include <cstddef> +#include <fuzzer/FuzzedDataProvider.h> +#include "androidfw/ResourceTypes.h" + +static void populateDynamicRefTableWithFuzzedData( + android::DynamicRefTable& table, + FuzzedDataProvider& fuzzedDataProvider) { + + const size_t numMappings = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 5); + for (size_t i = 0; i < numMappings; ++i) { + const uint8_t packageId = fuzzedDataProvider.ConsumeIntegralInRange<uint8_t>(0x02, 0x7F); + + // Generate a package name + std::string packageName; + size_t packageNameLength = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 128); + for (size_t j = 0; j < packageNameLength; ++j) { + // Consume characters only in the ASCII range (0x20 to 0x7E) to ensure valid UTF-8 + char ch = fuzzedDataProvider.ConsumeIntegralInRange<char>(0x20, 0x7E); + packageName.push_back(ch); + } + + // Convert std::string to String16 for compatibility + android::String16 androidPackageName(packageName.c_str(), packageName.length()); + + // Add the mapping to the table + table.addMapping(androidPackageName, packageId); + } +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + FuzzedDataProvider fuzzedDataProvider(data, size); + + auto dynamic_ref_table = std::make_shared<android::DynamicRefTable>(); + + // Populate the DynamicRefTable with fuzzed data + populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider); + + auto tree = android::ResXMLTree(std::move(dynamic_ref_table)); + + std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>(); + if (tree.setTo(xmlData.data(), xmlData.size()) != android::NO_ERROR) { + return 0; // Exit early if unable to parse XML data + } + + tree.restart(); + + size_t len = 0; + auto code = tree.next(); + if (code == android::ResXMLParser::START_TAG) { + // Access element name + auto name = tree.getElementName(&len); + + // Access attributes of the current element + for (size_t i = 0; i < tree.getAttributeCount(); i++) { + // Access attribute name + auto attrName = tree.getAttributeName(i, &len); + } + } else if (code == android::ResXMLParser::TEXT) { + const auto text = tree.getText(&len); + } + return 0; // Non-zero return values are reserved for future use. +} diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml new file mode 100644 index 000000000000..417fec72be6a --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <child id="1"> + <subchild type="A">Content A</subchild> + <subchild type="B">Content B</subchild> + </child> + <child id="2" extra="data"> + <subchild type="C">Content C</subchild> + </child> +</root> diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml new file mode 100644 index 000000000000..7e13db536fc9 --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <child1>Value 1</child1> + <child2>Value 2</child2> +</root> diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml new file mode 100644 index 000000000000..90cdf3513be9 --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<root> + <!-- Example with special characters and CDATA --> + <data><![CDATA[Some <encoded> data & other "special" characters]]></data> + <message>Hello & Welcome!</message> +</root> diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict new file mode 100644 index 000000000000..745ded4810f3 --- /dev/null +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict @@ -0,0 +1,11 @@ +root_tag=<root> +child_tag=<child> +end_child_tag=</child> +id_attr=id=" +type_attr=type=" +cdata_start=<![CDATA[ +cdata_end=]]> +ampersand_entity=& +xml_header=<?xml version="1.0" encoding="UTF-8"?> +comment_start=<!-- +comment_end= --> diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h index be1f98f4843d..f7c5007c80d2 100644 --- a/libs/androidfw/include/androidfw/ZipFileRO.h +++ b/libs/androidfw/include/androidfw/ZipFileRO.h @@ -151,6 +151,10 @@ public: uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, uint32_t* pCrc32) const; + bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const; + /* * Create a new FileMap object that maps a subset of the archive. For * an uncompressed entry this effectively provides a pointer to the @@ -187,6 +191,8 @@ public: */ bool uncompressEntry(ZipEntryRO entry, int fd) const; + const char* getZipFileName(); + ~ZipFileRO(); private: diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp index 4dafd0aa6df4..42547832133b 100644 --- a/libs/dream/lowlight/tests/Android.bp +++ b/libs/dream/lowlight/tests/Android.bp @@ -27,7 +27,7 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", - "animationlib", + "//frameworks/libs/systemui:animationlib", "frameworks-base-testutils", "junit", "kotlinx_coroutines_test", diff --git a/libs/hostgraphics/ANativeWindow.cpp b/libs/hostgraphics/ANativeWindow.cpp new file mode 100644 index 000000000000..fcfaf0235293 --- /dev/null +++ b/libs/hostgraphics/ANativeWindow.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <system/window.h> + +static int32_t query(ANativeWindow* window, int what) { + int value; + int res = window->query(window, what, &value); + return res < 0 ? res : value; +} + +static int64_t query64(ANativeWindow* window, int what) { + int64_t value; + int res = window->perform(window, what, &value); + return res < 0 ? res : value; +} + +int ANativeWindow_setCancelBufferInterceptor(ANativeWindow* window, + ANativeWindow_cancelBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_CANCEL_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setDequeueBufferInterceptor(ANativeWindow* window, + ANativeWindow_dequeueBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setQueueBufferInterceptor(ANativeWindow* window, + ANativeWindow_queueBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_QUEUE_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setPerformInterceptor(ANativeWindow* window, + ANativeWindow_performInterceptor interceptor, void* data) { + return window->perform(window, NATIVE_WINDOW_SET_PERFORM_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, int* fenceFd) { + return window->dequeueBuffer(window, buffer, fenceFd); +} + +int ANativeWindow_cancelBuffer(ANativeWindow* window, ANativeWindowBuffer* buffer, int fenceFd) { + return window->cancelBuffer(window, buffer, fenceFd); +} + +int ANativeWindow_setDequeueTimeout(ANativeWindow* window, int64_t timeout) { + return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_TIMEOUT, timeout); +} + +// extern "C", so that it can be used outside libhostgraphics (in host hwui/.../CanvasContext.cpp) +extern "C" void ANativeWindow_tryAllocateBuffers(ANativeWindow* window) { + if (!window || !query(window, NATIVE_WINDOW_IS_VALID)) { + return; + } + window->perform(window, NATIVE_WINDOW_ALLOCATE_BUFFERS); +} + +int64_t ANativeWindow_getLastDequeueStartTime(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_START); +} + +int64_t ANativeWindow_getLastDequeueDuration(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_DURATION); +} + +int64_t ANativeWindow_getLastQueueDuration(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_QUEUE_DURATION); +} + +int32_t ANativeWindow_getWidth(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_WIDTH); +} + +int32_t ANativeWindow_getHeight(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_HEIGHT); +} + +int32_t ANativeWindow_getFormat(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_FORMAT); +} + +void ANativeWindow_acquire(ANativeWindow* window) { + // incStrong/decStrong token must be the same, doesn't matter what it is + window->incStrong((void*)ANativeWindow_acquire); +} + +void ANativeWindow_release(ANativeWindow* window) { + // incStrong/decStrong token must be the same, doesn't matter what it is + window->decStrong((void*)ANativeWindow_acquire); +} diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index 4407af68de99..09232b64616d 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -17,26 +17,18 @@ cc_library_host_static { static_libs: [ "libbase", "libmath", + "libui-types", "libutils", ], srcs: [ - ":libui_host_common", "ADisplay.cpp", + "ANativeWindow.cpp", "Fence.cpp", "HostBufferQueue.cpp", "PublicFormat.cpp", ], - include_dirs: [ - // Here we override all the headers automatically included with frameworks/native/include. - // When frameworks/native/include will be removed from the list of automatic includes. - // We will have to copy necessary headers with a pre-build step (generated headers). - ".", - "frameworks/native/libs/arect/include", - "frameworks/native/libs/ui/include_private", - ], - header_libs: [ "libnativebase_headers", "libnativedisplay_headers", diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h index 2573931c8543..36d8fba0d61a 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/gui/Surface.h @@ -52,6 +52,8 @@ public: virtual void destroy() {} + int getBuffersDataSpace() { return 0; } + protected: virtual ~Surface() {} diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 54f94f5c4b14..753a69960b4c 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -1,4 +1,5 @@ package { + default_team: "trendy_team_android_core_graphics_stack", default_applicable_licenses: ["frameworks_base_libs_hwui_license"], } @@ -31,6 +32,7 @@ license { aconfig_declarations { name: "hwui_flags", package: "com.android.graphics.hwui.flags", + container: "system", srcs: [ "aconfig/hwui_flags.aconfig", ], @@ -78,13 +80,13 @@ cc_defaults { include_dirs: [ "external/skia/include/private", "external/skia/src/core", + "external/skia/src/utils", ], target: { android: { include_dirs: [ "external/skia/src/image", - "external/skia/src/utils", "external/skia/src/gpu", "external/skia/src/shaders", ], @@ -92,9 +94,11 @@ cc_defaults { host: { include_dirs: [ "external/vulkan-headers/include", + "frameworks/av/media/ndk/include", ], cflags: [ "-Wno-unused-variable", + "-D__INTRODUCED_IN(n)=", ], }, }, @@ -140,7 +144,6 @@ cc_defaults { "libsync", "libui", "aconfig_text_flags_c_lib", - "server_configurable_flags", ], static_libs: [ "libEGL_blobCache", @@ -265,6 +268,7 @@ cc_defaults { cppflags: ["-Wno-conversion-null"], srcs: [ + "apex/android_canvas.cpp", "apex/android_matrix.cpp", "apex/android_paint.cpp", "apex/android_region.cpp", @@ -277,7 +281,6 @@ cc_defaults { android: { srcs: [ // sources that depend on android only libraries "apex/android_bitmap.cpp", - "apex/android_canvas.cpp", "apex/jni_runtime.cpp", ], }, @@ -336,6 +339,8 @@ cc_defaults { "jni/android_graphics_ColorSpace.cpp", "jni/android_graphics_drawable_AnimatedVectorDrawable.cpp", "jni/android_graphics_drawable_VectorDrawable.cpp", + "jni/android_graphics_HardwareRenderer.cpp", + "jni/android_graphics_HardwareBufferRenderer.cpp", "jni/android_graphics_HardwareRendererObserver.cpp", "jni/android_graphics_Matrix.cpp", "jni/android_graphics_Picture.cpp", @@ -345,6 +350,7 @@ cc_defaults { "jni/android_nio_utils.cpp", "jni/android_util_PathParser.cpp", + "jni/AnimatedImageDrawable.cpp", "jni/Bitmap.cpp", "jni/BitmapRegionDecoder.cpp", "jni/BufferUtils.cpp", @@ -418,17 +424,13 @@ cc_defaults { target: { android: { srcs: [ // sources that depend on android only libraries - "jni/AnimatedImageDrawable.cpp", "jni/android_graphics_TextureLayer.cpp", - "jni/android_graphics_HardwareRenderer.cpp", - "jni/android_graphics_HardwareBufferRenderer.cpp", "jni/GIFMovie.cpp", "jni/GraphicsStatsService.cpp", "jni/Movie.cpp", "jni/MovieImpl.cpp", "jni/pdf/PdfDocument.cpp", "jni/pdf/PdfEditor.cpp", - "jni/pdf/PdfRenderer.cpp", "jni/pdf/PdfUtils.cpp", ], shared_libs: [ @@ -447,6 +449,12 @@ cc_defaults { "libstatssocket_lazy", ], }, + linux: { + srcs: ["platform/linux/utils/SharedLib.cpp"], + }, + darwin: { + srcs: ["platform/darwin/utils/SharedLib.cpp"], + }, host: { cflags: [ "-Wno-unused-const-variable", @@ -530,16 +538,24 @@ cc_defaults { "effects/GainmapRenderer.cpp", "pipeline/skia/BackdropFilterDrawable.cpp", "pipeline/skia/HolePunch.cpp", + "pipeline/skia/SkiaCpuPipeline.cpp", "pipeline/skia/SkiaDisplayList.cpp", + "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", "pipeline/skia/StretchMask.cpp", "pipeline/skia/RenderNodeDrawable.cpp", "pipeline/skia/ReorderBarrierDrawables.cpp", "pipeline/skia/TransformCanvas.cpp", + "renderstate/RenderState.cpp", + "renderthread/CanvasContext.cpp", + "renderthread/DrawFrameTask.cpp", "renderthread/Frame.cpp", + "renderthread/RenderEffectCapabilityQuery.cpp", + "renderthread/RenderProxy.cpp", "renderthread/RenderTask.cpp", "renderthread/TimeLord.cpp", "hwui/AnimatedImageDrawable.cpp", + "hwui/AnimatedImageThread.cpp", "hwui/Bitmap.cpp", "hwui/BlurDrawLooper.cpp", "hwui/Canvas.cpp", @@ -548,6 +564,7 @@ cc_defaults { "hwui/MinikinUtils.cpp", "hwui/PaintImpl.cpp", "hwui/Typeface.cpp", + "thread/CommonPool.cpp", "utils/Blur.cpp", "utils/Color.cpp", "utils/LinearAllocator.cpp", @@ -564,8 +581,11 @@ cc_defaults { "FrameInfoVisualizer.cpp", "FrameMetricsReporter.cpp", "Gainmap.cpp", + "HWUIProperties.sysprop", "Interpolator.cpp", "JankTracker.cpp", + "Layer.cpp", + "LayerUpdateQueue.cpp", "LightingInfo.cpp", "Matrix.cpp", "Mesh.cpp", @@ -582,6 +602,7 @@ cc_defaults { "SkiaCanvas.cpp", "SkiaInterpolator.cpp", "Tonemapper.cpp", + "TreeInfo.cpp", "VectorDrawable.cpp", ], @@ -598,43 +619,32 @@ cc_defaults { local_include_dirs: ["platform/android"], srcs: [ - "hwui/AnimatedImageThread.cpp", "pipeline/skia/ATraceMemoryDump.cpp", "pipeline/skia/GLFunctorDrawable.cpp", "pipeline/skia/LayerDrawable.cpp", "pipeline/skia/ShaderCache.cpp", + "pipeline/skia/SkiaGpuPipeline.cpp", "pipeline/skia/SkiaMemoryTracer.cpp", "pipeline/skia/SkiaOpenGLPipeline.cpp", - "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaProfileRenderer.cpp", "pipeline/skia/SkiaVulkanPipeline.cpp", "pipeline/skia/VkFunctorDrawable.cpp", "pipeline/skia/VkInteropFunctorDrawable.cpp", - "renderstate/RenderState.cpp", "renderthread/CacheManager.cpp", - "renderthread/CanvasContext.cpp", - "renderthread/DrawFrameTask.cpp", "renderthread/EglManager.cpp", "renderthread/ReliableSurface.cpp", - "renderthread/RenderEffectCapabilityQuery.cpp", "renderthread/VulkanManager.cpp", "renderthread/VulkanSurface.cpp", - "renderthread/RenderProxy.cpp", "renderthread/RenderThread.cpp", "renderthread/HintSessionWrapper.cpp", "service/GraphicsStatsService.cpp", - "thread/CommonPool.cpp", "utils/GLUtils.cpp", "utils/NdkUtils.cpp", "AutoBackendTextureRelease.cpp", "DeferredLayerUpdater.cpp", "HardwareBitmapUploader.cpp", - "HWUIProperties.sysprop", - "Layer.cpp", - "LayerUpdateQueue.cpp", "ProfileDataContainer.cpp", "Readback.cpp", - "TreeInfo.cpp", "WebViewFunctorManager.cpp", "protos/graphicsstats.proto", ], @@ -652,6 +662,8 @@ cc_defaults { srcs: [ "platform/host/renderthread/CacheManager.cpp", + "platform/host/renderthread/HintSessionWrapper.cpp", + "platform/host/renderthread/ReliableSurface.cpp", "platform/host/renderthread/RenderThread.cpp", "platform/host/ProfileDataContainer.cpp", "platform/host/Readback.cpp", diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp index 078041411a21..8645995e3df1 100644 --- a/libs/hwui/AnimatorManager.cpp +++ b/libs/hwui/AnimatorManager.cpp @@ -90,7 +90,13 @@ void AnimatorManager::pushStaging() { } mCancelAllAnimators = false; } else { - for (auto& animator : mAnimators) { + // create a copy of mAnimators as onAnimatorTargetChanged can erase mAnimators. + FatVector<sp<BaseRenderNodeAnimator>> animators; + animators.reserve(mAnimators.size()); + for (const auto& animator : mAnimators) { + animators.push_back(animator); + } + for (auto& animator : animators) { animator->pushStaging(mAnimationHandle->context()); } } diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp index 32bc122fdc58..af7a49653829 100644 --- a/libs/hwui/DeviceInfo.cpp +++ b/libs/hwui/DeviceInfo.cpp @@ -108,6 +108,10 @@ void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) { get()->mSupportFp16ForHdr = supportFp16ForHdr; } +void DeviceInfo::setSupportRgba10101010ForHdr(bool supportRgba10101010ForHdr) { + get()->mSupportRgba10101010ForHdr = supportRgba10101010ForHdr; +} + void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) { get()->mSupportMixedColorSpaces = supportMixedColorSpaces; } diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index a5a841e07d7a..fb58a69747b3 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -69,6 +69,15 @@ public: return get()->mSupportFp16ForHdr; }; + static void setSupportRgba10101010ForHdr(bool supportRgba10101010ForHdr); + static bool isSupportRgba10101010ForHdr() { + if (!Properties::hdr10bitPlus) { + return false; + } + + return get()->mSupportRgba10101010ForHdr; + }; + static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; @@ -102,6 +111,7 @@ private: int mMaxTextureSize; sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB(); bool mSupportFp16ForHdr = false; + bool mSupportRgba10101010ForHdr = false; bool mSupportMixedColorSpaces = false; SkColorType mWideColorType = SkColorType::kN32_SkColorType; int mDisplaysSize = 0; diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp index 37a7d74330e9..5ef7acdaf0fa 100644 --- a/libs/hwui/Mesh.cpp +++ b/libs/hwui/Mesh.cpp @@ -21,6 +21,8 @@ #include "SafeMath.h" +namespace android { + static size_t min_vcount_for_mode(SkMesh::Mode mode) { switch (mode) { case SkMesh::Mode::kTriangles: @@ -28,6 +30,7 @@ static size_t min_vcount_for_mode(SkMesh::Mode mode) { case SkMesh::Mode::kTriangleStrip: return 3; } + return 1; } // Re-implementation of SkMesh::validate to validate user side that their mesh is valid. @@ -36,29 +39,30 @@ std::tuple<bool, SkString> Mesh::validate() { if (!mMeshSpec) { FAIL_MESH_VALIDATE("MeshSpecification is required."); } - if (mVertexBufferData.empty()) { + if (mBufferData->vertexData().empty()) { FAIL_MESH_VALIDATE("VertexBuffer is required."); } - auto meshStride = mMeshSpec->stride(); - auto meshMode = SkMesh::Mode(mMode); + size_t vertexStride = mMeshSpec->stride(); + size_t vertexCount = mBufferData->vertexCount(); + size_t vertexOffset = mBufferData->vertexOffset(); SafeMath sm; - size_t vsize = sm.mul(meshStride, mVertexCount); - if (sm.add(vsize, mVertexOffset) > mVertexBufferData.size()) { + size_t vertexSize = sm.mul(vertexStride, vertexCount); + if (sm.add(vertexSize, vertexOffset) > mBufferData->vertexData().size()) { FAIL_MESH_VALIDATE( "The vertex buffer offset and vertex count reads beyond the end of the" " vertex buffer."); } - if (mVertexOffset % meshStride != 0) { + if (vertexOffset % vertexStride != 0) { FAIL_MESH_VALIDATE("The vertex offset (%zu) must be a multiple of the vertex stride (%zu).", - mVertexOffset, meshStride); + vertexOffset, vertexStride); } if (size_t uniformSize = mMeshSpec->uniformSize()) { - if (!mBuilder->fUniforms || mBuilder->fUniforms->size() < uniformSize) { + if (!mUniformBuilder.fUniforms || mUniformBuilder.fUniforms->size() < uniformSize) { FAIL_MESH_VALIDATE("The uniform data is %zu bytes but must be at least %zu.", - mBuilder->fUniforms->size(), uniformSize); + mUniformBuilder.fUniforms->size(), uniformSize); } } @@ -69,29 +73,33 @@ std::tuple<bool, SkString> Mesh::validate() { case SkMesh::Mode::kTriangleStrip: return "triangle-strip"; } + return "unknown"; }; - if (!mIndexBufferData.empty()) { - if (mIndexCount < min_vcount_for_mode(meshMode)) { + + size_t indexCount = mBufferData->indexCount(); + size_t indexOffset = mBufferData->indexOffset(); + if (!mBufferData->indexData().empty()) { + if (indexCount < min_vcount_for_mode(mMode)) { FAIL_MESH_VALIDATE("%s mode requires at least %zu indices but index count is %zu.", - modeToStr(meshMode), min_vcount_for_mode(meshMode), mIndexCount); + modeToStr(mMode), min_vcount_for_mode(mMode), indexCount); } - size_t isize = sm.mul(sizeof(uint16_t), mIndexCount); - if (sm.add(isize, mIndexOffset) > mIndexBufferData.size()) { + size_t isize = sm.mul(sizeof(uint16_t), indexCount); + if (sm.add(isize, indexOffset) > mBufferData->indexData().size()) { FAIL_MESH_VALIDATE( "The index buffer offset and index count reads beyond the end of the" " index buffer."); } // If we allow 32 bit indices then this should enforce 4 byte alignment in that case. - if (!SkIsAlign2(mIndexOffset)) { + if (!SkIsAlign2(indexOffset)) { FAIL_MESH_VALIDATE("The index offset must be a multiple of 2."); } } else { - if (mVertexCount < min_vcount_for_mode(meshMode)) { + if (vertexCount < min_vcount_for_mode(mMode)) { FAIL_MESH_VALIDATE("%s mode requires at least %zu vertices but vertex count is %zu.", - modeToStr(meshMode), min_vcount_for_mode(meshMode), mVertexCount); + modeToStr(mMode), min_vcount_for_mode(mMode), vertexCount); } - LOG_ALWAYS_FATAL_IF(mIndexCount != 0); - LOG_ALWAYS_FATAL_IF(mIndexOffset != 0); + LOG_ALWAYS_FATAL_IF(indexCount != 0); + LOG_ALWAYS_FATAL_IF(indexOffset != 0); } if (!sm.ok()) { @@ -100,3 +108,5 @@ std::tuple<bool, SkString> Mesh::validate() { #undef FAIL_MESH_VALIDATE return {true, {}}; } + +} // namespace android diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h index 69fda34afc78..8c6ca9758479 100644 --- a/libs/hwui/Mesh.h +++ b/libs/hwui/Mesh.h @@ -25,6 +25,8 @@ #include <utility> +namespace android { + class MeshUniformBuilder { public: struct MeshUniform { @@ -103,111 +105,170 @@ private: sk_sp<SkMeshSpecification> fMeshSpec; }; -class Mesh { +// Storage for CPU and GPU copies of the vertex and index data of a mesh. +class MeshBufferData { public: - Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, - std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, - std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) - : mMeshSpec(meshSpec) - , mMode(mode) - , mVertexBufferData(std::move(vertexBufferData)) - , mVertexCount(vertexCount) - , mVertexOffset(vertexOffset) - , mBuilder(std::move(builder)) - , mBounds(bounds) {} - - Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, - std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, - std::vector<uint8_t>&& indexBuffer, jint indexCount, jint indexOffset, - std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) - : mMeshSpec(meshSpec) - , mMode(mode) - , mVertexBufferData(std::move(vertexBufferData)) - , mVertexCount(vertexCount) + MeshBufferData(std::vector<uint8_t> vertexData, int32_t vertexCount, int32_t vertexOffset, + std::vector<uint8_t> indexData, int32_t indexCount, int32_t indexOffset) + : mVertexCount(vertexCount) , mVertexOffset(vertexOffset) - , mIndexBufferData(std::move(indexBuffer)) , mIndexCount(indexCount) , mIndexOffset(indexOffset) - , mBuilder(std::move(builder)) - , mBounds(bounds) {} - - Mesh(Mesh&&) = default; + , mVertexData(std::move(vertexData)) + , mIndexData(std::move(indexData)) {} - Mesh& operator=(Mesh&&) = default; - - [[nodiscard]] std::tuple<bool, SkString> validate(); - - void updateSkMesh(GrDirectContext* context) const { - GrDirectContext::DirectContextID genId = GrDirectContext::DirectContextID(); - if (context) { - genId = context->directContextID(); + void updateBuffers(GrDirectContext* context) const { + GrDirectContext::DirectContextID currentId = context == nullptr + ? GrDirectContext::DirectContextID() + : context->directContextID(); + if (currentId == mSkiaBuffers.fGenerationId && mSkiaBuffers.fVertexBuffer != nullptr) { + // Nothing to update since the Android API does not support partial updates yet. + return; } - if (mIsDirty || genId != mGenerationId) { - auto vertexData = reinterpret_cast<const void*>(mVertexBufferData.data()); + mSkiaBuffers.fVertexBuffer = #ifdef __ANDROID__ - auto vb = SkMeshes::MakeVertexBuffer(context, - vertexData, - mVertexBufferData.size()); + SkMeshes::MakeVertexBuffer(context, mVertexData.data(), mVertexData.size()); #else - auto vb = SkMeshes::MakeVertexBuffer(vertexData, - mVertexBufferData.size()); + SkMeshes::MakeVertexBuffer(mVertexData.data(), mVertexData.size()); #endif - auto meshMode = SkMesh::Mode(mMode); - if (!mIndexBufferData.empty()) { - auto indexData = reinterpret_cast<const void*>(mIndexBufferData.data()); + if (mIndexCount != 0) { + mSkiaBuffers.fIndexBuffer = #ifdef __ANDROID__ - auto ib = SkMeshes::MakeIndexBuffer(context, - indexData, - mIndexBufferData.size()); + SkMeshes::MakeIndexBuffer(context, mIndexData.data(), mIndexData.size()); #else - auto ib = SkMeshes::MakeIndexBuffer(indexData, - mIndexBufferData.size()); + SkMeshes::MakeIndexBuffer(mIndexData.data(), mIndexData.size()); #endif - mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset, - ib, mIndexCount, mIndexOffset, mBuilder->fUniforms, - SkSpan<SkRuntimeEffect::ChildPtr>(), mBounds) - .mesh; - } else { - mMesh = SkMesh::Make(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset, - mBuilder->fUniforms, SkSpan<SkRuntimeEffect::ChildPtr>(), - mBounds) - .mesh; - } - mIsDirty = false; - mGenerationId = genId; } + mSkiaBuffers.fGenerationId = currentId; } - SkMesh& getSkMesh() const { - LOG_FATAL_IF(mIsDirty, - "Attempt to obtain SkMesh when Mesh is dirty, did you " - "forget to call updateSkMesh with a GrDirectContext? " - "Defensively creating a CPU mesh"); - return mMesh; - } + SkMesh::VertexBuffer* vertexBuffer() const { return mSkiaBuffers.fVertexBuffer.get(); } + + sk_sp<SkMesh::VertexBuffer> refVertexBuffer() const { return mSkiaBuffers.fVertexBuffer; } + int32_t vertexCount() const { return mVertexCount; } + int32_t vertexOffset() const { return mVertexOffset; } - void markDirty() { mIsDirty = true; } + sk_sp<SkMesh::IndexBuffer> refIndexBuffer() const { return mSkiaBuffers.fIndexBuffer; } + int32_t indexCount() const { return mIndexCount; } + int32_t indexOffset() const { return mIndexOffset; } - MeshUniformBuilder* uniformBuilder() { return mBuilder.get(); } + const std::vector<uint8_t>& vertexData() const { return mVertexData; } + const std::vector<uint8_t>& indexData() const { return mIndexData; } private: - sk_sp<SkMeshSpecification> mMeshSpec; - int mMode = 0; + struct CachedSkiaBuffers { + sk_sp<SkMesh::VertexBuffer> fVertexBuffer; + sk_sp<SkMesh::IndexBuffer> fIndexBuffer; + GrDirectContext::DirectContextID fGenerationId = GrDirectContext::DirectContextID(); + }; + + mutable CachedSkiaBuffers mSkiaBuffers; + int32_t mVertexCount = 0; + int32_t mVertexOffset = 0; + int32_t mIndexCount = 0; + int32_t mIndexOffset = 0; + std::vector<uint8_t> mVertexData; + std::vector<uint8_t> mIndexData; +}; - std::vector<uint8_t> mVertexBufferData; - size_t mVertexCount = 0; - size_t mVertexOffset = 0; +class Mesh { +public: + // A snapshot of the mesh for use by the render thread. + // + // After a snapshot is taken, future uniform changes to the original Mesh will not modify the + // uniforms returned by makeSkMesh. + class Snapshot { + public: + Snapshot() = delete; + Snapshot(const Snapshot&) = default; + Snapshot(Snapshot&&) = default; + Snapshot& operator=(const Snapshot&) = default; + Snapshot& operator=(Snapshot&&) = default; + ~Snapshot() = default; - std::vector<uint8_t> mIndexBufferData; - size_t mIndexCount = 0; - size_t mIndexOffset = 0; + const SkMesh& getSkMesh() const { + SkMesh::VertexBuffer* vertexBuffer = mBufferData->vertexBuffer(); + LOG_FATAL_IF(vertexBuffer == nullptr, + "Attempt to obtain SkMesh when vertexBuffer has not been created, did you " + "forget to call MeshBufferData::updateBuffers with a GrDirectContext?"); + if (vertexBuffer != mMesh.vertexBuffer()) mMesh = makeSkMesh(); + return mMesh; + } - std::unique_ptr<MeshUniformBuilder> mBuilder; - SkRect mBounds{}; + private: + friend class Mesh; - mutable SkMesh mMesh{}; - mutable bool mIsDirty = true; - mutable GrDirectContext::DirectContextID mGenerationId = GrDirectContext::DirectContextID(); + Snapshot(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode, + std::shared_ptr<const MeshBufferData> bufferData, sk_sp<const SkData> uniforms, + const SkRect& bounds) + : mMeshSpec(std::move(meshSpec)) + , mMode(mode) + , mBufferData(std::move(bufferData)) + , mUniforms(std::move(uniforms)) + , mBounds(bounds) {} + + SkMesh makeSkMesh() const { + const MeshBufferData& d = *mBufferData; + if (d.indexCount() != 0) { + return SkMesh::MakeIndexed(mMeshSpec, mMode, d.refVertexBuffer(), d.vertexCount(), + d.vertexOffset(), d.refIndexBuffer(), d.indexCount(), + d.indexOffset(), mUniforms, + SkSpan<SkRuntimeEffect::ChildPtr>(), mBounds) + .mesh; + } + return SkMesh::Make(mMeshSpec, mMode, d.refVertexBuffer(), d.vertexCount(), + d.vertexOffset(), mUniforms, SkSpan<SkRuntimeEffect::ChildPtr>(), + mBounds) + .mesh; + } + + mutable SkMesh mMesh; + sk_sp<SkMeshSpecification> mMeshSpec; + SkMesh::Mode mMode; + std::shared_ptr<const MeshBufferData> mBufferData; + sk_sp<const SkData> mUniforms; + SkRect mBounds; + }; + + Mesh(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode, std::vector<uint8_t> vertexData, + int32_t vertexCount, int32_t vertexOffset, const SkRect& bounds) + : Mesh(std::move(meshSpec), mode, std::move(vertexData), vertexCount, vertexOffset, + /* indexData = */ {}, /* indexCount = */ 0, /* indexOffset = */ 0, bounds) {} + + Mesh(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode, std::vector<uint8_t> vertexData, + int32_t vertexCount, int32_t vertexOffset, std::vector<uint8_t> indexData, + int32_t indexCount, int32_t indexOffset, const SkRect& bounds) + : mMeshSpec(std::move(meshSpec)) + , mMode(mode) + , mBufferData(std::make_shared<MeshBufferData>(std::move(vertexData), vertexCount, + vertexOffset, std::move(indexData), + indexCount, indexOffset)) + , mUniformBuilder(mMeshSpec) + , mBounds(bounds) {} + + Mesh(Mesh&&) = default; + + Mesh& operator=(Mesh&&) = default; + + [[nodiscard]] std::tuple<bool, SkString> validate(); + + std::shared_ptr<const MeshBufferData> refBufferData() const { return mBufferData; } + + Snapshot takeSnapshot() const { + return Snapshot(mMeshSpec, mMode, mBufferData, mUniformBuilder.fUniforms, mBounds); + } + + MeshUniformBuilder* uniformBuilder() { return &mUniformBuilder; } + +private: + sk_sp<SkMeshSpecification> mMeshSpec; + SkMesh::Mode mMode; + std::shared_ptr<MeshBufferData> mBufferData; + MeshUniformBuilder mUniformBuilder; + SkRect mBounds; }; + +} // namespace android + #endif // MESH_H_ diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 755332ff66fd..325bdd63ab22 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -16,10 +16,6 @@ #include "Properties.h" -#include "Debug.h" -#ifdef __ANDROID__ -#include "HWUIProperties.sysprop.h" -#endif #include <android-base/properties.h> #include <cutils/compiler.h> #include <log/log.h> @@ -28,6 +24,8 @@ #include <cstdlib> #include <optional> +#include "Debug.h" +#include "HWUIProperties.sysprop.h" #include "src/core/SkTraceEventCommon.h" #ifdef __ANDROID__ @@ -47,16 +45,6 @@ constexpr bool hdr_10bit_plus() { namespace android { namespace uirenderer { -#ifndef __ANDROID__ // Layoutlib does not compile HWUIProperties.sysprop as it depends on cutils properties -std::optional<bool> use_vulkan() { - return base::GetBoolProperty("ro.hwui.use_vulkan", true); -} - -std::optional<std::int32_t> render_ahead() { - return base::GetIntProperty("ro.hwui.render_ahead", 0); -} -#endif - bool Properties::debugLayersUpdates = false; bool Properties::debugOverdraw = false; bool Properties::debugTraceGpuResourceCategories = false; diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ec53070f6cb8..c1510d96461f 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -242,7 +242,7 @@ enum class ProfileType { None, Console, Bars }; enum class OverdrawColorSet { Default = 0, Deuteranomaly }; -enum class RenderPipelineType { SkiaGL, SkiaVulkan, NotInitialized = 128 }; +enum class RenderPipelineType { SkiaGL, SkiaVulkan, SkiaCpu, NotInitialized = 128 }; enum class StretchEffectBehavior { ShaderHWUI, // Stretch shader in HWUI only, matrix scale in SF diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 54aef55f8b90..d0263798d2c2 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -573,9 +573,9 @@ struct DrawSkMesh final : Op { struct DrawMesh final : Op { static const auto kType = Type::DrawMesh; DrawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint) - : mesh(mesh), blender(std::move(blender)), paint(paint) {} + : mesh(mesh.takeSnapshot()), blender(std::move(blender)), paint(paint) {} - const Mesh& mesh; + Mesh::Snapshot mesh; sk_sp<SkBlender> blender; SkPaint paint; @@ -1296,14 +1296,5 @@ void RecordingCanvas::drawWebView(skiapipeline::FunctorDrawable* drawable) { fDL->drawWebView(drawable); } -[[nodiscard]] const SkMesh& DrawMeshPayload::getSkMesh() const { - LOG_FATAL_IF(!meshWrapper && !mesh, "One of Mesh or Mesh must be non-null"); - if (meshWrapper) { - return meshWrapper->getSkMesh(); - } else { - return *mesh; - } -} - } // namespace uirenderer } // namespace android diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 965264f31119..f86785274224 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -41,11 +41,12 @@ enum class SkBlendMode; class SkRRect; -class Mesh; namespace android { -namespace uirenderer { +class Mesh; + +namespace uirenderer { namespace skiapipeline { class FunctorDrawable; } @@ -68,18 +69,6 @@ struct DisplayListOp { static_assert(sizeof(DisplayListOp) == 4); -class DrawMeshPayload { -public: - explicit DrawMeshPayload(const SkMesh* mesh) : mesh(mesh) {} - explicit DrawMeshPayload(const Mesh* meshWrapper) : meshWrapper(meshWrapper) {} - - [[nodiscard]] const SkMesh& getSkMesh() const; - -private: - const SkMesh* mesh = nullptr; - const Mesh* meshWrapper = nullptr; -}; - struct DrawImagePayload { explicit DrawImagePayload(Bitmap& bitmap) : image(bitmap.makeImage()), palette(bitmap.palette()) { diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index f526a280b113..589abb4d87f4 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -16,18 +16,6 @@ #include "RenderNode.h" -#include "DamageAccumulator.h" -#include "Debug.h" -#include "Properties.h" -#include "TreeInfo.h" -#include "VectorDrawable.h" -#include "private/hwui/WebViewFunctor.h" -#ifdef __ANDROID__ -#include "renderthread/CanvasContext.h" -#else -#include "DamageAccumulator.h" -#include "pipeline/skia/SkiaDisplayList.h" -#endif #include <SkPathOps.h> #include <gui/TraceUtils.h> #include <ui/FatVector.h> @@ -37,6 +25,14 @@ #include <sstream> #include <string> +#include "DamageAccumulator.h" +#include "Debug.h" +#include "Properties.h" +#include "TreeInfo.h" +#include "VectorDrawable.h" +#include "private/hwui/WebViewFunctor.h" +#include "renderthread/CanvasContext.h" + #ifdef __ANDROID__ #include "include/gpu/ganesh/SkImageGanesh.h" #endif @@ -186,7 +182,6 @@ void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) { } void RenderNode::pushLayerUpdate(TreeInfo& info) { -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext and Layers LayerType layerType = properties().effectiveLayerType(); // If we are not a layer OR we cannot be rendered (eg, view was detached) // we need to destroy any Layers we may have had previously @@ -218,7 +213,6 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) { // That might be us, so tell CanvasContext that this layer is in the // tree and should not be destroyed. info.canvasContext.markLayerInUse(this); -#endif } /** diff --git a/libs/hwui/RootRenderNode.cpp b/libs/hwui/RootRenderNode.cpp index ddbbf58b3071..5174e27ae587 100644 --- a/libs/hwui/RootRenderNode.cpp +++ b/libs/hwui/RootRenderNode.cpp @@ -18,11 +18,12 @@ #ifdef __ANDROID__ // Layoutlib does not support Looper (windows) #include <utils/Looper.h> +#else +#include "utils/MessageHandler.h" #endif namespace android::uirenderer { -#ifdef __ANDROID__ // Layoutlib does not support Looper class FinishAndInvokeListener : public MessageHandler { public: explicit FinishAndInvokeListener(PropertyValuesAnimatorSet* anim) : mAnimator(anim) { @@ -237,9 +238,13 @@ void RootRenderNode::detachVectorDrawableAnimator(PropertyValuesAnimatorSet* ani // user events, in which case the already posted listener's id will become stale, and // the onFinished callback will then be ignored. sp<FinishAndInvokeListener> message = new FinishAndInvokeListener(anim); +#ifdef __ANDROID__ // Layoutlib does not support Looper auto looper = Looper::getForThread(); LOG_ALWAYS_FATAL_IF(looper == nullptr, "Not on a looper thread?"); looper->sendMessageDelayed(ms2ns(remainingTimeInMs), message, 0); +#else + message->handleMessage(0); +#endif anim->clearOneShotListener(); } } @@ -285,22 +290,5 @@ private: AnimationContext* ContextFactoryImpl::createAnimationContext(renderthread::TimeLord& clock) { return new AnimationContextBridge(clock, mRootNode); } -#else - -void RootRenderNode::prepareTree(TreeInfo& info) { - info.errorHandler = mErrorHandler.get(); - info.updateWindowPositions = true; - RenderNode::prepareTree(info); - info.updateWindowPositions = false; - info.errorHandler = nullptr; -} - -void RootRenderNode::attachAnimatingNode(RenderNode* animatingNode) { } - -void RootRenderNode::destroy() { } - -void RootRenderNode::addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim) { } - -#endif } // namespace android::uirenderer diff --git a/libs/hwui/RootRenderNode.h b/libs/hwui/RootRenderNode.h index 1d3f5a8a51e0..7a5cda7041ed 100644 --- a/libs/hwui/RootRenderNode.h +++ b/libs/hwui/RootRenderNode.h @@ -74,7 +74,6 @@ private: void detachVectorDrawableAnimator(PropertyValuesAnimatorSet* anim); }; -#ifdef __ANDROID__ // Layoutlib does not support Animations class ContextFactoryImpl : public IContextFactory { public: explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {} @@ -84,6 +83,5 @@ public: private: RootRenderNode* mRootNode; }; -#endif } // namespace android::uirenderer diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 0b739c361d64..72e83afbd96f 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -596,8 +596,8 @@ void SkiaCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Pain if (recordingContext) { context = recordingContext->asDirectContext(); } - mesh.updateSkMesh(context); - mCanvas->drawMesh(mesh.getSkMesh(), blender, paint); + mesh.refBufferData()->updateBuffers(context); + mCanvas->drawMesh(mesh.takeSnapshot().getSkMesh(), blender, paint); } // ---------------------------------------------------------------------------- diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index 2ea4e3f21163..af169f4bc4cd 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -540,7 +540,7 @@ bool Tree::allocateBitmapIfNeeded(Cache& cache, int width, int height) { } bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) { - return bitmap && width <= bitmap->width() && height <= bitmap->height(); + return bitmap && width == bitmap->width() && height == bitmap->height(); } void Tree::onPropertyChanged(TreeProperties* prop) { diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp index 6fc251dc815c..9d16ee86739e 100644 --- a/libs/hwui/WebViewFunctorManager.cpp +++ b/libs/hwui/WebViewFunctorManager.cpp @@ -16,15 +16,16 @@ #include "WebViewFunctorManager.h" +#include <log/log.h> #include <private/hwui/WebViewFunctor.h> +#include <utils/Trace.h> + +#include <atomic> + #include "Properties.h" #include "renderthread/CanvasContext.h" #include "renderthread/RenderThread.h" -#include <log/log.h> -#include <utils/Trace.h> -#include <atomic> - namespace android::uirenderer { namespace { @@ -86,6 +87,10 @@ void WebViewFunctor_release(int functor) { WebViewFunctorManager::instance().releaseFunctor(functor); } +void WebViewFunctor_reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size) { + WebViewFunctorManager::instance().reportRenderingThreads(functor, thread_ids, size); +} + static std::atomic_int sNextId{1}; WebViewFunctor::WebViewFunctor(void* data, const WebViewFunctorCallbacks& callbacks, @@ -260,6 +265,10 @@ void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) { funcs.transactionDeleteFunc(transaction); } +void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) { + mRenderingThreads = std::vector<pid_t>(thread_ids, thread_ids + size); +} + WebViewFunctorManager& WebViewFunctorManager::instance() { static WebViewFunctorManager sInstance; return sInstance; @@ -346,6 +355,32 @@ void WebViewFunctorManager::destroyFunctor(int functor) { } } +void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids, + size_t size) { + std::lock_guard _lock{mLock}; + for (auto& iter : mFunctors) { + if (iter->id() == functor) { + iter->reportRenderingThreads(thread_ids, size); + break; + } + } +} + +std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() { + std::vector<pid_t> renderingThreads; + std::lock_guard _lock{mLock}; + for (const auto& iter : mActiveFunctors) { + const auto& functorThreads = iter->getRenderingThreads(); + for (const auto& tid : functorThreads) { + if (std::find(renderingThreads.begin(), renderingThreads.end(), tid) == + renderingThreads.end()) { + renderingThreads.push_back(tid); + } + } + } + return renderingThreads; +} + sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) { std::lock_guard _lock{mLock}; for (auto& iter : mActiveFunctors) { diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h index 0a02f2d4b720..ec17640f9b5e 100644 --- a/libs/hwui/WebViewFunctorManager.h +++ b/libs/hwui/WebViewFunctorManager.h @@ -17,13 +17,11 @@ #pragma once #include <private/hwui/WebViewFunctor.h> -#ifdef __ANDROID__ // Layoutlib does not support render thread #include <renderthread/RenderProxy.h> -#endif - #include <utils/LightRefBase.h> #include <utils/Log.h> #include <utils/StrongPointer.h> + #include <mutex> #include <vector> @@ -38,11 +36,7 @@ public: class Handle : public LightRefBase<Handle> { public: - ~Handle() { -#ifdef __ANDROID__ // Layoutlib does not support render thread - renderthread::RenderProxy::destroyFunctor(id()); -#endif - } + ~Handle() { renderthread::RenderProxy::destroyFunctor(id()); } int id() const { return mReference.id(); } @@ -60,6 +54,10 @@ public: void onRemovedFromTree() { mReference.onRemovedFromTree(); } + const std::vector<pid_t>& getRenderingThreads() const { + return mReference.getRenderingThreads(); + } + private: friend class WebViewFunctor; @@ -81,6 +79,9 @@ public: ASurfaceControl* getSurfaceControl(); void mergeTransaction(ASurfaceTransaction* transaction); + void reportRenderingThreads(const pid_t* thread_ids, size_t size); + const std::vector<pid_t>& getRenderingThreads() const { return mRenderingThreads; } + sp<Handle> createHandle() { LOG_ALWAYS_FATAL_IF(mCreatedHandle); mCreatedHandle = true; @@ -100,6 +101,7 @@ private: bool mCreatedHandle = false; int32_t mParentSurfaceControlGenerationId = 0; ASurfaceControl* mSurfaceControl = nullptr; + std::vector<pid_t> mRenderingThreads; }; class WebViewFunctorManager { @@ -110,6 +112,8 @@ public: void releaseFunctor(int functor); void onContextDestroyed(); void destroyFunctor(int functor); + void reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size); + std::vector<pid_t> getRenderingThreadsForActiveFunctors(); sp<WebViewFunctor::Handle> handleFor(int functor); diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 3d7e559bebe0..50f8b3929e1e 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -1,7 +1,9 @@ package: "com.android.graphics.hwui.flags" +container: "system" flag { name: "clip_shader" + is_exported: true namespace: "core_graphics" description: "API for canvas shader clipping operations" bug: "280116960" @@ -9,6 +11,7 @@ flag { flag { name: "matrix_44" + is_exported: true namespace: "core_graphics" description: "API for 4x4 matrix and related canvas functions" bug: "280116960" @@ -16,6 +19,7 @@ flag { flag { name: "limited_hdr" + is_exported: true namespace: "core_graphics" description: "API to enable apps to restrict the amount of HDR headroom that is used" bug: "234181960" @@ -44,6 +48,7 @@ flag { flag { name: "gainmap_animations" + is_exported: true namespace: "core_graphics" description: "APIs to help enable animations involving gainmaps" bug: "296482289" @@ -51,6 +56,7 @@ flag { flag { name: "gainmap_constructor_with_metadata" + is_exported: true namespace: "core_graphics" description: "APIs to create a new gainmap with a bitmap for metadata." bug: "304478551" @@ -65,6 +71,7 @@ flag { flag { name: "requested_formats_v" + is_exported: true namespace: "core_graphics" description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK" bug: "292545615" @@ -76,3 +83,10 @@ flag { description: "Automatically animate all changes in HDR headroom" bug: "314810174" } + +flag { + name: "draw_region" + namespace: "core_graphics" + description: "Add canvas#drawRegion API" + bug: "318612129" +} diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 770822a049b7..fd9915a54bb5 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -164,8 +164,10 @@ static vector<string> parseCsv(JNIEnv* env, jstring csvJString) { } // namespace android using namespace android; +using namespace android::uirenderer; void init_android_graphics() { + Properties::overrideRenderPipelineType(RenderPipelineType::SkiaCpu); SkGraphics::Init(); } diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index 883f273b5d3d..fb0cdb034575 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -70,7 +70,6 @@ extern int register_android_graphics_fonts_Font(JNIEnv* env); extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env); extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); -extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv *env); extern int register_android_graphics_text_TextShaper(JNIEnv *env); @@ -142,7 +141,6 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); REG_JNI(register_android_graphics_fonts_FontFamily), REG_JNI(register_android_graphics_pdf_PdfDocument), REG_JNI(register_android_graphics_pdf_PdfEditor), - REG_JNI(register_android_graphics_pdf_PdfRenderer), REG_JNI(register_android_graphics_text_MeasuredText), REG_JNI(register_android_graphics_text_LineBreaker), REG_JNI(register_android_graphics_text_TextShaper), diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 27773a60355a..69613c7d17cb 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -15,18 +15,16 @@ */ #include "AnimatedImageDrawable.h" -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread -#include "AnimatedImageThread.h" -#endif - -#include <gui/TraceUtils.h> -#include "pipeline/skia/SkiaUtils.h" #include <SkPicture.h> #include <SkRefCnt.h> +#include <gui/TraceUtils.h> #include <optional> +#include "AnimatedImageThread.h" +#include "pipeline/skia/SkiaUtils.h" + namespace android { AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed, @@ -185,10 +183,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { } else if (starting) { // The image has animated, and now is being reset. Queue up the first // frame, but keep showing the current frame until the first is ready. -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread auto& thread = uirenderer::AnimatedImageThread::getInstance(); mNextSnapshot = thread.reset(sk_ref_sp(this)); -#endif } bool finalFrame = false; @@ -214,10 +210,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { } if (mRunning && !mNextSnapshot.valid()) { -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread auto& thread = uirenderer::AnimatedImageThread::getInstance(); mNextSnapshot = thread.decodeNextFrame(sk_ref_sp(this)); -#endif } if (!drawDirectly) { diff --git a/libs/hwui/hwui/AnimatedImageThread.cpp b/libs/hwui/hwui/AnimatedImageThread.cpp index 825dd4cf2bf1..e39c8d57d31c 100644 --- a/libs/hwui/hwui/AnimatedImageThread.cpp +++ b/libs/hwui/hwui/AnimatedImageThread.cpp @@ -16,7 +16,9 @@ #include "AnimatedImageThread.h" +#ifdef __ANDROID__ #include <sys/resource.h> +#endif namespace android { namespace uirenderer { @@ -31,7 +33,9 @@ AnimatedImageThread& AnimatedImageThread::getInstance() { } AnimatedImageThread::AnimatedImageThread() { +#ifdef __ANDROID__ setpriority(PRIO_PROCESS, 0, PRIORITY_NORMAL + PRIORITY_MORE_FAVORABLE); +#endif } std::future<AnimatedImageDrawable::Snapshot> AnimatedImageThread::decodeNextFrame( diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 14b4f584f0f3..4eb6918d7e9a 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -34,7 +34,6 @@ class SkCanvasState; class SkRRect; class SkRuntimeShaderBuilder; class SkVertices; -class Mesh; namespace minikin { class Font; @@ -61,6 +60,7 @@ typedef std::function<void(uint16_t* text, float* positions)> ReadGlyphFunc; class AnimatedImageDrawable; class Bitmap; +class Mesh; class Paint; struct Typeface; diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index 90b1da846205..b01e38d014a9 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -25,7 +25,11 @@ #include <hwui/AnimatedImageDrawable.h> #include <hwui/Canvas.h> #include <hwui/ImageDecoder.h> +#ifdef __ANDROID__ #include <utils/Looper.h> +#else +#include "utils/MessageHandler.h" +#endif #include "ColorFilter.h" #include "GraphicsJNI.h" @@ -204,6 +208,7 @@ private: }; class JniAnimationEndListener : public OnAnimationEndListener { +#ifdef __ANDROID__ public: JniAnimationEndListener(sp<Looper>&& looper, JNIEnv* env, jobject javaObject) { mListener = new InvokeListener(env, javaObject); @@ -215,6 +220,17 @@ public: private: sp<InvokeListener> mListener; sp<Looper> mLooper; +#else +public: + JniAnimationEndListener(JNIEnv* env, jobject javaObject) { + mListener = new InvokeListener(env, javaObject); + } + + void onAnimationEnd() override { mListener->handleMessage(0); } + +private: + sp<InvokeListener> mListener; +#endif }; static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobject /*clazz*/, @@ -223,6 +239,7 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec if (!jdrawable) { drawable->setOnAnimationEndListener(nullptr); } else { +#ifdef __ANDROID__ sp<Looper> looper = Looper::getForThread(); if (!looper.get()) { doThrowISE(env, @@ -233,6 +250,10 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec drawable->setOnAnimationEndListener( std::make_unique<JniAnimationEndListener>(std::move(looper), env, jdrawable)); +#else + drawable->setOnAnimationEndListener( + std::make_unique<JniAnimationEndListener>(env, jdrawable)); +#endif } } diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 9e21f860ce21..d4157008ca46 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -1,8 +1,14 @@ // #define LOG_NDEBUG 0 #include "Bitmap.h" +#include <android-base/unique_fd.h> #include <hwui/Bitmap.h> #include <hwui/Paint.h> +#include <inttypes.h> +#include <renderthread/RenderProxy.h> +#include <string.h> + +#include <memory> #include "CreateJavaOutputStreamAdaptor.h" #include "Gainmap.h" @@ -24,16 +30,6 @@ #include "SkTypes.h" #include "android_nio_utils.h" -#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread -#include <android-base/unique_fd.h> -#include <renderthread/RenderProxy.h> -#endif - -#include <inttypes.h> -#include <string.h> - -#include <memory> - #define DEBUG_PARCEL 0 static jclass gBitmap_class; @@ -1105,11 +1101,9 @@ static jboolean Bitmap_sameAs(JNIEnv* env, jobject, jlong bm0Handle, jlong bm1Ha } static void Bitmap_prepareToDraw(JNIEnv* env, jobject, jlong bitmapPtr) { -#ifdef __ANDROID__ // Layoutlib does not support render thread LocalScopedBitmap bitmapHandle(bitmapPtr); if (!bitmapHandle.valid()) return; android::uirenderer::renderthread::RenderProxy::prepareToDraw(bitmapHandle->bitmap()); -#endif } static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) { diff --git a/libs/hwui/jni/HardwareBufferHelpers.cpp b/libs/hwui/jni/HardwareBufferHelpers.cpp index 7e3f771b6b3d..d3b48d36b677 100644 --- a/libs/hwui/jni/HardwareBufferHelpers.cpp +++ b/libs/hwui/jni/HardwareBufferHelpers.cpp @@ -16,7 +16,9 @@ #include "HardwareBufferHelpers.h" +#ifdef __ANDROID__ #include <dlfcn.h> +#endif #include <log/log.h> #ifdef __ANDROID__ diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp index 426644ee6a4e..948362c30a31 100644 --- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp +++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp @@ -16,22 +16,19 @@ #include "GraphicsJNI.h" -#ifdef __ANDROID__ // Layoutlib does not support Looper and device properties +#ifdef __ANDROID__ // Layoutlib does not support Looper #include <utils/Looper.h> #endif -#include <SkRegion.h> -#include <SkRuntimeEffect.h> - +#include <CanvasProperty.h> #include <Rect.h> #include <RenderNode.h> -#include <CanvasProperty.h> +#include <SkRegion.h> +#include <SkRuntimeEffect.h> #include <hwui/Canvas.h> #include <hwui/Paint.h> #include <minikin/Layout.h> -#ifdef __ANDROID__ // Layoutlib does not support RenderThread #include <renderthread/RenderProxy.h> -#endif namespace android { @@ -85,11 +82,7 @@ static void android_view_DisplayListCanvas_resetDisplayListCanvas(CRITICAL_JNI_P } static jint android_view_DisplayListCanvas_getMaxTextureSize(JNIEnv*, jobject) { -#ifdef __ANDROID__ // Layoutlib does not support RenderProxy (RenderThread) return android::uirenderer::renderthread::RenderProxy::maxTextureSize(); -#else - return 4096; -#endif } static void android_view_DisplayListCanvas_enableZ(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index d15b1680de94..df9f83036709 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -25,13 +25,16 @@ #include <SkColorSpace.h> #include <SkData.h> #include <SkImage.h> +#ifdef __ANDROID__ #include <SkImageAndroid.h> +#else +#include <SkImagePriv.h> +#endif #include <SkPicture.h> #include <SkPixmap.h> #include <SkSerialProcs.h> #include <SkStream.h> #include <SkTypeface.h> -#include <dlfcn.h> #include <gui/TraceUtils.h> #include <include/encode/SkPngEncoder.h> #include <inttypes.h> @@ -39,8 +42,10 @@ #include <media/NdkImage.h> #include <media/NdkImageReader.h> #include <nativehelper/JNIPlatformHelp.h> +#ifdef __ANDROID__ #include <pipeline/skia/ShaderCache.h> #include <private/EGL/cache.h> +#endif #include <renderthread/CanvasContext.h> #include <renderthread/RenderProxy.h> #include <renderthread/RenderTask.h> @@ -59,6 +64,7 @@ #include "JvmErrorReporter.h" #include "android_graphics_HardwareRendererObserver.h" #include "utils/ForceDark.h" +#include "utils/SharedLib.h" namespace android { @@ -498,7 +504,11 @@ public: return sk_ref_sp(img); } bm.setImmutable(); +#ifdef __ANDROID__ return SkImages::PinnableRasterFromBitmap(bm); +#else + return SkMakeImageFromRasterBitmap(bm, kNever_SkCopyPixelsMode); +#endif } return sk_ref_sp(img); } @@ -713,6 +723,7 @@ public: static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode(JNIEnv* env, jobject clazz, jlong renderNodePtr, jint jwidth, jint jheight) { +#ifdef __ANDROID__ RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); if (jwidth <= 0 || jheight <= 0) { ALOGW("Invalid width %d or height %d", jwidth, jheight); @@ -796,6 +807,9 @@ static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode( sk_sp<Bitmap> bitmap = Bitmap::createFrom(buffer, cs); return bitmap::createBitmap(env, bitmap.release(), android::bitmap::kBitmapCreateFlag_Premultiplied); +#else + return nullptr; +#endif } static void android_view_ThreadedRenderer_disableVsync(JNIEnv*, jclass) { @@ -860,7 +874,8 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, static void android_view_ThreadedRenderer_initDisplayInfo( JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate, jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos, - jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) { + jboolean supportFp16ForHdr, jboolean supportRgba10101010ForHdr, + jboolean supportMixedColorSpaces) { DeviceInfo::setWidth(physicalWidth); DeviceInfo::setHeight(physicalHeight); DeviceInfo::setRefreshRate(refreshRate); @@ -868,6 +883,7 @@ static void android_view_ThreadedRenderer_initDisplayInfo( DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr); + DeviceInfo::setSupportRgba10101010ForHdr(supportRgba10101010ForHdr); DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces); } @@ -907,6 +923,7 @@ static void android_view_ThreadedRenderer_removeObserver(JNIEnv* env, jclass cla static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, jobject clazz, jstring diskCachePath, jstring skiaDiskCachePath) { +#ifdef __ANDROID__ const char* cacheArray = env->GetStringUTFChars(diskCachePath, NULL); android::egl_set_cache_filename(cacheArray); env->ReleaseStringUTFChars(diskCachePath, cacheArray); @@ -914,6 +931,7 @@ static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, job const char* skiaCacheArray = env->GetStringUTFChars(skiaDiskCachePath, NULL); uirenderer::skiapipeline::ShaderCache::get().setFilename(skiaCacheArray); env->ReleaseStringUTFChars(skiaDiskCachePath, skiaCacheArray); +#endif } static jboolean android_view_ThreadedRenderer_isWebViewOverlaysEnabled(JNIEnv* env, jobject clazz) { @@ -1020,7 +1038,7 @@ static const JNINativeMethod gMethods[] = { {"nSetForceDark", "(JI)V", (void*)android_view_ThreadedRenderer_setForceDark}, {"nSetDisplayDensityDpi", "(I)V", (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, - {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, @@ -1090,8 +1108,12 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) { gCopyRequest.getDestinationBitmap = GetMethodIDOrDie(env, copyRequest, "getDestinationBitmap", "(II)J"); - void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); - fromSurface = (ANW_fromSurface)dlsym(handle_, "ANativeWindow_fromSurface"); +#ifdef __ANDROID__ + void* handle_ = SharedLib::openSharedLib("libandroid"); +#else + void* handle_ = SharedLib::openSharedLib("libandroid_runtime"); +#endif + fromSurface = (ANW_fromSurface)SharedLib::getSymbol(handle_, "ANativeWindow_fromSurface"); LOG_ALWAYS_FATAL_IF(fromSurface == nullptr, "Failed to find required symbol ANativeWindow_fromSurface!"); diff --git a/libs/hwui/jni/android_graphics_Mesh.cpp b/libs/hwui/jni/android_graphics_Mesh.cpp index 5cb43e54e499..3109de5055ca 100644 --- a/libs/hwui/jni/android_graphics_Mesh.cpp +++ b/libs/hwui/jni/android_graphics_Mesh.cpp @@ -38,8 +38,8 @@ static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject verte return 0; } auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto meshPtr = new Mesh(skMeshSpec, mode, std::move(buffer), vertexCount, vertexOffset, - std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); + auto meshPtr = new Mesh(skMeshSpec, static_cast<SkMesh::Mode>(mode), std::move(buffer), + vertexCount, vertexOffset, skRect); auto [valid, msg] = meshPtr->validate(); if (!valid) { jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str()); @@ -63,9 +63,9 @@ static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobjec return 0; } auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto meshPtr = new Mesh(skMeshSpec, mode, std::move(vBuf), vertexCount, vertexOffset, - std::move(iBuf), indexCount, indexOffset, - std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); + auto meshPtr = + new Mesh(skMeshSpec, static_cast<SkMesh::Mode>(mode), std::move(vBuf), vertexCount, + vertexOffset, std::move(iBuf), indexCount, indexOffset, skRect); auto [valid, msg] = meshPtr->validate(); if (!valid) { jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str()); @@ -133,7 +133,6 @@ static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring ScopedUtfChars name(env, uniformName); const float values[4] = {value1, value2, value3, value4}; nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count, false); - wrapper->markDirty(); } static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName, @@ -143,7 +142,6 @@ static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, js AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess); nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(), autoValues.length(), isColor); - wrapper->markDirty(); } static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder, @@ -166,7 +164,6 @@ static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring u ScopedUtfChars name(env, uniformName); const int values[4] = {value1, value2, value3, value4}; nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count); - wrapper->markDirty(); } static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, @@ -176,7 +173,6 @@ static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstr AutoJavaIntArray autoValues(env, values, 0); nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(), autoValues.length()); - wrapper->markDirty(); } static void MeshWrapper_destroy(Mesh* wrapper) { diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index a7d64231da80..6e03bbd0fa16 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -15,19 +15,17 @@ */ #define ATRACE_TAG ATRACE_TAG_VIEW -#include "GraphicsJNI.h" - #include <Animator.h> #include <DamageAccumulator.h> #include <Matrix.h> #include <RenderNode.h> -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext -#include <renderthread/CanvasContext.h> -#endif #include <TreeInfo.h> #include <effects/StretchEffect.h> #include <gui/TraceUtils.h> #include <hwui/Paint.h> +#include <renderthread/CanvasContext.h> + +#include "GraphicsJNI.h" namespace android { @@ -640,7 +638,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("Update SurfaceView position"); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext JNIEnv* env = jnienv(); // Update the new position synchronously. We cannot defer this to // a worker pool to process asynchronously because the UI thread @@ -669,7 +666,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override { @@ -682,7 +678,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("SurfaceView position lost"); JNIEnv* env = jnienv(); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext // Update the lost position synchronously. We cannot defer this to // a worker pool to process asynchronously because the UI thread // may be unblocked by the time a worker thread can process this, @@ -698,7 +693,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } private: @@ -750,7 +744,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, StretchEffectBehavior::Shader) { JNIEnv* env = jnienv(); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext SkVector stretchDirection = effect->getStretchDirection(); jboolean keepListening = env->CallStaticBooleanMethod( gPositionListener.clazz, gPositionListener.callApplyStretch, mListener, @@ -762,7 +755,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } } diff --git a/libs/hwui/jni/pdf/PdfRenderer.cpp b/libs/hwui/jni/pdf/PdfRenderer.cpp deleted file mode 100644 index cc1f96197c74..000000000000 --- a/libs/hwui/jni/pdf/PdfRenderer.cpp +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2014 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. - */ - -#include "PdfUtils.h" - -#include "GraphicsJNI.h" -#include "SkBitmap.h" -#include "SkMatrix.h" -#include "fpdfview.h" - -#include <vector> -#include <utils/Log.h> -#include <unistd.h> -#include <sys/types.h> -#include <unistd.h> - -namespace android { - -static const int RENDER_MODE_FOR_DISPLAY = 1; -static const int RENDER_MODE_FOR_PRINT = 2; - -static struct { - jfieldID x; - jfieldID y; -} gPointClassInfo; - -static jlong nativeOpenPageAndGetSize(JNIEnv* env, jclass thiz, jlong documentPtr, - jint pageIndex, jobject outSize) { - FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr); - - FPDF_PAGE page = FPDF_LoadPage(document, pageIndex); - if (!page) { - jniThrowException(env, "java/lang/IllegalStateException", - "cannot load page"); - return -1; - } - - double width = 0; - double height = 0; - - int result = FPDF_GetPageSizeByIndex(document, pageIndex, &width, &height); - if (!result) { - jniThrowException(env, "java/lang/IllegalStateException", - "cannot get page size"); - return -1; - } - - env->SetIntField(outSize, gPointClassInfo.x, width); - env->SetIntField(outSize, gPointClassInfo.y, height); - - return reinterpret_cast<jlong>(page); -} - -static void nativeClosePage(JNIEnv* env, jclass thiz, jlong pagePtr) { - FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr); - FPDF_ClosePage(page); -} - -static void nativeRenderPage(JNIEnv* env, jclass thiz, jlong documentPtr, jlong pagePtr, - jlong bitmapPtr, jint clipLeft, jint clipTop, jint clipRight, jint clipBottom, - jlong transformPtr, jint renderMode) { - FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr); - - SkBitmap skBitmap; - bitmap::toBitmap(bitmapPtr).getSkBitmap(&skBitmap); - - const int stride = skBitmap.width() * 4; - - FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(skBitmap.width(), skBitmap.height(), - FPDFBitmap_BGRA, skBitmap.getPixels(), stride); - - int renderFlags = FPDF_REVERSE_BYTE_ORDER; - if (renderMode == RENDER_MODE_FOR_DISPLAY) { - renderFlags |= FPDF_LCD_TEXT; - } else if (renderMode == RENDER_MODE_FOR_PRINT) { - renderFlags |= FPDF_PRINTING; - } - - SkMatrix matrix = *reinterpret_cast<SkMatrix*>(transformPtr); - SkScalar transformValues[6]; - if (!matrix.asAffine(transformValues)) { - jniThrowException(env, "java/lang/IllegalArgumentException", - "transform matrix has perspective. Only affine matrices are allowed."); - return; - } - - FS_MATRIX transform = {transformValues[SkMatrix::kAScaleX], transformValues[SkMatrix::kASkewY], - transformValues[SkMatrix::kASkewX], transformValues[SkMatrix::kAScaleY], - transformValues[SkMatrix::kATransX], - transformValues[SkMatrix::kATransY]}; - - FS_RECTF clip = {(float) clipLeft, (float) clipTop, (float) clipRight, (float) clipBottom}; - - FPDF_RenderPageBitmapWithMatrix(bitmap, page, &transform, &clip, renderFlags); - - skBitmap.notifyPixelsChanged(); -} - -static const JNINativeMethod gPdfRenderer_Methods[] = { - {"nativeCreate", "(IJ)J", (void*) nativeOpen}, - {"nativeClose", "(J)V", (void*) nativeClose}, - {"nativeGetPageCount", "(J)I", (void*) nativeGetPageCount}, - {"nativeScaleForPrinting", "(J)Z", (void*) nativeScaleForPrinting}, - {"nativeRenderPage", "(JJJIIIIJI)V", (void*) nativeRenderPage}, - {"nativeOpenPageAndGetSize", "(JILandroid/graphics/Point;)J", (void*) nativeOpenPageAndGetSize}, - {"nativeClosePage", "(J)V", (void*) nativeClosePage} -}; - -int register_android_graphics_pdf_PdfRenderer(JNIEnv* env) { - int result = RegisterMethodsOrDie( - env, "android/graphics/pdf/PdfRenderer", gPdfRenderer_Methods, - NELEM(gPdfRenderer_Methods)); - - jclass clazz = FindClassOrDie(env, "android/graphics/Point"); - gPointClassInfo.x = GetFieldIDOrDie(env, clazz, "x", "I"); - gPointClassInfo.y = GetFieldIDOrDie(env, clazz, "y", "I"); - - return result; -}; - -}; diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 6ccb212fe6ca..40dfc9d4309b 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -16,6 +16,7 @@ #pragma once +#include <FileBlobCache.h> #include <GrContextOptions.h> #include <SkRefCnt.h> #include <cutils/compiler.h> @@ -32,7 +33,6 @@ class SkData; namespace android { class BlobCache; -class FileBlobCache; namespace uirenderer { namespace skiapipeline { diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp new file mode 100644 index 000000000000..5bbbc1009541 --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pipeline/skia/SkiaCpuPipeline.h" + +#include <system/window.h> + +#include "DeviceInfo.h" +#include "LightingInfo.h" +#include "renderthread/Frame.h" +#include "utils/Color.h" + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +void SkiaCpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + RenderNode* layerNode = layers.entries()[i].renderNode.get(); + // only schedule repaint if node still on layer - possible it may have been + // removed during a dropped frame, but layers may still remain scheduled so + // as not to lose info on what portion is damaged + if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { + continue; + } + bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage); + if (!rendered) { + return; + } + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaCpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + node->setLayerSurface(SkSurfaces::Raster(info, &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +MakeCurrentResult SkiaCpuPipeline::makeCurrent() { + return MakeCurrentResult::AlreadyCurrent; +} + +Frame SkiaCpuPipeline::getFrame() { + return Frame(mSurface->width(), mSurface->height(), 0); +} + +IRenderPipeline::DrawResult SkiaCpuPipeline::draw( + const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) { + LightingInfo::updateLighting(lightGeometry, lightInfo); + renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mSurface, + SkMatrix::I()); + return {true, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd{}}; +} + +bool SkiaCpuPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { + if (surface) { + ANativeWindowBuffer* buffer; + surface->dequeueBuffer(surface, &buffer, nullptr); + int width, height; + surface->query(surface, NATIVE_WINDOW_WIDTH, &width); + surface->query(surface, NATIVE_WINDOW_HEIGHT, &height); + SkImageInfo imageInfo = + SkImageInfo::Make(width, height, mSurfaceColorType, + SkAlphaType::kPremul_SkAlphaType, mSurfaceColorSpace); + size_t widthBytes = width * imageInfo.bytesPerPixel(); + void* pixels = buffer->reserved[0]; + mSurface = SkSurfaces::WrapPixels(imageInfo, pixels, widthBytes); + } else { + mSurface = sk_sp<SkSurface>(); + } + return true; +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.h b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h new file mode 100644 index 000000000000..5a1014c2c2de --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaCpuPipeline : public SkiaPipeline { +public: + SkiaCpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaCpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override; + renderthread::Frame getFrame() override; + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override; + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override; + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return mSurface.get() != nullptr; } + bool isContextReady() override { return true; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + +private: + sk_sp<SkSurface> mSurface; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index 5c8285a8e1e9..36dc933aa7b0 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -15,22 +15,18 @@ */ #include "SkiaDisplayList.h" -#include "FunctorDrawable.h" +#include <SkImagePriv.h> +#include <SkPathOps.h> + +// clang-format off +#include "FunctorDrawable.h" // Must be included before DumpOpsCanvas.h #include "DumpOpsCanvas.h" -#ifdef __ANDROID__ // Layoutlib does not support SkiaPipeline +// clang-format on #include "SkiaPipeline.h" -#else -#include "DamageAccumulator.h" -#endif #include "TreeInfo.h" #include "VectorDrawable.h" -#ifdef __ANDROID__ #include "renderthread/CanvasContext.h" -#endif - -#include <SkImagePriv.h> -#include <SkPathOps.h> namespace android { namespace uirenderer { @@ -101,7 +97,6 @@ bool SkiaDisplayList::prepareListAndChildren( // If the prepare tree is triggered by the UI thread and no previous call to // pinImages has failed then we must pin all mutable images in the GPU cache // until the next UI thread draw. -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext if (info.prepareTextures && !info.canvasContext.pinImages(mMutableImages)) { // In the event that pinning failed we prevent future pinImage calls for the // remainder of this tree traversal and also unpin any currently pinned images @@ -110,11 +105,11 @@ bool SkiaDisplayList::prepareListAndChildren( info.canvasContext.unpinImages(); } +#ifdef __ANDROID__ auto grContext = info.canvasContext.getGrContext(); - for (auto mesh : mMeshes) { - mesh->updateSkMesh(grContext); + for (const auto& bufferData : mMeshBufferData) { + bufferData->updateBuffers(grContext); } - #endif bool hasBackwardProjectedNodesHere = false; @@ -181,7 +176,7 @@ void SkiaDisplayList::reset() { mDisplayList.reset(); - mMeshes.clear(); + mMeshBufferData.clear(); mMutableImages.clear(); mVectorDrawables.clear(); mAnimatedImages.clear(); diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index b9dc1c49f09e..071a4e8caaff 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -17,6 +17,7 @@ #pragma once #include <deque> +#include <memory> #include "Mesh.h" #include "RecordingCanvas.h" @@ -172,7 +173,7 @@ public: std::deque<RenderNodeDrawable> mChildNodes; std::deque<FunctorDrawable*> mChildFunctors; std::vector<SkImage*> mMutableImages; - std::vector<const Mesh*> mMeshes; + std::vector<std::shared_ptr<const MeshBufferData>> mMeshBufferData; private: std::vector<Pair<VectorDrawableRoot*, SkMatrix>> mVectorDrawables; diff --git a/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp new file mode 100644 index 000000000000..7bfbfdc4b96b --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "pipeline/skia/SkiaGpuPipeline.h" + +#include <SkImageAndroid.h> +#include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +SkiaGpuPipeline::SkiaGpuPipeline(RenderThread& thread) : SkiaPipeline(thread) {} + +SkiaGpuPipeline::~SkiaGpuPipeline() { + unpinImages(); +} + +void SkiaGpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + sk_sp<GrDirectContext> cachedContext; + + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + RenderNode* layerNode = layers.entries()[i].renderNode.get(); + // only schedule repaint if node still on layer - possible it may have been + // removed during a dropped frame, but layers may still remain scheduled so + // as not to lose info on what portion is damaged + if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { + continue; + } + bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage); + if (!rendered) { + return; + } + // cache the current context so that we can defer flushing it until + // either all the layers have been rendered or the context changes + GrDirectContext* currentContext = + GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); + if (cachedContext.get() != currentContext) { + if (cachedContext.get()) { + ATRACE_NAME("flush layers (context changed)"); + cachedContext->flushAndSubmit(); + } + cachedContext.reset(SkSafeRef(currentContext)); + } + } + if (cachedContext.get()) { + ATRACE_NAME("flush layers"); + cachedContext->flushAndSubmit(); + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaGpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + SkASSERT(mRenderThread.getGrContext() != nullptr); + node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, info, 0, + this->getSurfaceOrigin(), &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +bool SkiaGpuPipeline::pinImages(std::vector<SkImage*>& mutableImages) { + if (!mRenderThread.getGrContext()) { + ALOGD("Trying to pin an image with an invalid GrContext"); + return false; + } + for (SkImage* image : mutableImages) { + if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { + mPinnedImages.emplace_back(sk_ref_sp(image)); + } else { + return false; + } + } + return true; +} + +void SkiaGpuPipeline::unpinImages() { + for (auto& image : mPinnedImages) { + skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); + } + mPinnedImages.clear(); +} + +void SkiaGpuPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { + GrDirectContext* context = thread.getGrContext(); + if (context && !bitmap->isHardware()) { + ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); + auto image = bitmap->makeImage(); + if (image.get()) { + skgpu::ganesh::PinAsTexture(context, image.get()); + skgpu::ganesh::UnpinTexture(context, image.get()); + // A submit is necessary as there may not be a frame coming soon, so without a call + // to submit these texture uploads can just sit in the queue building up until + // we run out of RAM + context->flushAndSubmit(); + } + } +} + +sk_sp<SkSurface> SkiaGpuPipeline::getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams) { + auto bufferColorSpace = bufferParams.getColorSpace(); + if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || + !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { + mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( + mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, + bufferColorSpace, nullptr, true); + mBufferColorSpace = bufferColorSpace; + } + return mBufferSurface; +} + +void SkiaGpuPipeline::dumpResourceCacheUsage() const { + int resources; + size_t bytes; + mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); + size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); + + SkString log("Resource Cache Usage:\n"); + log.appendf("%8d items\n", resources); + log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, + bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); + + ALOGD("%s", log.c_str()); +} + +void SkiaGpuPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { + if (mHardwareBuffer) { + AHardwareBuffer_release(mHardwareBuffer); + mHardwareBuffer = nullptr; + } + + if (buffer) { + AHardwareBuffer_acquire(buffer); + mHardwareBuffer = buffer; + } +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index c8d598702a7c..e4b1f916b4d6 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -14,25 +14,25 @@ * limitations under the License. */ -#include "SkiaOpenGLPipeline.h" +#include "pipeline/skia/SkiaOpenGLPipeline.h" -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> -#include <include/gpu/gl/GrGLTypes.h> #include <GrBackendSurface.h> #include <SkBlendMode.h> #include <SkImageInfo.h> #include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> +#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> +#include <include/gpu/gl/GrGLTypes.h> #include <strings.h> #include "DeferredLayerUpdater.h" #include "FrameInfo.h" -#include "LayerDrawable.h" #include "LightingInfo.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" #include "hwui/Bitmap.h" +#include "pipeline/skia/LayerDrawable.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" #include "private/hwui/DrawGlInfo.h" #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" @@ -47,7 +47,7 @@ namespace uirenderer { namespace skiapipeline { SkiaOpenGLPipeline::SkiaOpenGLPipeline(RenderThread& thread) - : SkiaPipeline(thread), mEglManager(thread.eglManager()) { + : SkiaGpuPipeline(thread), mEglManager(thread.eglManager()) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 326b6ed77fe0..dc669a5eca73 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -14,11 +14,8 @@ * limitations under the License. */ -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaPipeline.h" -#include <include/android/SkSurfaceAndroid.h> -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/encode/SkPngEncoder.h> #include <SkCanvas.h> #include <SkColor.h> #include <SkColorSpace.h> @@ -27,7 +24,6 @@ #include <SkImageAndroid.h> #include <SkImageInfo.h> #include <SkMatrix.h> -#include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> #include <SkPicture.h> @@ -40,6 +36,10 @@ #include <SkTypeface.h> #include <android-base/properties.h> #include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/docs/SkMultiPictureDocument.h> +#include <include/encode/SkPngEncoder.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <unistd.h> #include <sstream> @@ -62,37 +62,13 @@ SkiaPipeline::SkiaPipeline(RenderThread& thread) : mRenderThread(thread) { setSurfaceColorProperties(mColorMode); } -SkiaPipeline::~SkiaPipeline() { - unpinImages(); -} +SkiaPipeline::~SkiaPipeline() {} void SkiaPipeline::onDestroyHardwareResources() { unpinImages(); mRenderThread.cacheManager().trimStaleResources(); } -bool SkiaPipeline::pinImages(std::vector<SkImage*>& mutableImages) { - if (!mRenderThread.getGrContext()) { - ALOGD("Trying to pin an image with an invalid GrContext"); - return false; - } - for (SkImage* image : mutableImages) { - if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { - mPinnedImages.emplace_back(sk_ref_sp(image)); - } else { - return false; - } - } - return true; -} - -void SkiaPipeline::unpinImages() { - for (auto& image : mPinnedImages) { - skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); - } - mPinnedImages.clear(); -} - void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) { @@ -102,136 +78,48 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, layerUpdateQueue->clear(); } -void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { - sk_sp<GrDirectContext> cachedContext; - - // Render all layers that need to be updated, in order. - for (size_t i = 0; i < layers.entries().size(); i++) { - RenderNode* layerNode = layers.entries()[i].renderNode.get(); - // only schedule repaint if node still on layer - possible it may have been - // removed during a dropped frame, but layers may still remain scheduled so - // as not to lose info on what portion is damaged - if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { - continue; - } - SkASSERT(layerNode->getLayerSurface()); - SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); - if (!displayList || displayList->isEmpty()) { - ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); - return; - } - - const Rect& layerDamage = layers.entries()[i].damage; - - SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - - int saveCount = layerCanvas->save(); - SkASSERT(saveCount == 1); - - layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - - // TODO: put localized light center calculation and storage to a drawable related code. - // It does not seem right to store something localized in a global state - // fix here and in recordLayers - const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); - Vector3 transformedLightCenter(savedLightCenter); - // map current light center into RenderNode's coordinate space - layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); - LightingInfo::setLightCenterRaw(transformedLightCenter); - - const RenderProperties& properties = layerNode->properties(); - const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); - if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { - return; - } - - ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), - bounds.height()); +bool SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) { + SkASSERT(layerNode->getLayerSurface()); + SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); + if (!displayList || displayList->isEmpty()) { + ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); + return false; + } - layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; - layerCanvas->clear(SK_ColorTRANSPARENT); + SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - RenderNodeDrawable root(layerNode, layerCanvas, false); - root.forceDraw(layerCanvas); - layerCanvas->restoreToCount(saveCount); + int saveCount = layerCanvas->save(); + SkASSERT(saveCount == 1); - LightingInfo::setLightCenterRaw(savedLightCenter); + layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - // cache the current context so that we can defer flushing it until - // either all the layers have been rendered or the context changes - GrDirectContext* currentContext = - GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); - if (cachedContext.get() != currentContext) { - if (cachedContext.get()) { - ATRACE_NAME("flush layers (context changed)"); - cachedContext->flushAndSubmit(); - } - cachedContext.reset(SkSafeRef(currentContext)); - } + // TODO: put localized light center calculation and storage to a drawable related code. + // It does not seem right to store something localized in a global state + // fix here and in recordLayers + const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); + Vector3 transformedLightCenter(savedLightCenter); + // map current light center into RenderNode's coordinate space + layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); + LightingInfo::setLightCenterRaw(transformedLightCenter); + + const RenderProperties& properties = layerNode->properties(); + const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); + if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { + return false; } - if (cachedContext.get()) { - ATRACE_NAME("flush layers"); - cachedContext->flushAndSubmit(); - } -} + ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), + bounds.height()); -bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) { - // compute the size of the surface (i.e. texture) to be allocated for this layer - const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; - const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; - - SkSurface* layer = node->getLayerSurface(); - if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { - SkImageInfo info; - info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), - kPremul_SkAlphaType, getSurfaceColorSpace()); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); - SkASSERT(mRenderThread.getGrContext() != nullptr); - node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), - skgpu::Budgeted::kYes, info, 0, - this->getSurfaceOrigin(), &props)); - if (node->getLayerSurface()) { - // update the transform in window of the layer to reset its origin wrt light source - // position - Matrix4 windowTransform; - damageAccumulator.computeCurrentTransform(&windowTransform); - node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); - } else { - String8 cachesOutput; - mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, - &mRenderThread.renderState()); - ALOGE("%s", cachesOutput.c_str()); - if (errorHandler) { - std::ostringstream err; - err << "Unable to create layer for " << node->getName(); - const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); - err << ", size " << info.width() << "x" << info.height() << " max size " - << maxTextureSize << " color type " << (int)info.colorType() << " has context " - << (int)(mRenderThread.getGrContext() != nullptr); - errorHandler->onError(err.str()); - } - } - return true; - } - return false; -} + layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; + layerCanvas->clear(SK_ColorTRANSPARENT); -void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - GrDirectContext* context = thread.getGrContext(); - if (context && !bitmap->isHardware()) { - ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); - auto image = bitmap->makeImage(); - if (image.get()) { - skgpu::ganesh::PinAsTexture(context, image.get()); - skgpu::ganesh::UnpinTexture(context, image.get()); - // A submit is necessary as there may not be a frame coming soon, so without a call - // to submit these texture uploads can just sit in the queue building up until - // we run out of RAM - context->flushAndSubmit(); - } - } + RenderNodeDrawable root(layerNode, layerCanvas, false); + root.forceDraw(layerCanvas); + layerCanvas->restoreToCount(saveCount); + + LightingInfo::setLightCenterRaw(savedLightCenter); + return true; } static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) { @@ -297,7 +185,7 @@ bool SkiaPipeline::setupMultiFrameCapture() { // we need to keep it until after mMultiPic.close() // procs is passed as a pointer, but just as a method of having an optional default. // procs doesn't need to outlive this Make call. - mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs, + mMultiPic = SkMultiPictureDocument::Make(mOpenMultiPicStream.get(), &procs, [sharingCtx = mSerialContext.get()](const SkPicture* pic) { SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx); }); @@ -599,45 +487,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, } } -void SkiaPipeline::dumpResourceCacheUsage() const { - int resources; - size_t bytes; - mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); - size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); - - SkString log("Resource Cache Usage:\n"); - log.appendf("%8d items\n", resources); - log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, - bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); - - ALOGD("%s", log.c_str()); -} - -void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { - if (mHardwareBuffer) { - AHardwareBuffer_release(mHardwareBuffer); - mHardwareBuffer = nullptr; - } - - if (buffer) { - AHardwareBuffer_acquire(buffer); - mHardwareBuffer = buffer; - } -} - -sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams) { - auto bufferColorSpace = bufferParams.getColorSpace(); - if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || - !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { - mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( - mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, - bufferColorSpace, nullptr, true); - mBufferColorSpace = bufferColorSpace; - } - return mBufferSurface; -} - void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mColorMode = colorMode; switch (colorMode) { @@ -650,7 +499,11 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); break; case ColorMode::Hdr: - if (DeviceInfo::get()->isSupportFp16ForHdr()) { + if (DeviceInfo::get()->isSupportRgba10101010ForHdr()) { + mSurfaceColorType = SkColorType::kRGBA_10x6_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } else if (DeviceInfo::get()->isSupportFp16ForHdr()) { mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; mSurfaceColorSpace = SkColorSpace::MakeSRGB(); } else { @@ -675,7 +528,8 @@ void SkiaPipeline::setTargetSdrHdrRatio(float ratio) { if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { mTargetSdrHdrRatio = ratio; - if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) { + if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr() && + !DeviceInfo::get()->isSupportRgba10101010ForHdr()) { mSurfaceColorSpace = SkColorSpace::MakeSRGB(); } else { mSurfaceColorSpace = SkColorSpace::MakeRGB( diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index befee8989383..823b209017a5 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -18,7 +18,6 @@ #include <SkColorSpace.h> #include <SkDocument.h> -#include <SkMultiPictureDocument.h> #include <SkSurface.h> #include "Lighting.h" @@ -42,18 +41,9 @@ public: void onDestroyHardwareResources() override; - bool pinImages(std::vector<SkImage*>& mutableImages) override; - bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } - void unpinImages() override; - void renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) override; - // If the given node didn't have a layer surface, or had one of the wrong size, this method - // creates a new one and returns true. Otherwise does nothing and returns false. - bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) override; - void setSurfaceColorProperties(ColorMode colorMode) override; SkColorType getSurfaceColorType() const override { return mSurfaceColorType; } sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; } @@ -63,9 +53,8 @@ public: const Rect& contentDrawBounds, sk_sp<SkSurface> surface, const SkMatrix& preTransform); - static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); - - void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque); + bool renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage); + virtual void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) = 0; // Sets the recording callback to the provided function and the recording mode // to CallbackAPI @@ -75,19 +64,11 @@ public: mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None; } - virtual void setHardwareBuffer(AHardwareBuffer* buffer) override; - bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } - void setTargetSdrHdrRatio(float ratio) override; protected: - sk_sp<SkSurface> getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams); - void dumpResourceCacheUsage() const; - renderthread::RenderThread& mRenderThread; - AHardwareBuffer* mHardwareBuffer = nullptr; sk_sp<SkSurface> mBufferSurface = nullptr; sk_sp<SkColorSpace> mBufferColorSpace = nullptr; @@ -125,8 +106,6 @@ private: // Set up a multi frame capture. bool setupMultiFrameCapture(); - std::vector<sk_sp<SkImage>> mPinnedImages; - // Block of properties used only for debugging to record a SkPicture and save it in a file. // There are three possible ways of recording drawing commands. enum class CaptureMode { diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index e917f9a66917..45bfe1c4957f 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -342,7 +342,7 @@ double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedIma } void SkiaRecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) { - mDisplayList->mMeshes.push_back(&mesh); + mDisplayList->mMeshBufferData.push_back(mesh.refBufferData()); mRecorder.drawMesh(mesh, blender, paint); } diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index fd0a8e06f39c..d06dba05ee88 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "SkiaVulkanPipeline.h" +#include "pipeline/skia/SkiaVulkanPipeline.h" #include <GrDirectContext.h> #include <GrTypes.h> @@ -28,10 +28,10 @@ #include "DeferredLayerUpdater.h" #include "LightingInfo.h" #include "Readback.h" -#include "ShaderCache.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" -#include "VkInteropFunctorDrawable.h" +#include "pipeline/skia/ShaderCache.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" +#include "pipeline/skia/VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" #include "renderthread/IRenderPipeline.h" @@ -42,7 +42,8 @@ namespace android { namespace uirenderer { namespace skiapipeline { -SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) { +SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) + : SkiaGpuPipeline(thread) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index b62711f50c94..21fe6ff14f56 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -16,10 +16,10 @@ #include "VkFunctorDrawable.h" -#include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> +#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h> #include <gui/TraceUtils.h> #include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> diff --git a/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..9159eae46065 --- /dev/null +++ b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread); + virtual ~SkiaGpuPipeline(); + + virtual GrSurfaceOrigin getSurfaceOrigin() = 0; + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + + bool pinImages(std::vector<SkImage*>& mutableImages) override; + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override; + bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } + + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); + +protected: + sk_sp<SkSurface> getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams); + void dumpResourceCacheUsage() const; + + AHardwareBuffer* mHardwareBuffer = nullptr; + +private: + std::vector<sk_sp<SkImage>> mPinnedImages; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h index ebe8b6e15d44..6e7478288777 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h @@ -19,7 +19,7 @@ #include <EGL/egl.h> #include <system/window.h> -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" @@ -30,7 +30,7 @@ class Bitmap; namespace uirenderer { namespace skiapipeline { -class SkiaOpenGLPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaOpenGLPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: SkiaOpenGLPipeline(renderthread::RenderThread& thread); virtual ~SkiaOpenGLPipeline(); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h index 624eaa51a584..0d30df48baee 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h @@ -17,7 +17,7 @@ #pragma once #include "SkRefCnt.h" -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" #include "renderthread/VulkanManager.h" @@ -30,7 +30,7 @@ namespace android { namespace uirenderer { namespace skiapipeline { -class SkiaVulkanPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaVulkanPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: explicit SkiaVulkanPipeline(renderthread::RenderThread& thread); virtual ~SkiaVulkanPipeline(); diff --git a/libs/hwui/platform/android/thread/CommonPoolBase.h b/libs/hwui/platform/android/thread/CommonPoolBase.h new file mode 100644 index 000000000000..8f836b612440 --- /dev/null +++ b/libs/hwui/platform/android/thread/CommonPoolBase.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FRAMEWORKS_BASE_COMMONPOOLBASE_H +#define FRAMEWORKS_BASE_COMMONPOOLBASE_H + +#include <sys/resource.h> + +#include "renderthread/RenderThread.h" + +namespace android { +namespace uirenderer { + +class CommonPoolBase { + PREVENT_COPY_AND_ASSIGN(CommonPoolBase); + +protected: + CommonPoolBase() {} + + void setupThread(int i, std::mutex& mLock, std::vector<int>& tids, + std::vector<std::condition_variable>& tidConditionVars) { + std::array<char, 20> name{"hwuiTask"}; + snprintf(name.data(), name.size(), "hwuiTask%d", i); + auto self = pthread_self(); + pthread_setname_np(self, name.data()); + { + std::unique_lock lock(mLock); + tids[i] = pthread_gettid_np(self); + tidConditionVars[i].notify_one(); + } + setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND); + auto startHook = renderthread::RenderThread::getOnStartHook(); + if (startHook) { + startHook(name.data()); + } + } + + bool supportsTid() { return true; } +}; + +} // namespace uirenderer +} // namespace android + +#endif // FRAMEWORKS_BASE_COMMONPOOLBASE_H diff --git a/libs/hwui/platform/darwin/utils/SharedLib.cpp b/libs/hwui/platform/darwin/utils/SharedLib.cpp new file mode 100644 index 000000000000..6e9f0b486513 --- /dev/null +++ b/libs/hwui/platform/darwin/utils/SharedLib.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "utils/SharedLib.h" + +#include <dlfcn.h> + +namespace android { +namespace uirenderer { + +void* SharedLib::openSharedLib(std::string filename) { + return dlopen((filename + ".dylib").c_str(), RTLD_NOW | RTLD_NODELETE); +} + +void* SharedLib::getSymbol(void* library, const char* symbol) { + return dlsym(library, symbol); +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/platform/host/WebViewFunctorManager.cpp b/libs/hwui/platform/host/WebViewFunctorManager.cpp index 1d16655bf73c..4ba206b41b39 100644 --- a/libs/hwui/platform/host/WebViewFunctorManager.cpp +++ b/libs/hwui/platform/host/WebViewFunctorManager.cpp @@ -50,6 +50,8 @@ ASurfaceControl* WebViewFunctor::getSurfaceControl() { void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) {} +void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) {} + void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) {} WebViewFunctorManager& WebViewFunctorManager::instance() { @@ -68,6 +70,13 @@ void WebViewFunctorManager::onContextDestroyed() {} void WebViewFunctorManager::destroyFunctor(int functor) {} +void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids, + size_t size) {} + +std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() { + return {}; +} + sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) { return nullptr; } diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h new file mode 120000 index 000000000000..4fb4784f9f60 --- /dev/null +++ b/libs/hwui/platform/host/android/api-level.h @@ -0,0 +1 @@ +../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..a71726585081 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" +#include "renderthread/Frame.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaGpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override { + return false; + } + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override {} + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override { + return renderthread::MakeCurrentResult::Failed; + } + renderthread::Frame getFrame() override { return renderthread::Frame(0, 0, 0); } + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override { + return {false, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd(-1)}; + } + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override { + return false; + } + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return false; } + bool isContextReady() override { return false; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h new file mode 100644 index 000000000000..4fafbcc4748d --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaOpenGLPipeline : public SkiaGpuPipeline { +public: + SkiaOpenGLPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h new file mode 100644 index 000000000000..d54caef45bb5 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaVulkanPipeline : public SkiaGpuPipeline { +public: + SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp new file mode 100644 index 000000000000..b1b1d5830834 --- /dev/null +++ b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 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. + */ + +#include "renderthread/HintSessionWrapper.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +void HintSessionWrapper::HintSessionBinding::init() {} + +HintSessionWrapper::HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId) + : mUiThreadId(uiThreadId) + , mRenderThreadId(renderThreadId) + , mBinding(std::make_shared<HintSessionBinding>()) {} + +HintSessionWrapper::~HintSessionWrapper() {} + +void HintSessionWrapper::destroy() {} + +bool HintSessionWrapper::init() { + return false; +} + +void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) {} + +void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {} + +void HintSessionWrapper::sendLoadResetHint() {} + +void HintSessionWrapper::sendLoadIncreaseHint() {} + +bool HintSessionWrapper::alive() { + return false; +} + +nsecs_t HintSessionWrapper::getLastUpdate() { + return -1; +} + +void HintSessionWrapper::delayedDestroy(RenderThread& rt, nsecs_t delay, + std::shared_ptr<HintSessionWrapper> wrapperPtr) {} + +void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) {} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/renderthread/ReliableSurface.cpp b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp new file mode 100644 index 000000000000..2deaaf3b909c --- /dev/null +++ b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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. + */ + +#include "renderthread/ReliableSurface.h" + +#include <log/log_main.h> +#include <system/window.h> + +namespace android::uirenderer::renderthread { + +ReliableSurface::ReliableSurface(ANativeWindow* window) : mWindow(window) { + LOG_ALWAYS_FATAL_IF(!mWindow, "Error, unable to wrap a nullptr"); + ANativeWindow_acquire(mWindow); +} + +ReliableSurface::~ReliableSurface() { + ANativeWindow_release(mWindow); +} + +void ReliableSurface::init() {} + +int ReliableSurface::reserveNext() { + return OK; +} + +}; // namespace android::uirenderer::renderthread diff --git a/libs/hwui/platform/host/renderthread/RenderThread.cpp b/libs/hwui/platform/host/renderthread/RenderThread.cpp index 6f08b5979772..f9d0f4704e08 100644 --- a/libs/hwui/platform/host/renderthread/RenderThread.cpp +++ b/libs/hwui/platform/host/renderthread/RenderThread.cpp @@ -17,6 +17,7 @@ #include "renderthread/RenderThread.h" #include "Readback.h" +#include "renderstate/RenderState.h" #include "renderthread/VulkanManager.h" namespace android { @@ -66,6 +67,7 @@ RenderThread::RenderThread() RenderThread::~RenderThread() {} void RenderThread::initThreadLocals() { + mRenderState = new RenderState(*this); mCacheManager = new CacheManager(*this); } diff --git a/libs/hwui/platform/host/thread/CommonPoolBase.h b/libs/hwui/platform/host/thread/CommonPoolBase.h new file mode 100644 index 000000000000..cd091013ce0c --- /dev/null +++ b/libs/hwui/platform/host/thread/CommonPoolBase.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FRAMEWORKS_BASE_COMMONPOOLBASE_H +#define FRAMEWORKS_BASE_COMMONPOOLBASE_H + +#include <condition_variable> +#include <mutex> +#include <vector> + +#include "renderthread/RenderThread.h" + +namespace android { +namespace uirenderer { + +class CommonPoolBase { + PREVENT_COPY_AND_ASSIGN(CommonPoolBase); + +protected: + CommonPoolBase() {} + + void setupThread(int i, std::mutex& mLock, std::vector<int>& tids, + std::vector<std::condition_variable>& tidConditionVars) { + std::array<char, 20> name{"hwuiTask"}; + snprintf(name.data(), name.size(), "hwuiTask%d", i); + { + std::unique_lock lock(mLock); + tids[i] = -1; + tidConditionVars[i].notify_one(); + } + auto startHook = renderthread::RenderThread::getOnStartHook(); + if (startHook) { + startHook(name.data()); + } + } + + bool supportsTid() { return false; } +}; + +} // namespace uirenderer +} // namespace android + +#endif // FRAMEWORKS_BASE_COMMONPOOLBASE_H diff --git a/libs/hwui/platform/host/utils/MessageHandler.h b/libs/hwui/platform/host/utils/MessageHandler.h new file mode 100644 index 000000000000..51ee48e0c6d2 --- /dev/null +++ b/libs/hwui/platform/host/utils/MessageHandler.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <utils/RefBase.h> + +struct Message { + Message(int w) {} +}; + +class MessageHandler : public virtual android::RefBase { +protected: + virtual ~MessageHandler() override {} + +public: + /** + * Handles a message. + */ + virtual void handleMessage(const Message& message) = 0; +}; diff --git a/libs/hwui/platform/linux/utils/SharedLib.cpp b/libs/hwui/platform/linux/utils/SharedLib.cpp new file mode 100644 index 000000000000..a9acf37dfef4 --- /dev/null +++ b/libs/hwui/platform/linux/utils/SharedLib.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "utils/SharedLib.h" + +#include <dlfcn.h> + +namespace android { +namespace uirenderer { + +void* SharedLib::openSharedLib(std::string filename) { + return dlopen((filename + ".so").c_str(), RTLD_NOW | RTLD_NODELETE); +} + +void* SharedLib::getSymbol(void* library, const char* symbol) { + return dlsym(library, symbol); +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/private/hwui/WebViewFunctor.h b/libs/hwui/private/hwui/WebViewFunctor.h index 493c943079ab..dbd8a16dfcfc 100644 --- a/libs/hwui/private/hwui/WebViewFunctor.h +++ b/libs/hwui/private/hwui/WebViewFunctor.h @@ -106,6 +106,11 @@ ANDROID_API int WebViewFunctor_create(void* data, const WebViewFunctorCallbacks& // and it should be considered alive & active until that point. ANDROID_API void WebViewFunctor_release(int functor); +// Reports the list of threads critical for frame production for the given +// functor. Must be called on render thread. +ANDROID_API void WebViewFunctor_reportRenderingThreads(int functor, const int32_t* thread_ids, + size_t size); + } // namespace android::uirenderer #endif // FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H diff --git a/libs/hwui/renderstate/RenderState.h b/libs/hwui/renderstate/RenderState.h index e08d32a7735c..60657cf91123 100644 --- a/libs/hwui/renderstate/RenderState.h +++ b/libs/hwui/renderstate/RenderState.h @@ -16,11 +16,13 @@ #ifndef RENDERSTATE_H #define RENDERSTATE_H -#include "utils/Macros.h" - +#include <pthread.h> #include <utils/RefBase.h> + #include <set> +#include "utils/Macros.h" + namespace android { namespace uirenderer { diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 1d0330185b1c..66e089627a7b 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -35,8 +35,9 @@ #include "Properties.h" #include "RenderThread.h" #include "hwui/Canvas.h" +#include "pipeline/skia/SkiaCpuPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" -#include "pipeline/skia/SkiaPipeline.h" #include "pipeline/skia/SkiaVulkanPipeline.h" #include "thread/CommonPool.h" #include "utils/GLUtils.h" @@ -72,7 +73,7 @@ CanvasContext* ScopedActiveContext::sActiveContext = nullptr; CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, - int32_t uiThreadId, int32_t renderThreadId) { + pid_t uiThreadId, pid_t renderThreadId) { auto renderType = Properties::getRenderPipelineType(); switch (renderType) { @@ -84,6 +85,12 @@ CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent, return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread), uiThreadId, renderThreadId); +#ifndef __ANDROID__ + case RenderPipelineType::SkiaCpu: + return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, + std::make_unique<skiapipeline::SkiaCpuPipeline>(thread), + uiThreadId, renderThreadId); +#endif default: LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType); break; @@ -108,7 +115,7 @@ void CanvasContext::invokeFunctor(const RenderThread& thread, Functor* functor) } void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - skiapipeline::SkiaPipeline::prepareToDraw(thread, bitmap); + skiapipeline::SkiaGpuPipeline::prepareToDraw(thread, bitmap); } CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, @@ -182,6 +189,7 @@ static void setBufferCount(ANativeWindow* window) { } void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) { +#ifdef __ANDROID__ if (mHardwareBuffer) { AHardwareBuffer_release(mHardwareBuffer); mHardwareBuffer = nullptr; @@ -192,6 +200,7 @@ void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) { mHardwareBuffer = buffer; } mRenderPipeline->setHardwareBuffer(mHardwareBuffer); +#endif } void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { @@ -411,7 +420,8 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy // If the previous frame was dropped we don't need to hold onto it, so // just keep using the previous frame's structure instead - if (const auto reason = wasSkipped(mCurrentFrameInfo)) { + const auto reason = wasSkipped(mCurrentFrameInfo); + if (reason.has_value()) { // Use the oldest skipped frame in case we skip more than a single frame if (!mSkippedFrameInfo) { switch (*reason) { @@ -560,6 +570,7 @@ Frame CanvasContext::getFrame() { } void CanvasContext::draw(bool solelyTextureViewUpdates) { +#ifdef __ANDROID__ if (auto grContext = getGrContext()) { if (grContext->abandoned()) { if (grContext->isDeviceLost()) { @@ -570,6 +581,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { return; } } +#endif SkRect dirty; mDamageAccumulator.finish(&dirty); @@ -593,11 +605,13 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { if (skippedFrameReason) { mCurrentFrameInfo->setSkippedFrameReason(*skippedFrameReason); +#ifdef __ANDROID__ if (auto grContext = getGrContext()) { // Submit to ensure that any texture uploads complete and Skia can // free its staging buffers. grContext->flushAndSubmit(); } +#endif // Notify the callbacks, even if there's nothing to draw so they aren't waiting // indefinitely @@ -776,6 +790,8 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { (std::min(syncDelayDuration, mLastDequeueBufferDuration)) - dequeueBufferDuration - idleDuration; mHintSessionWrapper->reportActualWorkDuration(actualDuration); + mHintSessionWrapper->setActiveFunctorThreads( + WebViewFunctorManager::instance().getRenderingThreadsForActiveFunctors()); } mLastDequeueBufferDuration = dequeueBufferDuration; diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 1b333bfccbf1..826d00e1f32f 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -140,12 +140,14 @@ void DrawFrameTask::run() { if (CC_LIKELY(canDrawThisFrame)) { context->draw(solelyTextureViewUpdates); } else { +#ifdef __ANDROID__ // Do a flush in case syncFrameState performed any texture uploads. Since we skipped // the draw() call, those uploads (or deletes) will end up sitting in the queue. // Do them now if (GrDirectContext* grContext = mRenderThread->getGrContext()) { grContext->flushAndSubmit(); } +#endif // wait on fences so tasks don't overlap next frame context->waitOnFences(); } @@ -176,11 +178,13 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) { bool canDraw = mContext->makeCurrent(); mContext->unpinImages(); +#ifdef __ANDROID__ for (size_t i = 0; i < mLayers.size(); i++) { if (mLayers[i]) { mLayers[i]->apply(); } } +#endif mLayers.clear(); mContext->setContentDrawBounds(mContentDrawBounds); diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 2904dfe76f40..708b0113e13e 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -442,14 +442,17 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, } // TODO: maybe we want to get rid of the WCG check if overlay properties just works? - const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() || - DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType; - - if (canUseFp16) { - if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { - colorMode = ColorMode::Default; - } else { - config = mEglConfigF16; + bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() || + DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType; + + if (colorMode == ColorMode::Hdr) { + if (canUseFp16 && !DeviceInfo::get()->isSupportRgba10101010ForHdr()) { + if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { + // If the driver doesn't support fp16 then fallback to 8-bit + canUseFp16 = false; + } else { + config = mEglConfigF16; + } } } diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp index 2362331aca26..6993d5240187 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.cpp +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -20,6 +20,7 @@ #include <private/performance_hint_private.h> #include <utils/Log.h> +#include <algorithm> #include <chrono> #include <vector> @@ -49,6 +50,7 @@ void HintSessionWrapper::HintSessionBinding::init() { BIND_APH_METHOD(updateTargetWorkDuration); BIND_APH_METHOD(reportActualWorkDuration); BIND_APH_METHOD(sendHint); + BIND_APH_METHOD(setThreads); mInitialized = true; } @@ -67,6 +69,10 @@ void HintSessionWrapper::destroy() { mHintSession = mHintSessionFuture->get(); mHintSessionFuture = std::nullopt; } + if (mSetThreadsFuture.has_value()) { + mSetThreadsFuture->wait(); + mSetThreadsFuture = std::nullopt; + } if (mHintSession) { mBinding->closeSession(mHintSession); mSessionValid = true; @@ -106,16 +112,16 @@ bool HintSessionWrapper::init() { APerformanceHintManager* manager = mBinding->getManager(); if (!manager) return false; - std::vector<pid_t> tids = CommonPool::getThreadIds(); - tids.push_back(mUiThreadId); - tids.push_back(mRenderThreadId); + mPermanentSessionTids = CommonPool::getThreadIds(); + mPermanentSessionTids.push_back(mUiThreadId); + mPermanentSessionTids.push_back(mRenderThreadId); // Use the cached target value if there is one, otherwise use a default. This is to ensure // the cached target and target in PowerHAL are consistent, and that it updates correctly // whenever there is a change. int64_t targetDurationNanos = mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration; - mHintSessionFuture = CommonPool::async([=, this, tids = std::move(tids)] { + mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] { return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos); }); return false; @@ -143,6 +149,23 @@ void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) { mLastFrameNotification = systemTime(); } +void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) { + if (!init()) return; + if (!mBinding || !mHintSession) return; + // Sort the vector to make sure they're compared as sets. + std::sort(threadIds.begin(), threadIds.end()); + if (threadIds == mActiveFunctorTids) return; + mActiveFunctorTids = std::move(threadIds); + std::vector<pid_t> combinedTids = mPermanentSessionTids; + std::copy(mActiveFunctorTids.begin(), mActiveFunctorTids.end(), + std::back_inserter(combinedTids)); + mSetThreadsFuture = CommonPool::async([this, tids = std::move(combinedTids)] { + int ret = mBinding->setThreads(mHintSession, tids.data(), tids.size()); + ALOGE_IF(ret != 0, "APerformaceHint_setThreads failed: %d", ret); + return ret; + }); +} + void HintSessionWrapper::sendLoadResetHint() { static constexpr int kMaxResetsSinceLastReport = 2; if (!init()) return; diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 41891cd80a42..14e7a53fd94f 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -20,6 +20,7 @@ #include <future> #include <optional> +#include <vector> #include "utils/TimeUtils.h" @@ -47,11 +48,15 @@ public: nsecs_t getLastUpdate(); void delayedDestroy(renderthread::RenderThread& rt, nsecs_t delay, std::shared_ptr<HintSessionWrapper> wrapperPtr); + // Must be called on Render thread. Otherwise can cause a race condition. + void setActiveFunctorThreads(std::vector<pid_t> threadIds); private: APerformanceHintSession* mHintSession = nullptr; // This needs to work concurrently for testing std::optional<std::shared_future<APerformanceHintSession*>> mHintSessionFuture; + // This needs to work concurrently for testing + std::optional<std::shared_future<int>> mSetThreadsFuture; int mResetsSinceLastReport = 0; nsecs_t mLastFrameNotification = 0; @@ -59,6 +64,8 @@ private: pid_t mUiThreadId; pid_t mRenderThreadId; + std::vector<pid_t> mPermanentSessionTids; + std::vector<pid_t> mActiveFunctorTids; bool mSessionValid = true; @@ -82,6 +89,8 @@ private: void (*reportActualWorkDuration)(APerformanceHintSession* session, int64_t actualDuration) = nullptr; void (*sendHint)(APerformanceHintSession* session, int32_t hintId) = nullptr; + int (*setThreads)(APerformanceHintSession* session, const pid_t* tids, + size_t size) = nullptr; private: bool mInitialized = false; diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index b8c3a4de2bd4..ee1d1f8789d9 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -30,8 +30,6 @@ #include "SwapBehavior.h" #include "hwui/Bitmap.h" -class GrDirectContext; - struct ANativeWindow; namespace android { @@ -94,7 +92,6 @@ public: virtual void setSurfaceColorProperties(ColorMode colorMode) = 0; virtual SkColorType getSurfaceColorType() const = 0; virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0; - virtual GrSurfaceOrigin getSurfaceOrigin() = 0; virtual void setPictureCapturedCallback( const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0; diff --git a/libs/hwui/renderthread/ReliableSurface.h b/libs/hwui/renderthread/ReliableSurface.h index 595964741049..d6a4d50d3327 100644 --- a/libs/hwui/renderthread/ReliableSurface.h +++ b/libs/hwui/renderthread/ReliableSurface.h @@ -21,7 +21,9 @@ #include <apex/window.h> #include <utils/Errors.h> #include <utils/Macros.h> +#ifdef __ANDROID__ #include <utils/NdkUtils.h> +#endif #include <utils/StrongPointer.h> #include <memory> @@ -62,9 +64,11 @@ private: mutable std::mutex mMutex; uint64_t mUsage = AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER; +#ifdef __ANDROID__ AHardwareBuffer_Format mFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; UniqueAHardwareBuffer mScratchBuffer; ANativeWindowBuffer* mReservedBuffer = nullptr; +#endif base::unique_fd mReservedFenceFd; bool mHasDequeuedBuffer = false; int mBufferQueueState = OK; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index eab36050896f..715153b5083d 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -42,7 +42,11 @@ namespace renderthread { RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory) : mRenderThread(RenderThread::getInstance()), mContext(nullptr) { +#ifdef __ANDROID__ pid_t uiThreadId = pthread_gettid_np(pthread_self()); +#else + pid_t uiThreadId = 0; +#endif pid_t renderThreadId = getRenderThreadTid(); mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* { CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode, @@ -90,6 +94,7 @@ void RenderProxy::setName(const char* name) { } void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) { +#ifdef __ANDROID__ if (buffer) { AHardwareBuffer_acquire(buffer); } @@ -99,6 +104,7 @@ void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) { AHardwareBuffer_release(hardwareBuffer); } }); +#endif } void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) { @@ -216,7 +222,9 @@ void RenderProxy::cancelLayerUpdate(DeferredLayerUpdater* layer) { } void RenderProxy::detachSurfaceTexture(DeferredLayerUpdater* layer) { +#ifdef __ANDROID__ return mRenderThread.queue().runSync([&]() { layer->detachSurfaceTexture(); }); +#endif } void RenderProxy::destroyHardwareResources() { @@ -324,11 +332,13 @@ void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData, bool reset } }); } +#ifdef __ANDROID__ if (!Properties::isolatedProcess) { std::string grallocInfo; GraphicBufferAllocator::getInstance().dump(grallocInfo); dprintf(fd, "%s\n", grallocInfo.c_str()); } +#endif } void RenderProxy::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) { @@ -352,7 +362,11 @@ void RenderProxy::rotateProcessStatsBuffer() { } int RenderProxy::getRenderThreadTid() { +#ifdef __ANDROID__ return mRenderThread.getTid(); +#else + return 0; +#endif } void RenderProxy::addRenderNode(RenderNode* node, bool placeFront) { @@ -461,7 +475,7 @@ void RenderProxy::prepareToDraw(Bitmap& bitmap) { int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { ATRACE_NAME("HardwareBitmap readback"); RenderThread& thread = RenderThread::getInstance(); - if (gettid() == thread.getTid()) { + if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); } else { @@ -472,7 +486,7 @@ int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { int RenderProxy::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) { RenderThread& thread = RenderThread::getInstance(); - if (gettid() == thread.getTid()) { + if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. return (int)thread.readback().copyImageInto(image, bitmap); } else { diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index b5f7caaf1b5b..0d0af1110ca4 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -25,6 +25,7 @@ #include <android/sync.h> #include <gui/TraceUtils.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> +#include <include/gpu/ganesh/vk/GrVkBackendSemaphore.h> #include <include/gpu/ganesh/vk/GrVkBackendSurface.h> #include <include/gpu/ganesh/vk/GrVkDirectContext.h> #include <ui/FatVector.h> @@ -597,15 +598,14 @@ Frame VulkanManager::dequeueNextBuffer(VulkanSurface* surface) { close(fence_clone); sync_wait(bufferInfo->dequeue_fence, -1 /* forever */); } else { - GrBackendSemaphore backendSemaphore; - backendSemaphore.initVulkan(semaphore); + GrBackendSemaphore beSemaphore = GrBackendSemaphores::MakeVk(semaphore); // Skia will take ownership of the VkSemaphore and delete it once the wait // has finished. The VkSemaphore also owns the imported fd, so it will // close the fd when it is deleted. - bufferInfo->skSurface->wait(1, &backendSemaphore); + bufferInfo->skSurface->wait(1, &beSemaphore); // The following flush blocks the GPU immediately instead of waiting for // other drawing ops. It seems dequeue_fence is not respected otherwise. - // TODO: remove the flush after finding why backendSemaphore is not working. + // TODO: remove the flush after finding why beSemaphore is not working. skgpu::ganesh::FlushAndSubmit(bufferInfo->skSurface.get()); } } @@ -626,7 +626,7 @@ class SharedSemaphoreInfo : public LightRefBase<SharedSemaphoreInfo> { SharedSemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device, VkSemaphore semaphore) : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) { - mGrBackendSemaphore.initVulkan(semaphore); + mGrBackendSemaphore = GrBackendSemaphores::MakeVk(mSemaphore); } ~SharedSemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); } @@ -798,8 +798,7 @@ status_t VulkanManager::fenceWait(int fence, GrDirectContext* grContext) { return UNKNOWN_ERROR; } - GrBackendSemaphore beSemaphore; - beSemaphore.initVulkan(semaphore); + GrBackendSemaphore beSemaphore = GrBackendSemaphores::MakeVk(semaphore); // Skia will take ownership of the VkSemaphore and delete it once the wait has finished. The // VkSemaphore also owns the imported fd, so it will close the fd when it is deleted. diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp index 10a740a1f803..c16602c29e2a 100644 --- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp +++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp @@ -58,6 +58,7 @@ protected: MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t)); MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t)); MOCK_METHOD(void, fakeSendHint, (APerformanceHintSession*, int32_t)); + MOCK_METHOD(int, fakeSetThreads, (APerformanceHintSession*, const std::vector<pid_t>&)); // Needs to be on the binding so it can be accessed from static methods std::promise<int> allowCreationToFinish; }; @@ -102,11 +103,20 @@ protected: static void stubSendHint(APerformanceHintSession* session, int32_t hintId) { sMockBinding->fakeSendHint(session, hintId); }; + static int stubSetThreads(APerformanceHintSession* session, const pid_t* ids, size_t size) { + std::vector<pid_t> tids(ids, ids + size); + return sMockBinding->fakeSetThreads(session, tids); + } void waitForWrapperReady() { if (mWrapper->mHintSessionFuture.has_value()) { mWrapper->mHintSessionFuture->wait(); } } + void waitForSetThreadsReady() { + if (mWrapper->mSetThreadsFuture.has_value()) { + mWrapper->mSetThreadsFuture->wait(); + } + } void scheduleDelayedDestroyManaged() { TestUtils::runOnRenderThread([&](renderthread::RenderThread& rt) { // Guaranteed to be scheduled first, allows destruction to start @@ -130,6 +140,7 @@ void HintSessionWrapperTests::SetUp() { mWrapper->mBinding = sMockBinding; EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr)); ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr)); + ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0)); } void HintSessionWrapperTests::MockHintSessionBinding::init() { @@ -141,6 +152,7 @@ void HintSessionWrapperTests::MockHintSessionBinding::init() { sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration; sMockBinding->reportActualWorkDuration = &stubReportActualWorkDuration; sMockBinding->sendHint = &stubSendHint; + sMockBinding->setThreads = &stubSetThreads; } void HintSessionWrapperTests::TearDown() { @@ -339,4 +351,44 @@ TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct EXPECT_EQ(mWrapper->alive(), false); } +TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) { + EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22}))) + .Times(1); + mWrapper->init(); + waitForWrapperReady(); + + // This changes the overall set of threads in the session, so the session wrapper should call + // setThreads. + mWrapper->setActiveFunctorThreads({11, 22}); + waitForSetThreadsReady(); + + // The set of threads doesn't change, so the session wrapper should not call setThreads this + // time. The order of the threads shouldn't matter. + mWrapper->setActiveFunctorThreads({22, 11}); + waitForSetThreadsReady(); +} + +TEST_F(HintSessionWrapperTests, setThreadsDoesntCrashAfterDestroy) { + EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); + + mWrapper->init(); + waitForWrapperReady(); + // Init a second time just to grab the wrapper from the promise + mWrapper->init(); + EXPECT_EQ(mWrapper->alive(), true); + + // Then, kill the session + mWrapper->destroy(); + + // Verify it died + Mock::VerifyAndClearExpectations(sMockBinding.get()); + EXPECT_EQ(mWrapper->alive(), false); + + // setActiveFunctorThreads shouldn't do anything, and shouldn't crash. + EXPECT_CALL(*sMockBinding, fakeSetThreads(_, _)).Times(0); + mWrapper->setActiveFunctorThreads({11, 22}); + waitForSetThreadsReady(); +} + } // namespace android::uirenderer::renderthread
\ No newline at end of file diff --git a/libs/hwui/thread/CommonPool.cpp b/libs/hwui/thread/CommonPool.cpp index dc92f9f0d39a..6c0c30f95955 100644 --- a/libs/hwui/thread/CommonPool.cpp +++ b/libs/hwui/thread/CommonPool.cpp @@ -16,16 +16,14 @@ #include "CommonPool.h" -#include <sys/resource.h> #include <utils/Trace.h> -#include "renderthread/RenderThread.h" #include <array> namespace android { namespace uirenderer { -CommonPool::CommonPool() { +CommonPool::CommonPool() : CommonPoolBase() { ATRACE_CALL(); CommonPool* pool = this; @@ -36,22 +34,7 @@ CommonPool::CommonPool() { // Create 2 workers for (int i = 0; i < THREAD_COUNT; i++) { std::thread worker([pool, i, &mLock, &tids, &tidConditionVars] { - { - std::array<char, 20> name{"hwuiTask"}; - snprintf(name.data(), name.size(), "hwuiTask%d", i); - auto self = pthread_self(); - pthread_setname_np(self, name.data()); - { - std::unique_lock lock(mLock); - tids[i] = pthread_gettid_np(self); - tidConditionVars[i].notify_one(); - } - setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND); - auto startHook = renderthread::RenderThread::getOnStartHook(); - if (startHook) { - startHook(name.data()); - } - } + pool->setupThread(i, mLock, tids, tidConditionVars); pool->workerLoop(); }); worker.detach(); @@ -64,7 +47,9 @@ CommonPool::CommonPool() { } } } - mWorkerThreadIds = std::move(tids); + if (pool->supportsTid()) { + mWorkerThreadIds = std::move(tids); + } } CommonPool& CommonPool::instance() { @@ -95,7 +80,7 @@ void CommonPool::enqueue(Task&& task) { void CommonPool::workerLoop() { std::unique_lock lock(mLock); - while (true) { + while (!mIsStopping) { if (!mWorkQueue.hasWork()) { mWaitingThreads++; mCondition.wait(lock); diff --git a/libs/hwui/thread/CommonPool.h b/libs/hwui/thread/CommonPool.h index 74f852bd1413..0c025b4f0ee7 100644 --- a/libs/hwui/thread/CommonPool.h +++ b/libs/hwui/thread/CommonPool.h @@ -17,8 +17,6 @@ #ifndef FRAMEWORKS_BASE_COMMONPOOL_H #define FRAMEWORKS_BASE_COMMONPOOL_H -#include "utils/Macros.h" - #include <log/log.h> #include <condition_variable> @@ -27,6 +25,9 @@ #include <mutex> #include <vector> +#include "thread/CommonPoolBase.h" +#include "utils/Macros.h" + namespace android { namespace uirenderer { @@ -73,7 +74,7 @@ private: int mTail = 0; }; -class CommonPool { +class CommonPool : private CommonPoolBase { PREVENT_COPY_AND_ASSIGN(CommonPool); public: @@ -107,7 +108,10 @@ private: static CommonPool& instance(); CommonPool(); - ~CommonPool() {} + ~CommonPool() { + mIsStopping = true; + mCondition.notify_all(); + } void enqueue(Task&&); void doWaitForIdle(); @@ -120,6 +124,7 @@ private: std::condition_variable mCondition; int mWaitingThreads = 0; ArrayQueue<Task, QUEUE_SIZE> mWorkQueue; + std::atomic_bool mIsStopping = false; }; } // namespace uirenderer diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 913af8ac3474..f6c57927cc85 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -16,22 +16,18 @@ #include "Color.h" -#include <ui/ColorSpace.h> -#include <utils/Log.h> - -#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows +#include <Properties.h> #include <android/hardware_buffer.h> #include <android/native_window.h> -#endif +#include <ui/ColorSpace.h> +#include <utils/Log.h> #include <algorithm> #include <cmath> -#include <Properties.h> namespace android { namespace uirenderer { -#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows static inline SkImageInfo createImageInfo(int32_t width, int32_t height, int32_t format, sk_sp<SkColorSpace> colorSpace) { SkColorType colorType = kUnknown_SkColorType; @@ -121,7 +117,6 @@ SkColorType BufferFormatToColorType(uint32_t format) { return kUnknown_SkColorType; } } -#endif namespace { static constexpr skcms_TransferFunction k2Dot6 = {2.6f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h index 0fd61c7b990b..08f1c9300c30 100644 --- a/libs/hwui/utils/Color.h +++ b/libs/hwui/utils/Color.h @@ -92,7 +92,6 @@ static constexpr float EOCF_sRGB(float srgb) { return srgb <= 0.04045f ? srgb / 12.92f : powf((srgb + 0.055f) / 1.055f, 2.4f); } -#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer, sk_sp<SkColorSpace> colorSpace); @@ -101,7 +100,6 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc, uint32_t ColorTypeToBufferFormat(SkColorType colorType); SkColorType BufferFormatToColorType(uint32_t bufferFormat); -#endif ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace); diff --git a/libs/hwui/utils/SharedLib.h b/libs/hwui/utils/SharedLib.h new file mode 100644 index 000000000000..f4dcf0f664a2 --- /dev/null +++ b/libs/hwui/utils/SharedLib.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHAREDLIB_H +#define SHAREDLIB_H + +#include <string> + +namespace android { +namespace uirenderer { + +class SharedLib { +public: + static void* openSharedLib(std::string filename); + static void* getSymbol(void* library, const char* symbol); +}; + +} /* namespace uirenderer */ +} /* namespace android */ + +#endif // SHAREDLIB_H diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index 6dc45a6aebec..a63453d655e2 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -148,8 +148,9 @@ void SpriteController::doUpdateSprites() { if (update.state.wantSurfaceVisible()) { int32_t desiredWidth = update.state.icon.width(); int32_t desiredHeight = update.state.icon.height(); - if (update.state.surfaceWidth < desiredWidth - || update.state.surfaceHeight < desiredHeight) { + // TODO(b/331260947): investigate using a larger surface width with smaller sprites. + if (update.state.surfaceWidth != desiredWidth || + update.state.surfaceHeight != desiredHeight) { needApplyTransaction = true; update.state.surfaceControl->updateDefaultBufferSize(desiredWidth, desiredHeight); @@ -202,11 +203,13 @@ void SpriteController::doUpdateSprites() { && update.state.surfaceDrawn; bool becomingVisible = wantSurfaceVisibleAndDrawn && !update.state.surfaceVisible; bool becomingHidden = !wantSurfaceVisibleAndDrawn && update.state.surfaceVisible; - if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden - || (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA - | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER - | DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID - | DIRTY_ICON_STYLE))))) { + if (update.state.surfaceControl != NULL && + (becomingVisible || becomingHidden || + (wantSurfaceVisibleAndDrawn && + (update.state.dirty & + (DIRTY_ALPHA | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER | + DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID | DIRTY_ICON_STYLE | + DIRTY_DRAW_DROP_SHADOW))))) { needApplyTransaction = true; if (wantSurfaceVisibleAndDrawn @@ -235,13 +238,15 @@ void SpriteController::doUpdateSprites() { update.state.transformationMatrix.dtdy); } - if (wantSurfaceVisibleAndDrawn - && (becomingVisible - || (update.state.dirty & (DIRTY_HOTSPOT | DIRTY_ICON_STYLE)))) { + if (wantSurfaceVisibleAndDrawn && + (becomingVisible || + (update.state.dirty & + (DIRTY_HOTSPOT | DIRTY_ICON_STYLE | DIRTY_DRAW_DROP_SHADOW)))) { Parcel p; p.writeInt32(static_cast<int32_t>(update.state.icon.style)); p.writeFloat(update.state.icon.hotSpotX); p.writeFloat(update.state.icon.hotSpotY); + p.writeBool(update.state.icon.drawNativeDropShadow); // Pass cursor metadata in the sprite surface so that when Android is running as a // client OS (e.g. ARC++) the host OS can get the requested cursor metadata and @@ -388,12 +393,13 @@ void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) { uint32_t dirty; if (icon.isValid()) { mLocked.state.icon.bitmap = icon.bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888); - if (!mLocked.state.icon.isValid() - || mLocked.state.icon.hotSpotX != icon.hotSpotX - || mLocked.state.icon.hotSpotY != icon.hotSpotY) { + if (!mLocked.state.icon.isValid() || mLocked.state.icon.hotSpotX != icon.hotSpotX || + mLocked.state.icon.hotSpotY != icon.hotSpotY || + mLocked.state.icon.drawNativeDropShadow != icon.drawNativeDropShadow) { mLocked.state.icon.hotSpotX = icon.hotSpotX; mLocked.state.icon.hotSpotY = icon.hotSpotY; - dirty = DIRTY_BITMAP | DIRTY_HOTSPOT; + mLocked.state.icon.drawNativeDropShadow = icon.drawNativeDropShadow; + dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_DRAW_DROP_SHADOW; } else { dirty = DIRTY_BITMAP; } @@ -404,7 +410,7 @@ void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) { } } else if (mLocked.state.icon.isValid()) { mLocked.state.icon.bitmap.reset(); - dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_ICON_STYLE; + dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_ICON_STYLE | DIRTY_DRAW_DROP_SHADOW; } else { return; // setting to invalid icon and already invalid so nothing to do } diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 04ecb3895aa2..35776e9961b3 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -151,6 +151,7 @@ private: DIRTY_HOTSPOT = 1 << 6, DIRTY_DISPLAY_ID = 1 << 7, DIRTY_ICON_STYLE = 1 << 8, + DIRTY_DRAW_DROP_SHADOW = 1 << 9, }; /* Describes the state of a sprite. diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h index 0939af46c258..7d45d02b4a6f 100644 --- a/libs/input/SpriteIcon.h +++ b/libs/input/SpriteIcon.h @@ -40,7 +40,7 @@ struct SpriteIcon { PointerIconStyle style{PointerIconStyle::TYPE_NULL}; float hotSpotX{}; float hotSpotY{}; - bool drawNativeDropShadow{false}; + bool drawNativeDropShadow{}; inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); } |