diff options
Diffstat (limited to 'libs')
598 files changed, 25169 insertions, 6416 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/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java index d923a46c3b5d..d24164159b2b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java @@ -16,6 +16,8 @@ package androidx.window.common; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Activity; import android.app.Application; import android.os.Bundle; @@ -26,30 +28,30 @@ import android.os.Bundle; */ public class EmptyLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { } @Override - public void onActivityStarted(Activity activity) { + public void onActivityStarted(@NonNull Activity activity) { } @Override - public void onActivityResumed(Activity activity) { + public void onActivityResumed(@NonNull Activity activity) { } @Override - public void onActivityPaused(Activity activity) { + public void onActivityPaused(@NonNull Activity activity) { } @Override - public void onActivityStopped(Activity activity) { + public void onActivityStopped(@NonNull Activity activity) { } @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { } @Override - public void onActivityDestroyed(Activity activity) { + public void onActivityDestroyed(@NonNull Activity activity) { } } 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..29936cc2cac3 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -0,0 +1,1371 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.Activity; +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.ColorDrawable; +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.VelocityTracker; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.view.animation.PathInterpolator; +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.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; + + @VisibleForTesting + static final PathInterpolator FLING_ANIMATION_INTERPOLATOR = + new PathInterpolator(0.4f, 0f, 0.2f, 1f); + @VisibleForTesting + static final int FLING_ANIMATION_DURATION = 250; + @VisibleForTesting + static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; + @VisibleForTesting + static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; + + private final int mTaskId; + + @NonNull + private final Object mLock = new Object(); + + @NonNull + private final DragEventCallback mDragEventCallback; + + @NonNull + private final Executor mCallbackExecutor; + + /** + * The VelocityTracker of the divider, used to track the dragging velocity. This field is + * {@code null} until dragging starts. + */ + @GuardedBy("mLock") + @Nullable + VelocityTracker mVelocityTracker; + + /** + * 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, + getContainerBackgroundColor(topSplitContainer.getPrimaryContainer(), + DEFAULT_PRIMARY_VEIL_COLOR), + getContainerBackgroundColor(topSplitContainer.getSecondaryContainer(), + DEFAULT_SECONDARY_VEIL_COLOR) + )); + } + } + + @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); + } + } + + /** + * Returns the window background color of the top activity in the container if set, or the + * default color if the background color of the top activity is unavailable. + */ + @VisibleForTesting + @NonNull + static Color getContainerBackgroundColor( + @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { + final Activity activity = container.getTopNonFinishingActivity(); + if (activity == null) { + // This can happen when the activities in the container are from a different process. + // TODO(b/340984203) Report whether the top activity is in the same process. Use default + // color if not. + return defaultColor; + } + + final Drawable drawable = activity.getWindow().getDecorView().getBackground(); + if (drawable instanceof ColorDrawable colorDrawable) { + return Color.valueOf(colorDrawable.getColor()); + } + return defaultColor; + } + + /** + * 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 float getDisplayDensity() { + // TODO(b/329193115) support divider on secondary display + final Context applicationContext = + ActivityThread.currentActivityThread().getApplication(); + return applicationContext.getResources().getDisplayMetrics().density; + } + + /** + * 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(); + float minRatio = dividerAttributes.getPrimaryMinRatio(); + float maxRatio = dividerAttributes.getPrimaryMaxRatio(); + + if (widthDp == WIDTH_SYSTEM_DEFAULT) { + widthDp = DEFAULT_DIVIDER_WIDTH_DP; + } + + if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // Update minRatio and maxRatio only when it is a draggable divider. + if (minRatio == RATIO_SYSTEM_DEFAULT) { + minRatio = DEFAULT_MIN_RATIO; + } + 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) { + if (mProperties != null && mRenderer != null) { + 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(event); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFinishDragging(event); + break; + case MotionEvent.ACTION_MOVE: + onDrag(event); + 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(@NonNull MotionEvent event) { + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(event); + + mRenderer.mIsDragging = true; + mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); + mRenderer.updateSurface(); + + // Veil visibility change should be applied together with the surface boost transaction in + // the wct. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + 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(@NonNull MotionEvent event) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + mRenderer.updateSurface(); + } + + @GuardedBy("mLock") + private void onFinishDragging(@NonNull MotionEvent event) { + float velocity = 0.0f; + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(1000 /* units */); + velocity = mProperties.mIsVerticalSplit + ? mVelocityTracker.getXVelocity() + : mVelocityTracker.getYVelocity(); + mVelocityTracker.recycle(); + } + + final int prevDividerPosition = mDividerPosition; + mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity); + if (mDividerPosition != prevDividerPosition) { + ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition); + animator.start(); + } else { + onDraggingEnd(); + } + } + + @GuardedBy("mLock") + @NonNull + @VisibleForTesting + ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) { + final ValueAnimator animator = + getValueAnimator(prevDividerPosition, snappedDividerPosition); + animator.addUpdateListener(animation -> { + synchronized (mLock) { + updateDividerPosition((int) animation.getAnimatedValue()); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + synchronized (mLock) { + onDraggingEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + synchronized (mLock) { + onDraggingEnd(); + } + } + }); + return animator; + } + + @VisibleForTesting + static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) { + ValueAnimator animator = ValueAnimator + .ofInt(prevDividerPosition, snappedDividerPosition) + .setDuration(FLING_ANIMATION_DURATION); + animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR); + return animator; + } + + @GuardedBy("mLock") + private void updateDividerPosition(int position) { + mRenderer.setDividerPosition(position); + mRenderer.updateSurface(); + } + + @GuardedBy("mLock") + private void onDraggingEnd() { + // Veil visibility change should be applied together with the surface boost transaction in + // the wct. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + 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. + * 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 dividerPositionForSnapPoints(int dividerPosition, float velocity) { + 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)) { + final float displayDensity = getDisplayDensity(); + return dividerPositionWithDraggingToFullscreenAllowed( + dividerPosition, + minPosition, + maxPosition, + fullyExpandedPosition, + velocity, + displayDensity); + } + return Math.clamp(dividerPosition, minPosition, maxPosition); + } + + /** + * Returns the divider position given a set of position options. A snap algorithm is used to + * adjust the ending position to either fully expand one container or move the divider back to + * the specified min/max ratio depending on the dragging velocity. + */ + @VisibleForTesting + static int dividerPositionWithDraggingToFullscreenAllowed(int dividerPosition, int minPosition, + int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity) { + final float minDismissVelocityPxPerSecond = + MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; + final float minFlingVelocityPxPerSecond = + MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; + if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { + return 0; + } + if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { + return fullyExpandedPosition; + } + if (Math.abs(velocity) < minFlingVelocityPxPerSecond) { + if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { + return dividerPosition; + } + int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition}; + return snap(dividerPosition, possiblePositions); + } + if (velocity < 0) { + return 0; + } else { + return fullyExpandedPosition; + } + } + + /** Calculates the snapped divider position based on the possible positions and distance. */ + private static int snap(int dividerPosition, int[] possiblePositions) { + int snappedPosition = dividerPosition; + float minDistance = Float.MAX_VALUE; + for (int position : possiblePositions) { + float distance = Math.abs(dividerPosition - position); + if (distance < minDistance) { + snappedPosition = position; + minDistance = distance; + } + } + return snappedPosition; + } + + 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; + private final Color mPrimaryVeilColor; + private final Color mSecondaryVeilColor; + + @VisibleForTesting + Properties( + @NonNull Configuration configuration, + @NonNull DividerAttributes dividerAttributes, + @NonNull SurfaceControl decorSurface, + int initialDividerPosition, + boolean isVerticalSplit, + boolean isReversedLayout, + int displayId, + boolean isDraggableExpandType, + @NonNull Color primaryVeilColor, + @NonNull Color secondaryVeilColor) { + mConfiguration = configuration; + mDividerAttributes = dividerAttributes; + mDecorSurface = decorSurface; + mInitialDividerPosition = initialDividerPosition; + mIsVerticalSplit = isVerticalSplit; + mIsReversedLayout = isReversedLayout; + mDisplayId = displayId; + mIsDraggableExpandType = isDraggableExpandType; + mPrimaryVeilColor = primaryVeilColor; + mSecondaryVeilColor = secondaryVeilColor; + } + + /** + * 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 + && a.mPrimaryVeilColor.equals(b.mPrimaryVeilColor) + && a.mSecondaryVeilColor.equals(b.mSecondaryVeilColor); + } + + 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 mDividerLine; + private View mDragHandle; + @NonNull + private final View.OnTouchListener mListener; + @NonNull + private Properties mProperties; + private int mDividerWidthPx; + private int mHandleWidthPx; + @Nullable + private SurfaceControl mPrimaryVeil; + @Nullable + private SurfaceControl mSecondaryVeil; + private boolean mIsDragging; + private int mDividerPosition; + private int mDividerSurfaceWidthPx; + + 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); + mDividerLine = new View(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); + + if (mProperties.mDividerAttributes.getDividerType() + == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // TODO(b/329193115) support divider on secondary display + final Context context = ActivityThread.currentActivityThread().getApplication(); + mHandleWidthPx = context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width); + } else { + mHandleWidthPx = 0; + } + + // 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. + * + * This method applies the changes in a stand-alone surface transaction immediately. + */ + private void updateSurface() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + updateSurface(t); + t.apply(); + } + + /** + * 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. + * + * This method applies the changes in the provided surface transaction and can be synced + * with other changes. + */ + private void updateSurface(@NonNull SurfaceControl.Transaction t) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + + int dividerSurfacePosition; + if (mProperties.mDividerAttributes.getDividerType() + == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // When the divider drag handle width is larger than the divider width, the position + // of the divider surface is adjusted so that it is large enough to host both the + // divider line and the divider drag handle. + mDividerSurfaceWidthPx = Math.max(mDividerWidthPx, mHandleWidthPx); + dividerSurfacePosition = + mProperties.mIsReversedLayout + ? mDividerPosition + : mDividerPosition + mDividerWidthPx - mDividerSurfaceWidthPx; + dividerSurfacePosition = Math.clamp(dividerSurfacePosition, 0, + mProperties.mIsVerticalSplit ? taskBounds.width() : taskBounds.height()); + } else { + mDividerSurfaceWidthPx = mDividerWidthPx; + dividerSurfacePosition = mDividerPosition; + } + + if (mProperties.mIsVerticalSplit) { + t.setPosition(mDividerSurface, dividerSurfacePosition, 0.0f); + t.setWindowCrop(mDividerSurface, mDividerSurfaceWidthPx, taskBounds.height()); + } else { + t.setPosition(mDividerSurface, 0.0f, dividerSurfacePosition); + t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerSurfaceWidthPx); + } + + // Update divider line position in the surface + if (!mProperties.mIsReversedLayout) { + final int offset = mDividerPosition - dividerSurfacePosition; + mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); + mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); + } else { + // For reversed layout, the divider line is always at the start of the divider + // surface. + mDividerLine.setX(0); + mDividerLine.setY(0); + } + + 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( + mDividerSurfaceWidthPx, + taskBounds.height(), + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT) + : new WindowManager.LayoutParams( + taskBounds.width(), + mDividerSurfaceWidthPx, + 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(); + mDividerLayout.addView(mDividerLine); + if (mProperties.mIsDraggableExpandType && !mIsDragging) { + // If a container is fully expanded, the divider overlays on the expanded container. + mDividerLine.setBackgroundColor(Color.TRANSPARENT); + } else { + mDividerLine.setBackgroundColor(mProperties.mDividerAttributes.getDividerColor()); + } + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + mDividerLine.setLayoutParams( + mProperties.mIsVerticalSplit + ? new FrameLayout.LayoutParams(mDividerWidthPx, taskBounds.height()) + : new FrameLayout.LayoutParams(taskBounds.width(), mDividerWidthPx) + ); + 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(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(mProperties.mPrimaryVeilColor)) + .setColor(mSecondaryVeil, colorToFloatArray(mProperties.mSecondaryVeilColor)) + .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..f9a6caf42e6e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -21,6 +21,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -165,10 +166,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); @@ -353,14 +355,21 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean isolatedNav) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build(); + OP_TYPE_SET_ISOLATED_NAVIGATION).setBooleanValue(isolatedNav).build(); + wct.addTaskFragmentOperation(fragmentToken, operation); + } + + void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, boolean pinned) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_PINNED).setBooleanValue(pinned).build(); wct.addTaskFragmentOperation(fragmentToken, operation); } void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean dimOnTask) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_DIM_ON_TASK).setDimOnTask(dimOnTask).build(); + OP_TYPE_SET_DIM_ON_TASK).setBooleanValue(dimOnTask).build(); wct.addTaskFragmentOperation(fragmentToken, operation); } 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 3061d9789255..13c2d1f73461 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -21,12 +21,14 @@ 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; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE; +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE; import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; @@ -56,6 +58,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; @@ -65,7 +68,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.SystemProperties; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -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,15 +105,20 @@ 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); + static final boolean ENABLE_SHELL_TRANSITIONS = 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") @@ -161,6 +168,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 +189,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(); @@ -324,8 +350,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Resets the isolated navigation and updates the container. final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); final WindowContainerTransaction wct = transactionRecord.getTransaction(); - mPresenter.setTaskFragmentIsolatedNavigation(wct, containerToUnpin, - false /* isolated */); + mPresenter.setTaskFragmentPinned(wct, containerToUnpin, false /* pinned */); updateContainer(wct, containerToUnpin); transactionRecord.apply(false /* shouldApplyIndependently */); updateCallbackIfNecessary(); @@ -394,7 +419,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 +432,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,7 +854,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!parentInfo.isVisible()) { // Only making the TaskContainer invisible and drops the other info, and perform the // update when the next time the Task becomes visible. - taskContainer.setIsVisible(false); + if (taskContainer.isVisible()) { + taskContainer.setInvisible(); + } return; } @@ -837,9 +864,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; @@ -998,6 +1030,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; } @@ -1044,8 +1077,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return true; } - // Skip resolving if the activity is on an isolated navigated TaskFragmentContainer. - if (container != null && container.isIsolatedNavigationEnabled()) { + if (container != null && container.shouldSkipActivityResolving()) { return true; } @@ -1216,7 +1248,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)); @@ -1386,9 +1418,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. @@ -1398,7 +1448,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. @@ -1479,12 +1529,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { - // Skip resolving if started from an isolated navigated TaskFragmentContainer. if (launchingActivity != null) { final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity( launchingActivity); if (taskFragmentContainer != null - && taskFragmentContainer.isIsolatedNavigationEnabled()) { + && taskFragmentContainer.shouldSkipActivityResolving()) { + return null; + } + if (isAssociatedWithOverlay(launchingActivity)) { + // Skip resolving if the launching activity associated with an overlay. return null; } } @@ -1578,7 +1631,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 */); } /** @@ -1593,7 +1647,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; @@ -1611,15 +1665,14 @@ 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. final TaskContainer taskContainer = container.getTaskContainer(); // TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken. - final Rect taskBounds = taskContainer.getBounds(); final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(), - getMinDimensions(intent), taskBounds); + getMinDimensions(intent), container); final int windowingMode = taskContainer .getWindowingModeForTaskFragment(sanitizedBounds); mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(), @@ -1680,17 +1733,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - // Check pending appeared activity first because there can be a delay for the server - // update. - TaskFragmentContainer taskFragmentContainer = - getContainer(container -> container.hasPendingAppearedActivity(activityToken)); - if (taskFragmentContainer != null) { - return taskFragmentContainer; + for (int i = mTaskContainers.size() - 1; i >= 0; --i) { + final TaskFragmentContainer container = mTaskContainers.valueAt(i) + .getContainerWithActivity(activityToken); + if (container != null) { + return container; + } } - - - // Check appeared activity if there is no such pending appeared activity. - return getContainer(container -> container.hasAppearedActivity(activityToken)); + return null; } @GuardedBy("mLock") @@ -1703,7 +1753,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") @@ -1711,7 +1761,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") @@ -1720,7 +1770,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 */); } /** @@ -1732,29 +1782,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; } @@ -1920,7 +1973,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; @@ -1943,7 +1996,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); - if (dismissOverlayContainerIfNeeded(wct, taskContainer)) { + if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) { return; } @@ -1967,22 +2020,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; } /** @@ -2046,19 +2104,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return null; } - final List<SplitContainer> splitContainers = - container.getTaskContainer().getSplitContainers(); - if (splitContainers.isEmpty()) { - return null; - } - for (int i = splitContainers.size() - 1; i >= 0; i--) { - final SplitContainer splitContainer = splitContainers.get(i); - if (container.equals(splitContainer.getSecondaryContainer()) - || container.equals(splitContainer.getPrimaryContainer())) { - return splitContainer; - } - } - return null; + return container.getTaskContainer().getActiveSplitForContainer(container); } /** @@ -2108,6 +2154,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + if (isAssociatedWithOverlay(activity)) { + // Can't launch the placeholder if the activity associates an overlay. + return false; + } + final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null && !allowLaunchPlaceholder(container)) { // We don't allow activity in this TaskFragment to launch placeholder. @@ -2121,6 +2172,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()); @@ -2143,6 +2198,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) { + if (container.isOverlay()) { + // Don't launch placeholder if the container is an overlay. + return false; + } + final TaskFragmentContainer topContainer = container.getTaskContainer() .getTopNonFinishingTaskFragmentContainer(); if (container != topContainer) { @@ -2216,6 +2276,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); @@ -2413,13 +2476,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i) - .getTaskFragmentContainers(); - for (int j = containers.size() - 1; j >= 0; j--) { - final TaskFragmentContainer container = containers.get(j); - if (predicate.test(container)) { - return container; - } + final TaskFragmentContainer container = mTaskContainers.valueAt(i) + .getContainer(predicate); + if (container != null) { + return container; } } return null; @@ -2464,6 +2524,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; } @@ -2476,8 +2543,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; } @@ -2560,25 +2626,52 @@ 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; } + @GuardedBy("mLock") + private boolean isAssociatedWithOverlay(@NonNull Activity activity) { + final TaskContainer taskContainer = getTaskContainer(getTaskId(activity)); + if (taskContainer == null) { + return false; + } + return taskContainer.getContainer(c -> c.isOverlay() && !c.isFinished() + && c.getAssociatedActivityToken() == activity.getActivityToken()) != null; + } + + /** + * 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 @@ -2589,8 +2682,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. @@ -2611,35 +2706,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.isOverlayWithActivityAssociation()) { + // 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 { @@ -2728,6 +2879,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 @@ -2735,7 +2903,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 */); } } } @@ -2884,17 +3057,100 @@ 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 + 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 static ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { + 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 taskBounds = new Rect(activityWindowInfo.getTaskBounds()); + final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds()); + return new EmbeddedActivityWindowInfo(activity, isEmbedded, 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 @@ -2965,4 +3221,50 @@ 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(); + 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(); + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_DRAG_RESIZE); + 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..27048136afd8 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, @@ -399,18 +401,26 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return; } - setTaskFragmentIsolatedNavigation(wct, secondaryContainer, !isStacked /* isolatedNav */); + setTaskFragmentPinned(wct, secondaryContainer, !isStacked /* pinned */); if (isStacked && !splitPinRule.isSticky()) { secondaryContainer.getTaskContainer().removeSplitPinContainer(); } } /** - * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer} + * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}. + * <p> + * If a container enables isolated navigation, activities can't be launched to this container + * unless explicitly requested to be launched to. + * + * @see TaskFragmentContainer#isOverlayWithActivityAssociation() */ void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean isolatedNavigationEnabled) { + if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) { + return; + } if (container.isIsolatedNavigationEnabled() == isolatedNavigationEnabled) { return; } @@ -420,6 +430,28 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } /** + * Sets whether to pin this {@link TaskFragmentContainer}. + * <p> + * If a container is pinned, it won't be chosen as the launch target unless it's the launching + * container. + * + * @see TaskFragmentContainer#isAlwaysOnTopOverlay() + * @see TaskContainer#getSplitPinContainer() + */ + void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container, + boolean pinned) { + if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) { + return; + } + if (container.isPinned() == pinned) { + return; + } + container.setPinned(pinned); + setTaskFragmentPinned(wct, container.getTaskFragmentToken(), pinned); + } + + /** * Resizes the task fragment if it was already registered. Skips the operation if the container * creation has not been reported from the server yet. */ @@ -465,6 +497,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 +600,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" @@ -579,21 +616,30 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.setCompanionTaskFragment(wct, primary, secondary); } + /** + * Applies the {@code attributes} to a standalone {@code container}. + * + * @param minDimensions the minimum dimension of the container. + */ void applyActivityStackAttributes( @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes, @Nullable Size minDimensions) { - final Rect taskBounds = container.getTaskContainer().getBounds(); final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, - taskBounds); + container); final boolean isFillParent = relativeBounds.isEmpty(); - final boolean isIsolatedNavigated = !isFillParent && container.isOverlay(); final boolean dimOnTask = !isFillParent - && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK - && Flags.fullscreenDimFlag(); + && Flags.fullscreenDimFlag() + && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; final IBinder fragmentToken = container.getTaskFragmentToken(); + if (container.isAlwaysOnTopOverlay()) { + setTaskFragmentPinned(wct, container, !isFillParent); + } else if (container.isOverlayWithActivityAssociation()) { + setTaskFragmentIsolatedNavigation(wct, container, !isFillParent); + } + // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds // and WCT#setWindowingMode to take fragmentToken. resizeTaskFragmentIfRegistered(wct, container, relativeBounds); @@ -602,7 +648,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); // Always use default animation for standalone ActivityStack. updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); - setTaskFragmentIsolatedNavigation(wct, container, isIsolatedNavigated); setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); } @@ -612,7 +657,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ @NonNull static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, - @NonNull Rect taskBounds) { + @NonNull TaskFragmentContainer container) { if (bounds.isEmpty()) { // Don't need to check if the bounds follows the task bounds. return bounds; @@ -621,10 +666,33 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Expand the bounds if the bounds are smaller than minimum dimensions. return new Rect(); } + final TaskContainer taskContainer = container.getTaskContainer(); + final Rect taskBounds = taskContainer.getBounds(); if (!taskBounds.contains(bounds)) { // Expand the bounds if the bounds exceed the task bounds. return new Rect(); } + + if (!container.isOverlay()) { + // Stop here if the container is not an overlay. + return bounds; + } + + final IBinder associatedActivityToken = container.getAssociatedActivityToken(); + + if (associatedActivityToken == null) { + // Stop here if the container is an always-on-top overlay. + return bounds; + } + + // Expand the overlay with activity association if the associated activity is part of a + // split, or we may need to handle three change transition together. + final TaskFragmentContainer associatedContainer = taskContainer + .getContainerWithActivity(associatedActivityToken); + if (taskContainer.getActiveSplitForContainer(associatedContainer) != null) { + return new Rect(); + } + return bounds; } @@ -685,8 +753,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 +763,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 +782,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 +823,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 +1024,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 +1043,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 +1054,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 +1065,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 +1191,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 +1199,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 c4adf16f4536..c708da97d908 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,10 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Predicate; +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,18 +71,14 @@ 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; - - private int mDisplayId; - - private boolean mIsVisible; - - private boolean mHasDirectActivity; + private TaskFragmentParentInfo mInfo; /** * TaskFragments that the organizer has requested to be closed. They should be removed when @@ -86,13 +89,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) { @@ -101,12 +122,14 @@ class TaskContainer { mTaskId = taskId; final TaskProperties taskProperties = TaskProperties .getTaskPropertiesFromActivity(activityInTask); - mConfiguration = taskProperties.getConfiguration(); - mDisplayId = taskProperties.getDisplayId(); - // Note that it is always called when there's a new Activity is started, which implies - // the host task is visible and has an activity in the task. - mIsVisible = true; - mHasDirectActivity = true; + mInfo = new TaskFragmentParentInfo( + taskProperties.getConfiguration(), + taskProperties.getDisplayId(), + // Note that it is always called when there's a new Activity is started, which + // implies the host task is visible and has an activity in the task. + true /* visible */, + true /* hasDirectActivity */, + null /* decorSurface */); } int getTaskId() { @@ -114,36 +137,39 @@ class TaskContainer { } int getDisplayId() { - return mDisplayId; + return mInfo.getDisplayId(); } boolean isVisible() { - return mIsVisible; + return mInfo.isVisible(); } - void setIsVisible(boolean visible) { - mIsVisible = visible; + void setInvisible() { + mInfo = new TaskFragmentParentInfo(mInfo.getConfiguration(), mInfo.getDisplayId(), + false /* visible */, mInfo.hasDirectActivity(), mInfo.getDecorSurface()); } boolean hasDirectActivity() { - return mHasDirectActivity; + return mInfo.hasDirectActivity(); } @NonNull Rect getBounds() { - return mConfiguration.windowConfiguration.getBounds(); + return mInfo.getConfiguration().windowConfiguration.getBounds(); } @NonNull TaskProperties getTaskProperties() { - return new TaskProperties(mDisplayId, mConfiguration); + return new TaskProperties(mInfo.getDisplayId(), mInfo.getConfiguration()); } void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { - mConfiguration.setTo(info.getConfiguration()); - mDisplayId = info.getDisplayId(); - mIsVisible = info.isVisible(); - mHasDirectActivity = info.hasDirectActivity(); + mInfo = info; + } + + @NonNull + TaskFragmentParentInfo getTaskFragmentParentInfo() { + return mInfo; } /** @@ -159,16 +185,16 @@ class TaskContainer { // If the task properties equals regardless of starting position, don't // need to update the container. - return mConfiguration.diffPublicOnly(configuration) != 0 - || mDisplayId != info.getDisplayId(); + return mInfo.getConfiguration().diffPublicOnly(configuration) != 0 + || mInfo.getDisplayId() != info.getDisplayId(); } /** * 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) { @@ -187,7 +213,7 @@ class TaskContainer { } boolean isInPictureInPicture() { - return isInPictureInPicture(mConfiguration); + return isInPictureInPicture(mInfo.getConfiguration()); } private static boolean isInPictureInPicture(@NonNull Configuration configuration) { @@ -200,7 +226,7 @@ class TaskContainer { @WindowingMode private int getWindowingMode() { - return mConfiguration.windowConfiguration.getWindowingMode(); + return mInfo.getConfiguration().windowConfiguration.getWindowingMode(); } /** Whether there is any {@link TaskFragmentContainer} below this Task. */ @@ -208,10 +234,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); } } @@ -234,7 +269,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)) { @@ -279,17 +314,51 @@ class TaskContainer { return null; } - /** Returns the overlay container in the task, or {@code null} if it doesn't exist. */ @Nullable - TaskFragmentContainer getOverlayContainer() { - return mOverlayContainer; + TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + return getContainer(container -> container.hasAppearedActivity(activityToken) + || container.hasPendingAppearedActivity(activityToken)); + } + + @Nullable + TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final TaskFragmentContainer container = mContainers.get(i); + if (predicate.test(container)) { + return container; + } + } + return null; + } + + @Nullable + SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { + if (container == null) { + return null; + } + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = mSplitContainers.get(i); + if (container.equals(splitContainer.getSecondaryContainer()) + || container.equals(splitContainer.getPrimaryContainer())) { + return splitContainer; + } + } + return null; + } + + /** + * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. + */ + @Nullable + 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()) { @@ -310,6 +379,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); @@ -324,6 +398,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); } @@ -395,13 +502,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() { @@ -429,18 +605,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..482554351b97 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; @@ -172,13 +184,18 @@ class TaskFragmentContainer { private boolean mIsIsolatedNavigationEnabled; /** + * Whether this TaskFragment is pinned. + */ + private boolean mIsPinned; + + /** * Whether to apply dimming on the parent Task that was requested last. */ private boolean mLastDimOnTask; /** * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController, - * TaskFragmentContainer, String, Bundle) + * TaskFragmentContainer, String, Bundle, Activity) */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @@ -187,7 +204,7 @@ class TaskFragmentContainer { @Nullable TaskFragmentContainer pairedPrimaryContainer) { this(pendingAppearedActivity, pendingAppearedIntent, taskContainer, controller, pairedPrimaryContainer, null /* overlayTag */, - null /* launchOptions */); + null /* launchOptions */, null /* associatedActivity */); } /** @@ -197,12 +214,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 +233,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 +445,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 (!isOverlayWithActivityAssociation()) { + return; + } + if (mAssociatedActivityToken == activityToken) { + // If the associated activity is destroyed, also finish this overlay container. + mController.mPresenter.cleanupContainer(wct, this, false /* shouldFinishDependent */); + } } @Nullable @@ -456,7 +505,7 @@ class TaskFragmentContainer { // sure the controller considers this container as the one containing the activity. // This is needed when the activity is added as pending appeared activity to one // TaskFragment while it is also an appeared activity in another. - return mController.getContainerWithActivity(activityToken) == this; + return mTaskContainer.getContainerWithActivity(activityToken) == this; } /** Whether this activity has appeared in the TaskFragment on the server side. */ @@ -748,6 +797,10 @@ class TaskFragmentContainer { } } + @NonNull Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + /** * Checks if last requested windowing mode is equal to the provided value. * @see WindowContainerTransaction#setWindowingMode @@ -845,6 +898,34 @@ class TaskFragmentContainer { mIsIsolatedNavigationEnabled = isolatedNavigationEnabled; } + /** + * Returns whether this container is pinned. + * + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_PINNED + */ + boolean isPinned() { + return mIsPinned; + } + + /** + * Sets whether to pin this container or not. + * + * @see #isPinned() + */ + void setPinned(boolean pinned) { + mIsPinned = pinned; + } + + /** + * Indicates to skip activity resolving if the activity is from this container. + * + * @see #isIsolatedNavigationEnabled() + * @see #isPinned() + */ + boolean shouldSkipActivityResolving() { + return isIsolatedNavigationEnabled() || isPinned(); + } + /** Sets whether to apply dim on the parent Task. */ void setLastDimOnTask(boolean lastDimOnTask) { mLastDimOnTask = lastDimOnTask; @@ -957,6 +1038,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; + } + + /** + * 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() && mAssociatedActivityToken == null; + } + + boolean isOverlayWithActivityAssociation() { + return isOverlay() && mAssociatedActivityToken != null; + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); @@ -976,6 +1083,7 @@ class TaskFragmentContainer { + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished + " overlayTag=" + mOverlayTag + + " associatedActivityToken=" + mAssociatedActivityToken + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 4fd11c495529..070fa5bcfae4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -45,6 +45,7 @@ import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.extensions.core.util.function.Consumer; +import androidx.window.extensions.util.DeduplicateConsumer; import java.util.ArrayList; import java.util.Collections; @@ -62,7 +63,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final Object mLock = new Object(); @GuardedBy("mLock") - private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = + private final Map<Context, DeduplicateConsumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = new ArrayMap<>(); @GuardedBy("mLock") @@ -130,7 +131,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { if (mWindowLayoutChangeListeners.containsKey(context) // In theory this method can be called on the same consumer with different // context. - || mWindowLayoutChangeListeners.containsValue(consumer)) { + || containsConsumer(consumer)) { return; } if (!context.isUiContext()) { @@ -141,7 +142,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); consumer.accept(newWindowLayout); }); - mWindowLayoutChangeListeners.put(context, consumer); + mWindowLayoutChangeListeners.put(context, new DeduplicateConsumer<>(consumer)); final IBinder windowContextToken = context.getWindowContextToken(); if (windowContextToken != null) { @@ -176,19 +177,35 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { @Override public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) { synchronized (mLock) { + DeduplicateConsumer<WindowLayoutInfo> consumerToRemove = null; for (Context context : mWindowLayoutChangeListeners.keySet()) { - if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) { + final DeduplicateConsumer<WindowLayoutInfo> deduplicateConsumer = + mWindowLayoutChangeListeners.get(context); + if (!deduplicateConsumer.matchesConsumer(consumer)) { continue; } final IBinder token = context.getWindowContextToken(); + consumerToRemove = deduplicateConsumer; if (token != null) { context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token)); mConfigurationChangeListeners.remove(token); } break; } - mWindowLayoutChangeListeners.values().remove(consumer); + if (consumerToRemove != null) { + mWindowLayoutChangeListeners.values().remove(consumerToRemove); + } + } + } + + @GuardedBy("mLock") + private boolean containsConsumer(@NonNull Consumer<WindowLayoutInfo> consumer) { + for (DeduplicateConsumer<WindowLayoutInfo> c : mWindowLayoutChangeListeners.values()) { + if (c.matchesConsumer(consumer)) { + return true; + } } + return false; } @GuardedBy("mLock") diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java new file mode 100644 index 000000000000..ee271aa57003 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java @@ -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 androidx.window.extensions.util; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Consumer; + +/** + * A utility class that will not report a value if it is the same as the last reported value. + * @param <T> generic values to be reported. + */ +public class DeduplicateConsumer<T> implements Consumer<T> { + + private final Object mLock = new Object(); + @GuardedBy("mLock") + @Nullable + private T mLastReportedValue = null; + @NonNull + private final Consumer<T> mConsumer; + + public DeduplicateConsumer(@NonNull Consumer<T> consumer) { + mConsumer = consumer; + } + + /** + * Returns {@code true} if the given consumer matches this object or the wrapped + * {@link Consumer}, {@code false} otherwise + */ + public boolean matchesConsumer(@NonNull Consumer<T> consumer) { + return consumer == this || mConsumer.equals(consumer); + } + + /** + * Accepts a new value and relays it if it is different from + * the last reported value. + * @param value to report if different. + */ + @Override + public void accept(@NonNull T value) { + synchronized (mLock) { + if (mLastReportedValue != null && mLastReportedValue.equals(value)) { + return; + } + mLastReportedValue = value; + } + mConsumer.accept(value); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java index 56c3bce87d6e..339908a3a9a4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -16,16 +16,10 @@ package androidx.window.sidecar; -import static android.view.Display.DEFAULT_DISPLAY; - -import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; -import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; - +import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityThread; import android.app.Application; import android.content.Context; -import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.IBinder; @@ -38,7 +32,6 @@ import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.util.BaseDataProducer; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -76,64 +69,13 @@ class SampleSidecarImpl extends StubSidecar { @NonNull @Override public SidecarDeviceState getDeviceState() { - SidecarDeviceState deviceState = new SidecarDeviceState(); - deviceState.posture = deviceStateFromFeature(); - return deviceState; - } - - private int deviceStateFromFeature() { - for (int i = 0; i < mStoredFeatures.size(); i++) { - CommonFoldingFeature feature = mStoredFeatures.get(i); - final int state = feature.getState(); - switch (state) { - case CommonFoldingFeature.COMMON_STATE_FLAT: - return SidecarDeviceState.POSTURE_OPENED; - case CommonFoldingFeature.COMMON_STATE_HALF_OPENED: - return SidecarDeviceState.POSTURE_HALF_OPENED; - case CommonFoldingFeature.COMMON_STATE_UNKNOWN: - return SidecarDeviceState.POSTURE_UNKNOWN; - } - } - return SidecarDeviceState.POSTURE_UNKNOWN; + return SidecarHelper.calculateDeviceState(mStoredFeatures); } @NonNull @Override public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { - Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); - SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); - if (activity == null) { - return windowLayoutInfo; - } - windowLayoutInfo.displayFeatures = getDisplayFeatures(activity); - return windowLayoutInfo; - } - - private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { - int displayId = activity.getDisplay().getDisplayId(); - if (displayId != DEFAULT_DISPLAY) { - return Collections.emptyList(); - } - - if (activity.isInMultiWindowMode()) { - // It is recommended not to report any display features in multi-window mode, since it - // won't be possible to synchronize the display feature positions with window movement. - return Collections.emptyList(); - } - - List<SidecarDisplayFeature> features = new ArrayList<>(); - final int rotation = activity.getResources().getConfiguration().windowConfiguration - .getDisplayRotation(); - for (CommonFoldingFeature baseFeature : mStoredFeatures) { - SidecarDisplayFeature feature = new SidecarDisplayFeature(); - Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, rotation, featureRect); - transformToWindowSpaceRect(activity, featureRect); - feature.setRect(featureRect); - feature.setType(baseFeature.getType()); - features.add(feature); - } - return Collections.unmodifiableList(features); + return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures); } @Override @@ -145,13 +87,14 @@ class SampleSidecarImpl extends StubSidecar { private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + public void onActivityCreated(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { super.onActivityCreated(activity, savedInstanceState); onDisplayFeaturesChangedForActivity(activity); } @Override - public void onActivityConfigurationChanged(Activity activity) { + public void onActivityConfigurationChanged(@NonNull Activity activity) { super.onActivityConfigurationChanged(activity); onDisplayFeaturesChangedForActivity(activity); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java new file mode 100644 index 000000000000..bb6ab47b144d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java @@ -0,0 +1,129 @@ +/* + * 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.sidecar; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; + +import android.annotation.NonNull; +import android.app.Activity; +import android.app.ActivityThread; +import android.graphics.Rect; +import android.os.IBinder; + +import androidx.window.common.CommonFoldingFeature; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A utility class for transforming between Sidecar and Extensions features. + */ +class SidecarHelper { + + private SidecarHelper() {} + + /** + * Returns the {@link SidecarDeviceState} posture that is calculated for the first fold in + * the feature list. Sidecar devices only have one fold so we only pick the first one to + * determine the state. + * @param featureList the {@link CommonFoldingFeature} that are currently active. + * @return the {@link SidecarDeviceState} calculated from the {@link List} of + * {@link CommonFoldingFeature}. + */ + @SuppressWarnings("deprecation") + private static int deviceStateFromFeatureList(@NonNull List<CommonFoldingFeature> featureList) { + for (int i = 0; i < featureList.size(); i++) { + final CommonFoldingFeature feature = featureList.get(i); + final int state = feature.getState(); + switch (state) { + case CommonFoldingFeature.COMMON_STATE_FLAT: + return SidecarDeviceState.POSTURE_OPENED; + case CommonFoldingFeature.COMMON_STATE_HALF_OPENED: + return SidecarDeviceState.POSTURE_HALF_OPENED; + case CommonFoldingFeature.COMMON_STATE_UNKNOWN: + return SidecarDeviceState.POSTURE_UNKNOWN; + case CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES: + return SidecarDeviceState.POSTURE_UNKNOWN; + case CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE: + return SidecarDeviceState.POSTURE_UNKNOWN; + } + } + return SidecarDeviceState.POSTURE_UNKNOWN; + } + + /** + * Returns a {@link SidecarDeviceState} calculated from a {@link List} of + * {@link CommonFoldingFeature}s. + */ + @SuppressWarnings("deprecation") + static SidecarDeviceState calculateDeviceState( + @NonNull List<CommonFoldingFeature> featureList) { + final SidecarDeviceState deviceState = new SidecarDeviceState(); + deviceState.posture = deviceStateFromFeatureList(featureList); + return deviceState; + } + + @SuppressWarnings("deprecation") + private static List<SidecarDisplayFeature> calculateDisplayFeatures( + @NonNull Activity activity, + @NonNull List<CommonFoldingFeature> featureList + ) { + final int displayId = activity.getDisplay().getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + return Collections.emptyList(); + } + + if (activity.isInMultiWindowMode()) { + // It is recommended not to report any display features in multi-window mode, since it + // won't be possible to synchronize the display feature positions with window movement. + return Collections.emptyList(); + } + + final List<SidecarDisplayFeature> features = new ArrayList<>(); + final int rotation = activity.getResources().getConfiguration().windowConfiguration + .getDisplayRotation(); + for (CommonFoldingFeature baseFeature : featureList) { + final SidecarDisplayFeature feature = new SidecarDisplayFeature(); + final Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, rotation, featureRect); + transformToWindowSpaceRect(activity, featureRect); + feature.setRect(featureRect); + feature.setType(baseFeature.getType()); + features.add(feature); + } + return Collections.unmodifiableList(features); + } + + /** + * Returns a {@link SidecarWindowLayoutInfo} calculated from the {@link List} of + * {@link CommonFoldingFeature}. + */ + @SuppressWarnings("deprecation") + static SidecarWindowLayoutInfo calculateWindowLayoutInfo(@NonNull IBinder windowToken, + @NonNull List<CommonFoldingFeature> featureList) { + final Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); + final SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); + if (activity == null) { + return windowLayoutInfo; + } + windowLayoutInfo.displayFeatures = calculateDisplayFeatures(activity, featureList); + return windowLayoutInfo; + } +} 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/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index 4ddbd13978d5..61ea51a35f58 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -23,6 +23,7 @@ package { android_test { name: "WMJetpackUnitTests", + team: "trendy_team_windowing_sdk", // To make the test run via TEST_MAPPING test_suites: ["device-tests"], @@ -32,6 +33,7 @@ android_test { static_libs: [ "androidx.window.extensions", + "androidx.window.extensions.core_core", "junit", "androidx.test.runner", "androidx.test.rules", 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..746607c8094c --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -0,0 +1,785 @@ +/* + * 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.FLING_ANIMATION_DURATION; +import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_INTERPOLATOR; +import static androidx.window.extensions.embedding.DividerPresenter.MIN_DISMISS_VELOCITY_DP_PER_SECOND; +import static androidx.window.extensions.embedding.DividerPresenter.MIN_FLING_VELOCITY_DP_PER_SECOND; +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.animation.ValueAnimator; +import android.app.Activity; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +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.view.View; +import android.view.Window; +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +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 MockitoRule rule = MockitoJUnit.rule(); + + @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() { + 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 */, + Color.valueOf(Color.BLACK), /* primaryVeilColor */ + Color.valueOf(Color.GRAY) /* secondaryVeilColor */ + ); + + 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_setDefaultValues_fixedDivider() { + DividerAttributes attributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_FIXED).build(); + DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes); + + assertEquals(DividerAttributes.DIVIDER_TYPE_FIXED, sanitized.getDividerType()); + assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp()); + + // The ratios should not be set for fixed divider + assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, sanitized.getPrimaryMinRatio(), + 0.0f /* delta */); + assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, 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 */); + } + + @Test + public void testGetContainerBackgroundColor() { + final Color defaultColor = Color.valueOf(Color.RED); + final Color activityBackgroundColor = Color.valueOf(Color.BLUE); + final TaskFragmentContainer container = mock(TaskFragmentContainer.class); + final Activity activity = mock(Activity.class); + final Window window = mock(Window.class); + final View decorView = mock(View.class); + final ColorDrawable backgroundDrawable = + new ColorDrawable(activityBackgroundColor.toArgb()); + when(activity.getWindow()).thenReturn(window); + when(window.getDecorView()).thenReturn(decorView); + when(decorView.getBackground()).thenReturn(backgroundDrawable); + + // When the top non-finishing activity returns null, the default color should be returned. + when(container.getTopNonFinishingActivity()).thenReturn(null); + assertEquals(defaultColor, + DividerPresenter.getContainerBackgroundColor(container, defaultColor)); + + // When the top non-finishing activity is non-null, its background color should be returned. + when(container.getTopNonFinishingActivity()).thenReturn(activity); + assertEquals(activityBackgroundColor, + DividerPresenter.getContainerBackgroundColor(container, defaultColor)); + } + + @Test + public void testGetValueAnimator() { + ValueAnimator animator = + DividerPresenter.getValueAnimator( + 375 /* prevDividerPosition */, + 500 /* snappedDividerPosition */); + + assertEquals(animator.getDuration(), FLING_ANIMATION_DURATION); + assertEquals(animator.getInterpolator(), FLING_ANIMATION_INTERPOLATOR); + } + + @Test + public void testDividerPositionWithDraggingToFullscreenAllowed() { + final float displayDensity = 600F; + final float dismissVelocity = MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity + 10f; + final float nonFlingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity - 10f; + final float flingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity + 10f; + + // Divider position is less than minPosition and the velocity is enough to be dismissed + assertEquals( + 0, // Closed position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 10 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + -dismissVelocity, + displayDensity)); + + // Divider position is greater than maxPosition and the velocity is enough to be dismissed + assertEquals( + 1200, // Fully expanded position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 1000 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + dismissVelocity, + displayDensity)); + + // Divider position is returned when the velocity is not fast enough for fling and is in + // between minPosition and maxPosition + assertEquals( + 500, // dividerPosition is not snapped + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 500 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // Divider position is snapped when the velocity is not fast enough for fling and larger + // than maxPosition + assertEquals( + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 950 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // Divider position is snapped when the velocity is not fast enough for fling and smaller + // than minPosition + assertEquals( + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 20 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // Divider position is greater than minPosition and the velocity is enough for fling + assertEquals( + 0, // Closed position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 50 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity)); + + // Divider position is less than maxPosition and the velocity is enough for fling + assertEquals( + 1200, // Fully expanded position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 800 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + flingVelocity, + displayDensity)); + } + + 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..76e6a0ff2c21 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 @@ -42,10 +42,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; 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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; @@ -61,6 +63,9 @@ import java.util.ArrayList; @SmallTest @RunWith(AndroidJUnit4.class) public class JetpackTaskFragmentOrganizerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private WindowContainerTransaction mTransaction; @Mock @@ -73,7 +78,6 @@ public class JetpackTaskFragmentOrganizerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); spyOn(mOrganizer); @@ -107,7 +111,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 c64c3abb056f..9ebcb759115d 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 @@ -21,11 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -39,6 +43,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; @@ -80,9 +85,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -97,6 +104,11 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class OverlayPresentationTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( + new ComponentName("test", "placeholder")); @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); @@ -125,7 +137,6 @@ public class OverlayPresentationTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = @@ -177,37 +188,85 @@ public class OverlayPresentationTest { } @Test - public void testGetOverlayContainers() { - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()).isEmpty(); + public void testSetIsolatedNavigation_overlayFeatureDisabled_earlyReturn() { + mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG); + + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test"); + + mSplitPresenter.setTaskFragmentIsolatedNavigation(mTransaction, container, + !container.isIsolatedNavigationEnabled()); + + verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(), + any(IBinder.class), anyBoolean()); + } + + @Test + public void testSetPinned_overlayFeatureDisabled_earlyReturn() { + mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG); + + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test"); + + mSplitPresenter.setTaskFragmentPinned(mTransaction, container, + !container.isPinned()); + + verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), any(IBinder.class), + anyBoolean()); + } + + @Test + 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_dismissOverlay() { + 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 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 +281,23 @@ 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()); 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 +306,38 @@ 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(), createMockActivity()); + + assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + + " is associated with different launching activity") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, overlayContainer); + assertThat(overlayContainer).isNotEqualTo(mOverlayContainer1); + } + + @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 +348,20 @@ 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, + true /* associatedLaunchingActivity */, mActivity); + mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); List<TaskFragmentContainer> overlayContainers = mSplitController - .getAllOverlayTaskFragmentContainers(); + .getAllNonFinishingOverlayContainers(); assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2); } @@ -274,17 +370,49 @@ public class OverlayPresentationTest { mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); final Rect bounds = new Rect(0, 0, 100, 100); + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1"); - SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), - TASK_BOUNDS); + assertThat(sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), + overlayContainer).isEmpty()).isTrue(); } @Test public void testSanitizeBounds_notInTaskBounds_expandOverlay() { final Rect bounds = new Rect(TASK_BOUNDS); bounds.offset(10, 10); + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1"); + + assertThat(sanitizeBounds(bounds, null, overlayContainer) + .isEmpty()).isTrue(); + } + + @Test + public void testSanitizeBounds_visibleSplit_expandOverlay() { + // Launch a visible split + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + final TaskFragmentContainer primaryContainer = + createMockTaskFragmentContainer(primaryActivity, true /* isVisible */); + final TaskFragmentContainer secondaryContainer = + createMockTaskFragmentContainer(secondaryActivity, true /* isVisible */); + + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( + activityActivityPair -> true /* activityPairPredicate */, + activityIntentPair -> true /* activityIntentPairPredicate */, + parentWindowMetrics -> true /* parentWindowMetricsPredicate */) + .build(); + mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity, + secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes()); - SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS); + final Rect bounds = new Rect(0, 0, 100, 100); + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */, + true /* associatedLaunchingActivity */, secondaryActivity); + + assertThat(sanitizeBounds(bounds, null, overlayContainer) + .isEmpty()).isTrue(); } @Test @@ -294,9 +422,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 +432,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 +483,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 +503,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 +574,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 +593,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 +616,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 @@ -505,12 +634,15 @@ public class OverlayPresentationTest { WINDOWING_MODE_UNDEFINED); verify(mSplitPresenter).updateAnimationParams(mTransaction, token, TaskFragmentAnimationParams.DEFAULT); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); + verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), + any(TaskFragmentContainer.class), anyBoolean()); + verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(), + any(TaskFragmentContainer.class), anyBoolean()); } @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() @@ -528,6 +660,33 @@ public class OverlayPresentationTest { verify(mSplitPresenter).updateAnimationParams(mTransaction, token, TaskFragmentAnimationParams.DEFAULT); verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true); + verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), + any(TaskFragmentContainer.class), anyBoolean()); + 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); + verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(), + any(TaskFragmentContainer.class), anyBoolean()); + verify(mSplitPresenter).setTaskFragmentPinned(mTransaction, container, true); verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true); } @@ -547,6 +706,8 @@ public class OverlayPresentationTest { verify(mSplitPresenter).updateAnimationParams(mTransaction, token, TaskFragmentAnimationParams.DEFAULT); verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); + verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), + any(TaskFragmentContainer.class), anyBoolean()); verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); } @@ -563,8 +724,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,40 +733,187 @@ 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); + } + + @Test + public void testLaunchPlaceholderIfNecessary_skipIfActivityAssociateOverlay() { + setupPlaceholderRule(mActivity); + createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, + true /* associateLaunchingActivity */, mActivity); + + + mSplitController.mTransactionManager.startNewTransaction(); + assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + false /* isOnCreated */)).isFalse(); + + verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); + } + + @Test + public void testLaunchPlaceholderIfNecessary_skipIfActivityInOverlay() { + setupPlaceholderRule(mActivity); + createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); + + mSplitController.mTransactionManager.startNewTransaction(); + assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + false /* isOnCreated */)).isFalse(); + + verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); + } + + /** Setups a rule to launch placeholder for the given activity. */ + private void setupPlaceholderRule(@NonNull Activity primaryActivity) { + final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT, + primaryActivity::equals, i -> false, w -> true) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); + } + + @Test + public void testResolveStartActivityIntent_skipIfAssociateOverlay() { + final Intent intent = new Intent(); + mSplitController.setEmbeddingRules(Collections.singleton( + createSplitRule(mActivity, intent))); + createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, + true /* associateLaunchingActivity */, mActivity); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertThat(container).isNull(); + verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), + any()); + } + + @Test + public void testResolveStartActivityIntent_skipIfLaunchingActivityInOverlay() { + final Intent intent = new Intent(); + mSplitController.setEmbeddingRules(Collections.singleton( + createSplitRule(mActivity, intent))); + createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertThat(container).isNull(); + verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), + any()); + } + /** - * 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. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { + return createMockTaskFragmentContainer(activity, false /* isVisible */); + } + + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ + @NonNull + private TaskFragmentContainer createMockTaskFragmentContainer( + @NonNull Activity activity, boolean isVisible) { final TaskFragmentContainer container = mSplitController.newContainer(activity, activity.getTaskId()); - setupTaskFragmentInfo(container, activity); + setupTaskFragmentInfo(container, activity, isVisible); return container; } @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) { - Activity activity = createMockActivity(); + 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 */); + } + + @NonNull + private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, + boolean isVisible, boolean associateLaunchingActivity) { + return createTestOverlayContainer(taskId, tag, isVisible, associateLaunchingActivity, + null /* launchingActivity */); + } + + // 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, + @Nullable Activity launchingActivity) { + final Activity activity = launchingActivity != null + ? launchingActivity : 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, createMockActivity(), 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..7d86ec2272af 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,17 +103,23 @@ 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; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; 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 +137,12 @@ public class SplitControllerTest { private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( new ComponentName("test", "placeholder")); + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Rule + public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + private Activity mActivity; @Mock private Resources mActivityResources; @@ -138,6 +154,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; @@ -147,7 +170,6 @@ public class SplitControllerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = @@ -208,13 +230,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 +297,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 +615,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); + anyString(), any(), anyBoolean()); } @Test @@ -620,7 +645,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container); } @Test @@ -753,7 +778,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 +821,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 +1554,113 @@ 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 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, 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()); + } + + @Test + public void testTaskFragmentParentInfoChanged() { + // Making a split + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + // Updates the parent info. + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final Configuration configuration = new Configuration(); + final TaskFragmentParentInfo originalInfo = new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); + mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), + TASK_ID, originalInfo); + assertTrue(taskContainer.isVisible()); + + // Making a public configuration change while the Task is invisible. + configuration.densityDpi += 100; + final TaskFragmentParentInfo invisibleInfo = new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, false /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); + mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), + TASK_ID, invisibleInfo); + + // Ensure the TaskContainer is inivisible, but the configuration is not updated. + assertFalse(taskContainer.isVisible()); + assertTrue(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly( + configuration) > 0); + + // Updates when Task to become visible + final TaskFragmentParentInfo visibleInfo = new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); + mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), + TASK_ID, visibleInfo); + + // Ensure the Task is visible and configuration is updated. + assertTrue(taskContainer.isVisible()); + assertFalse(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly( + configuration) > 0); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { return createMockActivity(TASK_ID); @@ -1537,13 +1669,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..3fbce9ec31a5 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 @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; @@ -85,10 +86,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; 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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; @@ -106,6 +109,10 @@ import java.util.ArrayList; public class SplitPresenterTest { private Activity mActivity; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private Resources mActivityResources; @Mock @@ -119,7 +126,6 @@ public class SplitPresenterTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = @@ -280,6 +286,28 @@ public class SplitPresenterTest { } @Test + public void testSetTaskFragmentPinned() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + + // Verify the default. + assertFalse(container.isPinned()); + + mPresenter.setTaskFragmentPinned(mTransaction, container, true); + + final TaskFragmentOperation expectedOperation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_PINNED).setBooleanValue(true).build(); + verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(), + expectedOperation); + assertTrue(container.isPinned()); + + // No request to set the same animation params. + clearInvocations(mTransaction); + mPresenter.setTaskFragmentPinned(mTransaction, container, true); + + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test public void testGetMinDimensionsForIntent() { final Intent intent = new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class); @@ -665,8 +693,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 +703,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/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index a5995a3027ac..8913b22115e9 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -42,11 +42,12 @@ import android.window.TaskFragmentParentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.List; @@ -60,14 +61,12 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskContainerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private SplitController mController; - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - @Test public void testGetWindowingModeForSplitTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java index 379ea0c534ba..a1e9f08585f6 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java @@ -27,10 +27,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; 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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Test class for {@link TaskFragmentAnimationController}. @@ -42,13 +44,15 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentAnimationControllerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private TaskFragmentOrganizer mOrganizer; private TaskFragmentAnimationController mAnimationController; @Before public void setup() { - MockitoAnnotations.initMocks(this); mAnimationController = new TaskFragmentAnimationController(mOrganizer); } 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..44ab2c458e39 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 @@ -51,10 +51,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.google.android.collect.Lists; 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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -71,6 +73,9 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentContainerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private SplitPresenter mPresenter; private SplitController mController; @@ -83,7 +88,6 @@ public class TaskFragmentContainerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); DeviceStateManagerFoldingFeatureProducer producer = mock(DeviceStateManagerFoldingFeatureProducer.class); WindowLayoutComponentImpl component = mock(WindowLayoutComponentImpl.class); @@ -402,7 +406,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())); @@ -534,9 +538,7 @@ public class TaskFragmentContainerTest { // container1. container2.setInfo(mTransaction, mInfo); - assertTrue(container1.hasActivity(mActivity.getActivityToken())); - assertFalse(container2.hasActivity(mActivity.getActivityToken())); - + assertTrue(container2.hasActivity(mActivity.getActivityToken())); // When the pending appeared record is removed from container1, we respect the appeared // record in container2. container1.removePendingAppearedActivity(mActivity.getActivityToken()); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java index 459b6d2c31f9..2598dd63bbde 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java @@ -41,10 +41,12 @@ import androidx.test.filters.SmallTest; import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; 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 org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Test class for {@link TransactionManager}. @@ -56,6 +58,8 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TransactionManagerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); @Mock private TaskFragmentOrganizer mOrganizer; @@ -63,7 +67,6 @@ public class TransactionManagerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mTransactionManager = new TransactionManager(mOrganizer); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java new file mode 100644 index 000000000000..4e9b4a02e1f8 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java @@ -0,0 +1,97 @@ +/* + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.window.extensions.core.util.function.Consumer; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * A class to validate {@link DeduplicateConsumer}. + */ +public class DeduplicateConsumerTest { + + @Test + public void test_duplicate_value_is_filtered() { + String value = "test_value"; + List<String> expected = new ArrayList<>(); + expected.add(value); + RecordingConsumer recordingConsumer = new RecordingConsumer(); + DeduplicateConsumer<String> deduplicateConsumer = + new DeduplicateConsumer<>(recordingConsumer); + + deduplicateConsumer.accept(value); + deduplicateConsumer.accept(value); + + assertEquals(expected, recordingConsumer.getValues()); + } + + @Test + public void test_different_value_is_filtered() { + String value = "test_value"; + String newValue = "test_value_new"; + List<String> expected = new ArrayList<>(); + expected.add(value); + expected.add(newValue); + RecordingConsumer recordingConsumer = new RecordingConsumer(); + DeduplicateConsumer<String> deduplicateConsumer = + new DeduplicateConsumer<>(recordingConsumer); + + deduplicateConsumer.accept(value); + deduplicateConsumer.accept(value); + deduplicateConsumer.accept(newValue); + + assertEquals(expected, recordingConsumer.getValues()); + } + + @Test + public void test_match_against_consumer_property_returns_true() { + RecordingConsumer recordingConsumer = new RecordingConsumer(); + DeduplicateConsumer<String> deduplicateConsumer = + new DeduplicateConsumer<>(recordingConsumer); + + assertTrue(deduplicateConsumer.matchesConsumer(recordingConsumer)); + } + + @Test + public void test_match_against_self_returns_true() { + RecordingConsumer recordingConsumer = new RecordingConsumer(); + DeduplicateConsumer<String> deduplicateConsumer = + new DeduplicateConsumer<>(recordingConsumer); + + assertTrue(deduplicateConsumer.matchesConsumer(deduplicateConsumer)); + } + + private static final class RecordingConsumer implements Consumer<String> { + + private final List<String> mValues = new ArrayList<>(); + + @Override + public void accept(String s) { + mValues.add(s); + } + + public List<String> getValues() { + return mValues; + } + } +} diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 8829d1b9e0e1..89781fd650a4 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,8 @@ android_library { "androidx.recyclerview_recyclerview", "kotlinx-coroutines-android", "kotlinx-coroutines-core", - "iconloader_base", + "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", + "//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/OWNERS b/libs/WindowManager/Shell/OWNERS index 0c4fd140780e..ebebd8a52c9a 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,5 +1,5 @@ xutan@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com per-file res*/*/tv_*.xml = bronger@google.com 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..8977d5cad780 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" @@ -63,3 +64,17 @@ flag { description: "Enables long-press action for nav handle when a bubble is expanded" bug: "324910035" } + +flag { + name: "enable_optional_bubble_overflow" + namespace: "multitasking" + description: "Hides the bubble overflow if there aren't any overflowed bubbles" + bug: "334175587" +} + +flag { + name: "enable_retrievable_bubbles" + namespace: "multitasking" + description: "Allow opening bubbles overflow UI without bubbles being visible" + bug: "340337839" +} 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..076414132e27 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,279 @@ +/* + * 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.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 drag_stayOnSameSide() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) + } + + @Test + fun drag_toLeft() { + // Drag to left, but don't finish + 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) + assertThat(testListener.locationReleases).isEmpty() + + // Finish the drag + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.LEFT) + } + + @Test + fun drag_toLeftAndBackToRight() { + // Drag to left + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + // Drag to right + 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) + assertThat(testListener.locationReleases).isEmpty() + + // Release the view + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) + } + + @Test + fun drag_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() + assertThat(testListener.locationReleases).isEmpty() + + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) + } + + @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 = Rect().also { + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it) + } + + 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>() + val locationReleases = mutableListOf<BubbleBarLocation>() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + + override fun onRelease(location: BubbleBarLocation) { + locationReleases.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..b928a0b20764 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml @@ -0,0 +1,26 @@ +<?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. + --> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:inset="@dimen/bubble_bar_expanded_view_drop_target_padding"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/bubble_bar_expanded_view_drop_target_corner" /> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="?androidprv:attr/materialColorPrimaryContainer" /> + </shape> +</inset> 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..9599658384f0 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,95 @@ ~ 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:background="@drawable/desktop_mode_maximize_menu_background"> + android:padding="16dp" + android:background="@drawable/desktop_mode_maximize_menu_background" + android:elevation="1dp"> + <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" + android:alpha="0"> + <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:id="@+id/maximize_menu_maximize_window_text" + 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" + android:alpha="0"/> + </LinearLayout> + + <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" + android:alpha="0"> + <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:id="@+id/maximize_menu_snap_window_text" + 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" + android:alpha="0"/> + </LinearLayout> +</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>
\ 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 5375a0913403..1c8f5e60c5c9 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 19d5d17c03fe..bcdc2a9c8539 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 0465850df5cf..3492f136c4f9 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 61c51ed82f17..4002e4d04d51 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 a07406942ff2..c371f7f62feb 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 554068662dc1..27d4cfcf22d5 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 c4014d35437c..302c0071a73a 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 ee7fb5a02f59..5e43506ab621 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 1ceb706927d1..a5bd2ab5c10b 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 3c6d4c1524e2..1210fe8fda05 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 b6af5824b227..971e146ba77e 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 30ec587c9c6e..fe0b74c469f4 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..8d24c161e3e4 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,13 @@ <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> + <!-- Corner radius for expanded view drop target --> + <dimen name="bubble_bar_expanded_view_drop_target_corner">28dp</dimen> + <dimen name="bubble_bar_expanded_view_drop_target_padding">24dp</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> @@ -414,13 +423,14 @@ <dimen name="freeform_decor_caption_height">42dp</dimen> <!-- Height of desktop mode caption for freeform tasks. --> - <dimen name="desktop_mode_freeform_decor_caption_height">42dp</dimen> + <dimen name="desktop_mode_freeform_decor_caption_height">40dp</dimen> <!-- 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 +462,22 @@ <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 padding of the maximize menu in desktop mode. --> + <dimen name="desktop_mode_menu_padding">16dp</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 height of the buttons in the maximize menu. --> + <dimen name="desktop_mode_maximize_menu_button_height">52dp</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 +518,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 +562,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/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java index 22ba70860587..bdd89c0e1ac9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java @@ -14,10 +14,14 @@ * limitations under the License. */ -package com.android.wm.shell.desktopmode; +package com.android.wm.shell.shared; +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; /** @@ -37,15 +41,6 @@ public class DesktopModeStatus { public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean( "persist.wm.debug.desktop_change_display", false); - - /** - * Flag to indicate that desktop stashing is enabled. - * When enabled, swiping home from desktop stashes the open apps. Next app that launches, - * will be added to the desktop. - */ - private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean( - "persist.wm.debug.desktop_stashing", false); - /** * Flag to indicate whether to apply shadows to windows in desktop mode. */ @@ -61,14 +56,38 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_use_window_shadows_focused_window", false); /** - * Flag to indicate whether to apply shadows to windows in desktop mode. + * Flag to indicate whether to use rounded corners for windows in desktop mode. */ private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean( "persist.wm.debug.desktop_use_rounded_corners", true); /** - * Return {@code true} if desktop windowing is enabled + * 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); + + /** + * Default value for {@code MAX_TASK_LIMIT}. + */ + @VisibleForTesting + public static final int DEFAULT_MAX_TASK_LIMIT = 4; + + // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode + /** + * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time. + * + * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen + * recording window, or Bluetooth pairing window). */ + private static final int MAX_TASK_LIMIT = SystemProperties.getInt( + "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT); + + /** + * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers + * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing. + */ + @VisibleForTesting public static boolean isEnabled() { return Flags.enableDesktopWindowingMode(); } @@ -81,14 +100,6 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop task stashing is enabled when going home. - * Allows users to use home screen to add tasks to desktop. - */ - public static boolean isStashingEnabled() { - return IS_STASHING_ENABLED; - } - - /** * Return whether to use window shadows. * * @param isFocusedWindow whether the window to apply shadows to is focused @@ -104,4 +115,34 @@ 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 the maximum limit on the number of Tasks to show in Desktop Mode at any one time. + */ + public static int getMaxTaskLimit() { + return MAX_TASK_LIMIT; + } + + /** + * 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 is enabled and can be entered on the current device. + */ + public static boolean canEnterDesktopMode(@NonNull Context context) { + return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled(); + } } 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..3256abf09116 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. @@ -28,13 +28,14 @@ import com.android.wm.shell.transition.IHomeTransitionListener; interface IShellTransitions { /** - * Registers a remote transition handler. + * Registers a remote transition handler for all operations excluding takeovers (see + * registerRemoteForTakeover()). */ oneway void registerRemote(in TransitionFilter filter, in RemoteTransition remoteTransition) = 1; /** - * Unregisters a remote transition handler. + * Unregisters a remote transition handler for all operations. */ oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2; @@ -52,4 +53,10 @@ interface IShellTransitions { * Returns a container surface for the home root task. */ SurfaceControl getHomeTaskOverlayContainer() = 5; + + /** + * Registers a remote transition for takeover operations only. + */ + oneway void registerRemoteForTakeover(in TransitionFilter filter, + in RemoteTransition remoteTransition) = 6; } 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..6d4ab4c1bd09 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. @@ -28,13 +28,20 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface ShellTransitions { /** - * Registers a remote transition. + * Registers a remote transition for all operations excluding takeovers (see + * {@link ShellTransitions#registerRemoteForTakeover(TransitionFilter, RemoteTransition)}). */ default void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) {} /** - * Unregisters a remote transition. + * Registers a remote transition for takeover operations only. + */ + default void registerRemoteForTakeover(@NonNull TransitionFilter filter, + @NonNull RemoteTransition remoteTransition) {} + + /** + * Unregisters a remote transition for all operations. */ default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {} } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index dcd4062cb819..785e30d879d2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -69,8 +69,12 @@ public class TransitionUtil { /** Returns {@code true} if the transition is opening or closing mode. */ public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) { - return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE - || mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK; + return isOpeningMode(mode) || mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; + } + + /** Returns {@code true} if the transition is opening mode. */ + public static boolean isOpeningMode(@TransitionInfo.TransitionMode int mode) { + return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT; } /** Returns {@code true} if the transition has a display change. */ 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/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index d8d0d876b4f2..3244837324b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -16,6 +16,7 @@ package com.android.wm.shell; + import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -30,7 +31,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; -import android.app.AppCompatTaskInfo; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.LocusId; @@ -718,8 +719,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override - public void onCameraControlStateUpdated( - int taskId, @AppCompatTaskInfo.CameraCompatControlState int state) { + public void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state) { final TaskAppearedInfo info; synchronized (mLock) { info = mTasks.get(taskId); 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..a426b206b0cd 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 @@ -26,6 +26,7 @@ import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationS import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; +import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; import android.animation.Animator; import android.animation.ValueAnimator; @@ -190,6 +191,10 @@ class ActivityEmbeddingAnimationRunner { @NonNull private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters( @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { + if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) { + // Jump cut for AE drag resizing because the content is veiled. + return new ArrayList<>(); + } boolean isChangeTransition = false; for (TransitionInfo.Change change : info.getChanges()) { if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { @@ -523,8 +528,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 +558,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 +569,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/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java index 1f9358e2aa91..d6b9d34c5ab3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -22,6 +22,7 @@ import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static com.android.wm.shell.transition.DefaultTransitionHandler.isSupportedOverrideAnimation; +import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; import static java.util.Objects.requireNonNull; @@ -90,6 +91,12 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle /** Whether ActivityEmbeddingController should animate this transition. */ public boolean shouldAnimate(@NonNull TransitionInfo info) { + if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) { + // The TRANSIT_TASK_FRAGMENT_DRAG_RESIZE type happens when the user drags the + // interactive divider to resize the split containers. The content is veiled, so we will + // handle the transition with a jump cut. + return true; + } boolean containsEmbeddingChange = false; for (TransitionInfo.Change change : info.getChanges()) { if (!change.hasFlags(FLAG_FILLS_TASK) && change.hasFlags( 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..196f89d5794e 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. @@ -49,9 +49,9 @@ public interface BackAnimation { @BackEvent.SwipeEdge int swipeEdge); /** - * Called when the input pointers are pilfered. + * Called when the back swipe threshold is crossed. */ - void onPilferPointers(); + void onThresholdCrossed(); /** * Sets whether the back gesture is past the trigger threshold or not. @@ -101,4 +101,10 @@ public interface BackAnimation { * @param customizer the controller to control system bar color. */ void setStatusBarCustomizer(StatusBarCustomizer customizer); + + /** + * Set a callback to pilfer pointers. + * @param pilferCallback the callback to pilfer pointers. + */ + void setPilferPointerCallback(Runnable pilferCallback); } 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..5600664a8f47 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 @@ -22,9 +22,6 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -32,6 +29,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; @@ -45,7 +43,6 @@ import android.os.UserHandle; import android.provider.Settings.Global; import android.util.DisplayMetrics; import android.util.Log; -import android.util.MathUtils; import android.view.IRemoteAnimationRunner; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -56,6 +53,7 @@ import android.window.BackAnimationAdapter; import android.window.BackEvent; import android.window.BackMotionEvent; import android.window.BackNavigationInfo; +import android.window.BackTouchTracker; import android.window.IBackAnimationFinishedCallback; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; @@ -64,12 +62,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 +80,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; @@ -114,7 +115,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont /** Tracks if we should start the back gesture on the next motion move event */ private boolean mShouldStartOnNextMoveEvent = false; private boolean mOnBackStartDispatched = false; - private boolean mPointerPilfered = false; + private boolean mThresholdCrossed = false; + private boolean mPointersPilfered = false; + private final boolean mRequirePointerPilfer; private final FlingAnimationUtils mFlingAnimationUtils; @@ -134,18 +137,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont /** * Tracks the current user back gesture. */ - private TouchTracker mCurrentTracker = new TouchTracker(); + private BackTouchTracker mCurrentTracker = new BackTouchTracker(); /** * Tracks the next back gesture in case a new user gesture has started while the back animation * (and navigation) associated with {@link #mCurrentTracker} have not yet finished. */ - private TouchTracker mQueuedTracker = new TouchTracker(); + private BackTouchTracker mQueuedTracker = new BackTouchTracker(); 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 +157,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mActiveCallback; + @Nullable + private RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( @@ -169,6 +174,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); }); } }); @@ -180,6 +187,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Keep previous navigation type before remove mBackNavigationInfo. @BackNavigationInfo.BackTargetType private int mPreviousNavigationType; + private Runnable mPilferPointerCallback; public BackAnimationController( @NonNull ShellInit shellInit, @@ -220,6 +228,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 +250,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 +300,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; } @@ -318,8 +334,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } @Override - public void onPilferPointers() { - BackAnimationController.this.onPilferPointers(); + public void onThresholdCrossed() { + BackAnimationController.this.onThresholdCrossed(); } @Override @@ -341,6 +357,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mCustomizer = customizer; mAnimationBackground.setStatusBarCustomizer(customizer); } + + @Override + public void setPilferPointerCallback(Runnable callback) { + mShellExecutor.execute(() -> { + mPilferPointerCallback = callback; + }); + } } private static class IBackAnimationImpl extends IBackAnimation.Stub @@ -397,20 +420,23 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellBackAnimationRegistry.unregisterAnimation(type); } - private TouchTracker getActiveTracker() { + private BackTouchTracker getActiveTracker() { if (mCurrentTracker.isActive()) return mCurrentTracker; if (mQueuedTracker.isActive()) return mQueuedTracker; return null; } @VisibleForTesting - void onPilferPointers() { - mPointerPilfered = true; + public void onThresholdCrossed() { + mThresholdCrossed = true; // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. - if (!shouldDispatchToAnimator() && mActiveCallback != null) { + final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); + if (!shouldDispatchToAnimator && mActiveCallback != null) { mCurrentTracker.updateStartLocation(); tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); + } else if (shouldDispatchToAnimator) { + tryPilferPointers(); } } @@ -426,7 +452,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont int keyAction, @BackEvent.SwipeEdge int swipeEdge) { - TouchTracker activeTouchTracker = getActiveTracker(); + BackTouchTracker activeTouchTracker = getActiveTracker(); if (activeTouchTracker != null) { activeTouchTracker.update(touchX, touchY, velocityX, velocityY); } @@ -462,7 +488,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { - TouchTracker touchTracker; + 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(); + } + BackTouchTracker touchTracker; if (mCurrentTracker.isInInitialState()) { touchTracker = mCurrentTracker; } else if (mQueuedTracker.isInInitialState()) { @@ -473,17 +507,23 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); - touchTracker.setState(TouchTracker.TouchTrackerState.ACTIVE); + touchTracker.setState(BackTouchTracker.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); } } - private void startBackNavigation(@NonNull TouchTracker touchTracker) { + private void startBackNavigation(@NonNull BackTouchTracker touchTracker) { try { startLatencyTracking(); mBackNavigationInfo = mActivityTaskManager.startBackNavigation( @@ -496,7 +536,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo, - @NonNull TouchTracker touchTracker) { + @NonNull BackTouchTracker touchTracker) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null."); @@ -509,6 +549,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mShellBackAnimationRegistry.startGesture(backType)) { mActiveCallback = null; } + tryPilferPointers(); } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); // App is handling back animation. Cancel system animation latency tracking. @@ -557,10 +598,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && mBackNavigationInfo.isPrepareRemoteAnimation(); } + private void tryPilferPointers() { + if (mPointersPilfered || !mThresholdCrossed) { + return; + } + if (mPilferPointerCallback != null) { + mPilferPointerCallback.run(); + } + mPointersPilfered = true; + } + private void tryDispatchOnBackStarted( IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (mOnBackStartDispatched || callback == null || !mPointerPilfered) { + if (mOnBackStartDispatched + || callback == null + || (!mThresholdCrossed && mRequirePointerPilfer)) { return; } dispatchOnBackStarted(callback, backEvent); @@ -580,79 +633,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - - /** - * Allows us to manage the fling gesture, it smoothly animates the current progress value to - * the final position, calculated based on the current velocity. - * - * @param callback the callback to be invoked when the animation ends. - */ - private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback, - @NonNull TouchTracker touchTracker) { - if (callback == null) { - return; - } - - boolean animationStarted = false; - - if (mBackNavigationInfo != null && mBackNavigationInfo.isAnimationCallback()) { - - final BackMotionEvent backMotionEvent = touchTracker.createProgressEvent(); - if (backMotionEvent != null) { - // Constraints - absolute values - float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond(); - float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond(); - float maxX = touchTracker.getMaxDistance(); // px - float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px - - // Current state - float currentX = backMotionEvent.getTouchX(); - float velocity = MathUtils.constrain(backMotionEvent.getVelocityX(), - -maxVelocity, maxVelocity); - - // Target state - float animationFaction = velocity / maxVelocity; // value between -1 and 1 - float flingDistance = animationFaction * maxFlingDistance; // px - float endX = MathUtils.constrain(currentX + flingDistance, 0f, maxX); - - if (!Float.isNaN(endX) - && currentX != endX - && Math.abs(velocity) >= minVelocity) { - ValueAnimator animator = ValueAnimator.ofFloat(currentX, endX); - - mFlingAnimationUtils.apply( - /* animator = */ animator, - /* currValue = */ currentX, - /* endValue = */ endX, - /* velocity = */ velocity, - /* maxDistance = */ maxFlingDistance - ); - - animator.addUpdateListener(animation -> { - Float animatedValue = (Float) animation.getAnimatedValue(); - float progress = touchTracker.getProgress(animatedValue); - final BackMotionEvent backEvent = touchTracker.createProgressEvent( - progress); - dispatchOnBackProgressed(mActiveCallback, backEvent); - }); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - dispatchOnBackInvoked(callback); - } - }); - animator.start(); - animationStarted = true; - } - } - } - - if (!animationStarted) { - dispatchOnBackInvoked(callback); - } - } - private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; @@ -664,7 +644,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void tryDispatchOnBackCancelled(IOnBackInvokedCallback callback) { + if (!mOnBackStartDispatched) { + Log.d(TAG, "Skipping dispatching onBackCancelled. Start was never dispatched."); + return; + } if (callback == null) { return; } @@ -677,7 +661,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (callback == null) { + if (callback == null || !shouldDispatchToAnimator()) { return; } try { @@ -691,7 +675,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ public void setTriggerBack(boolean triggerBack) { - TouchTracker activeBackGestureInfo = getActiveTracker(); + if (mActiveCallback != null) { + try { + mActiveCallback.setTriggerBack(triggerBack); + } catch (RemoteException e) { + Log.e(TAG, "remote setTriggerBack error: ", e); + } + } + BackTouchTracker activeBackGestureInfo = getActiveTracker(); if (activeBackGestureInfo != null) { activeBackGestureInfo.setTriggerBack(triggerBack); } @@ -705,7 +696,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); } - private void invokeOrCancelBack(@NonNull TouchTracker touchTracker) { + private void invokeOrCancelBack(@NonNull BackTouchTracker touchTracker) { // Make a synchronized call to core before dispatch back event to client side. // If the close transition happens before the core receives onAnimationFinished, there will // play a second close animation for that transition. @@ -721,9 +712,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mBackNavigationInfo != null) { final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); if (touchTracker.getTriggerBack()) { - dispatchOrAnimateOnBackInvoked(callback, touchTracker); + dispatchOnBackInvoked(callback); } else { - dispatchOnBackCancelled(callback); + tryDispatchOnBackCancelled(callback); } } finishBackNavigation(touchTracker.getTriggerBack()); @@ -733,7 +724,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Called when the gesture is released, then it could start the post commit animation. */ private void onGestureFinished() { - TouchTracker activeTouchTracker = getActiveTracker(); + BackTouchTracker activeTouchTracker = getActiveTracker(); if (!mBackGestureStarted || activeTouchTracker == null) { // This can happen when an unfinished gesture has been reset in resetTouchTracker ProtoLog.d(WM_SHELL_BACK_PREVIEW, @@ -743,8 +734,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean triggerBack = activeTouchTracker.getTriggerBack(); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack); + // Reset gesture states. + mThresholdCrossed = false; + mPointersPilfered = false; mBackGestureStarted = false; - activeTouchTracker.setState(TouchTracker.TouchTrackerState.FINISHED); + activeTouchTracker.setState(BackTouchTracker.TouchTrackerState.FINISHED); if (mPostCommitAnimationInProgress) { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running"); @@ -800,9 +794,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // The next callback should be {@link #onBackAnimationFinished}. if (mCurrentTracker.getTriggerBack()) { - dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker); + // notify gesture finished + mBackNavigationInfo.onBackGestureFinished(true); + dispatchOnBackInvoked(mActiveCallback); } else { - dispatchOnBackCancelled(mActiveCallback); + tryDispatchOnBackCancelled(mActiveCallback); } } @@ -812,6 +808,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; @@ -829,10 +839,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } /** - * Resets the TouchTracker and potentially starts a new back navigation in case one is queued + * Resets the BackTouchTracker and potentially starts a new back navigation in case one + * is queued. */ private void resetTouchTracker() { - TouchTracker temp = mCurrentTracker; + BackTouchTracker temp = mCurrentTracker; mCurrentTracker = mQueuedTracker; temp.reset(); mQueuedTracker = temp; @@ -840,7 +851,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,9 +883,11 @@ 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; + mThresholdCrossed = false; + mPointersPilfered = false; mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); if (mBackNavigationInfo != null) { @@ -908,6 +921,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 +984,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 +1035,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=" + mThresholdCrossed); + 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..c988c2fb5103 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -0,0 +1,529 @@ +/* + * 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.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.Color +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.view.animation.Transformation +import android.window.BackEvent +import android.window.BackMotionEvent +import android.window.BackNavigationInfo +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 kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +abstract class CrossActivityBackAnimation( + private val context: Context, + private val background: BackAnimationBackground, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + protected val transaction: SurfaceControl.Transaction, + private val choreographer: Choreographer +) : ShellBackAnimation() { + + protected val startClosingRect = RectF() + protected val targetClosingRect = RectF() + protected val currentClosingRect = RectF() + + protected val startEnteringRect = RectF() + protected val targetEnteringRect = RectF() + protected val currentEnteringRect = RectF() + + protected val backAnimRect = Rect() + private val cropRect = 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) + protected var enteringTarget: RemoteAnimationTarget? = null + protected var closingTarget: RemoteAnimationTarget? = null + 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 gestureInterpolator = Interpolators.BACK_GESTURE + private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator() + + private var scrimLayer: SurfaceControl? = null + private var maxScrimAlpha: Float = 0f + + private var isLetterboxed = false + private var enteringHasSameLetterbox = false + private var leftLetterboxLayer: SurfaceControl? = null + private var rightLetterboxLayer: SurfaceControl? = null + private var letterboxColor: Int = 0 + + /** Background color to be used during the animation, also see [getBackgroundColor] */ + protected var customizedBackgroundColor = 0 + + /** + * Whether the entering target should be shifted vertically with the user gesture in pre-commit + */ + abstract val allowEnteringYShift: Boolean + + /** + * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement + * of the enteringTarget during pre-commit phase. + */ + abstract fun preparePreCommitEnteringRectMovement() + + /** + * Returns a base transformation to apply to the entering target during pre-commit. The system + * will apply the default animation on top of it. + */ + protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? = + null + + override fun onConfigurationChanged(newConfiguration: Configuration) { + cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + } + + override fun getRunner() = backAnimationRunner + + private fun getBackgroundColor(): Int = + when { + customizedBackgroundColor != 0 -> customizedBackgroundColor + isLetterboxed -> letterboxColor + enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor + else -> 0 + } + + protected open 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() + isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed + enteringHasSameLetterbox = + isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds) + + if (isLetterboxed && !enteringHasSameLetterbox) { + // Play animation with letterboxes, if closing and entering target have mismatching + // letterboxes + backAnimRect.set(closingTarget!!.windowConfiguration.bounds) + } else { + // otherwise play animation on localBounds only + backAnimRect.set(closingTarget!!.localBounds) + } + // Offset start rectangle to align task bounds. + 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 + ) + } + + preparePreCommitEnteringRectMovement() + + background.ensureBackground( + closingTarget!!.windowConfiguration.bounds, + getBackgroundColor(), + transaction + ) + ensureScrimLayer() + if (isLetterboxed && enteringHasSameLetterbox) { + // crop left and right letterboxes + cropRect.set( + closingTarget!!.localBounds.left, + 0, + closingTarget!!.localBounds.right, + closingTarget!!.windowConfiguration.bounds.height() + ) + // and add fake letterbox square surfaces instead + ensureLetterboxes() + } else { + cropRect.set(backAnimRect) + } + 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) + if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset) + val enteringTransformation = getPreCommitEnteringBaseTransformation(progress) + applyTransform( + enteringTarget?.leash, + currentEnteringRect, + enteringTransformation?.alpha ?: 1f, + enteringTransformation + ) + 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 = + max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) * + interpolatedYRatio * + yDirection + return deltaY + } + + protected open fun onGestureCommitted() { + if ( + closingTarget?.leash == null || + enteringTarget?.leash == null || + !enteringTarget!!.leash.isValid || + !closingTarget!!.leash.isValid + ) { + finishAnimation() + return + } + + val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_COMMIT_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() + } + + protected open fun onPostCommitProgress(linearProgress: Float) { + scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) } + } + + protected open 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() + removeLetterbox() + isLetterboxed = false + enteringHasSameLetterbox = false + } + + protected fun applyTransform( + leash: SurfaceControl?, + rect: RectF, + alpha: Float, + baseTransformation: Transformation? = null + ) { + if (leash == null || !leash.isValid) return + val scale = rect.width() / backAnimRect.width() + val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() } + val scalePivotX = + if (isLetterboxed && enteringHasSameLetterbox) { + closingTarget!!.localBounds.left.toFloat() + } else { + 0f + } + matrix.postScale(scale, scale, scalePivotX, 0f) + matrix.postTranslate(rect.left, rect.top) + transaction + .setAlpha(leash, keepMinimumAlpha(alpha)) + .setMatrix(leash, matrix, tmpFloat9) + .setCrop(leash, cropRect) + .setCornerRadius(leash, cornerRadius) + } + + protected fun applyTransaction() { + transaction.setFrameTimelineVsync(choreographer.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 + val scrimCrop = + if (isLetterboxed) { + closingTarget!!.windowConfiguration.bounds + } else { + closingTarget!!.localBounds + } + transaction + .setColor(scrimLayer, colorComponents) + .setAlpha(scrimLayer!!, maxScrimAlpha) + .setCrop(scrimLayer!!, scrimCrop) + .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1) + .show(scrimLayer) + } + + private fun removeScrimLayer() { + if (removeLayer(scrimLayer)) applyTransaction() + scrimLayer = null + } + + /** + * Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the + * closing target + */ + private fun ensureLetterboxes() { + closingTarget?.let { t -> + if (t.localBounds.left != 0 && leftLetterboxLayer == null) { + val bounds = + Rect( + 0, + t.windowConfiguration.bounds.top, + t.localBounds.left, + t.windowConfiguration.bounds.bottom + ) + leftLetterboxLayer = ensureLetterbox(bounds) + } + if ( + t.localBounds.right != t.windowConfiguration.bounds.right && + rightLetterboxLayer == null + ) { + val bounds = + Rect( + t.localBounds.right, + t.windowConfiguration.bounds.top, + t.windowConfiguration.bounds.right, + t.windowConfiguration.bounds.bottom + ) + rightLetterboxLayer = ensureLetterbox(bounds) + } + } + } + + private fun ensureLetterbox(bounds: Rect): SurfaceControl { + val letterboxBuilder = + SurfaceControl.Builder() + .setName("Cross-Activity back animation letterbox") + .setCallsite("CrossActivityBackAnimation") + .setColorLayer() + .setOpaque(true) + .setHidden(false) + + rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder) + val layer = letterboxBuilder.build() + val colorComponents = + floatArrayOf( + Color.red(letterboxColor) / 255f, + Color.green(letterboxColor) / 255f, + Color.blue(letterboxColor) / 255f + ) + transaction + .setColor(layer, colorComponents) + .setCrop(layer, bounds) + .setRelativeLayer(layer, closingTarget!!.leash, 1) + .show(layer) + return layer + } + + private fun removeLetterbox() { + if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction() + leftLetterboxLayer = null + rightLetterboxLayer = null + } + + private fun removeLayer(layer: SurfaceControl?): Boolean { + layer?.let { + if (it.isValid) { + transaction.remove(it) + return true + } + } + return false + } + + override fun prepareNextAnimation( + animationInfo: BackNavigationInfo.CustomAnimationInfo?, + letterboxColor: Int + ): Boolean { + this.letterboxColor = letterboxColor + return false + } + + 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 closing window. */ + internal const val MAX_SCALE = 0.9f + private const val MAX_SCRIM_ALPHA_DARK = 0.8f + private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f + private const val POST_COMMIT_DURATION = 300L + } +} + +// The target will loose focus when alpha == 0, so keep a minimum value for it. +private fun keepMinimumAlpha(transAlpha: Float): Float { + return max(transAlpha.toDouble(), 0.005).toFloat() +} + +private fun isDarkMode(context: Context): Boolean { + return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES +} + +internal 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 +} + +internal 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/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt new file mode 100644 index 000000000000..e6ec2b449616 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt @@ -0,0 +1,256 @@ +/* + * 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.back + +import android.content.Context +import android.graphics.Rect +import android.graphics.RectF +import android.util.MathUtils +import android.view.Choreographer +import android.view.SurfaceControl +import android.view.animation.Animation +import android.view.animation.Transformation +import android.window.BackMotionEvent +import android.window.BackNavigationInfo +import com.android.internal.R +import com.android.internal.policy.TransitionAnimation +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.annotations.ShellMainThread +import javax.inject.Inject + +/** Class that handles customized predictive cross activity back animations. */ +@ShellMainThread +class CustomCrossActivityBackAnimation( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + transaction: SurfaceControl.Transaction, + choreographer: Choreographer, + private val customAnimationLoader: CustomAnimationLoader +) : + CrossActivityBackAnimation( + context, + background, + rootTaskDisplayAreaOrganizer, + transaction, + choreographer + ) { + + private var enterAnimation: Animation? = null + private var closeAnimation: Animation? = null + private val transformation = Transformation() + private var gestureProgress = 0f + + override val allowEnteringYShift = false + + @Inject + constructor( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + ) : this( + context, + background, + rootTaskDisplayAreaOrganizer, + SurfaceControl.Transaction(), + Choreographer.getInstance(), + CustomAnimationLoader( + TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation") + ) + ) + + override fun preparePreCommitEnteringRectMovement() { + // No movement for the entering rect + startEnteringRect.set(startClosingRect) + targetEnteringRect.set(startClosingRect) + } + + override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation { + gestureProgress = progress + transformation.clear() + enterAnimation!!.getTransformationAt(progress * PRE_COMMIT_MAX_PROGRESS, transformation) + return transformation + } + + override fun startBackAnimation(backMotionEvent: BackMotionEvent) { + super.startBackAnimation(backMotionEvent) + if ( + closeAnimation == null || + enterAnimation == null || + closingTarget == null || + enteringTarget == null + ) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, + "Enter animation or close animation is null." + ) + return + } + initializeAnimation(closeAnimation!!, closingTarget!!.localBounds) + initializeAnimation(enterAnimation!!, enteringTarget!!.localBounds) + } + + override fun onPostCommitProgress(linearProgress: Float) { + super.onPostCommitProgress(linearProgress) + if (closingTarget == null || enteringTarget == null) return + + // TODO: Should we use the duration from the custom xml spec for the post-commit animation? + applyTransform(closingTarget!!.leash, currentClosingRect, linearProgress, closeAnimation!!) + val enteringProgress = + MathUtils.lerp(gestureProgress * PRE_COMMIT_MAX_PROGRESS, 1f, linearProgress) + applyTransform( + enteringTarget!!.leash, + currentEnteringRect, + enteringProgress, + enterAnimation!! + ) + applyTransaction() + } + + private fun applyTransform( + leash: SurfaceControl, + rect: RectF, + progress: Float, + animation: Animation + ) { + transformation.clear() + animation.getTransformationAt(progress, transformation) + applyTransform(leash, rect, transformation.alpha, transformation) + } + + override fun finishAnimation() { + closeAnimation?.reset() + closeAnimation = null + enterAnimation?.reset() + enterAnimation = null + transformation.clear() + gestureProgress = 0f + super.finishAnimation() + } + + /** Load customize animation before animation start. */ + override fun prepareNextAnimation( + animationInfo: BackNavigationInfo.CustomAnimationInfo?, + letterboxColor: Int + ): Boolean { + super.prepareNextAnimation(animationInfo, letterboxColor) + if (animationInfo == null) return false + customAnimationLoader.loadAll(animationInfo)?.let { result -> + closeAnimation = result.closeAnimation + enterAnimation = result.enterAnimation + customizedBackgroundColor = result.backgroundColor + return true + } + return false + } + + class AnimationLoadResult { + var closeAnimation: Animation? = null + var enterAnimation: Animation? = null + var backgroundColor = 0 + } + + companion object { + private const val PRE_COMMIT_MAX_PROGRESS = 0.2f + } +} + +/** Helper class to load custom animation. */ +class CustomAnimationLoader(private val transitionAnimation: TransitionAnimation) { + + /** + * Load both enter and exit animation for the close activity transition. Note that the result is + * only valid if the exit animation has set and loaded success. If the entering animation has + * not set(i.e. 0), here will load the default entering animation for it. + * + * @param animationInfo The information of customize animation, which can be set from + * [Activity.overrideActivityTransition] and/or [LayoutParams.windowAnimations] + */ + fun loadAll( + animationInfo: BackNavigationInfo.CustomAnimationInfo + ): CustomCrossActivityBackAnimation.AnimationLoadResult? { + if (animationInfo.packageName.isEmpty()) return null + val close = loadAnimation(animationInfo, false) ?: return null + val open = loadAnimation(animationInfo, true) + val result = CustomCrossActivityBackAnimation.AnimationLoadResult() + result.closeAnimation = close + result.enterAnimation = open + result.backgroundColor = animationInfo.customBackground + return result + } + + /** + * Load enter or exit animation from CustomAnimationInfo + * + * @param animationInfo The information for customize animation. + * @param enterAnimation true when load for enter animation, false for exit animation. + * @return Loaded animation. + */ + fun loadAnimation( + animationInfo: BackNavigationInfo.CustomAnimationInfo, + enterAnimation: Boolean + ): Animation? { + var a: Animation? = null + // Activity#overrideActivityTransition has higher priority than windowAnimations + // Try to get animation from Activity#overrideActivityTransition + if ( + enterAnimation && animationInfo.customEnterAnim != 0 || + !enterAnimation && animationInfo.customExitAnim != 0 + ) { + a = + transitionAnimation.loadAppTransitionAnimation( + animationInfo.packageName, + if (enterAnimation) animationInfo.customEnterAnim + else animationInfo.customExitAnim + ) + } else if (animationInfo.windowAnimations != 0) { + // try to get animation from LayoutParams#windowAnimations + a = + transitionAnimation.loadAnimationAttr( + animationInfo.packageName, + animationInfo.windowAnimations, + if (enterAnimation) R.styleable.WindowAnimation_activityCloseEnterAnimation + else R.styleable.WindowAnimation_activityCloseExitAnimation, + false /* translucent */ + ) + } + // Only allow to load default animation for opening target. + if (a == null && enterAnimation) { + a = loadDefaultOpenAnimation() + } + if (a != null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a) + } else { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "No custom animation loaded") + } + return a + } + + private fun loadDefaultOpenAnimation(): Animation? { + return transitionAnimation.loadDefaultAnimationAttr( + R.styleable.WindowAnimation_activityCloseEnterAnimation, + false /* translucent */ + ) + } +} + +private fun initializeAnimation(animation: Animation, bounds: Rect) { + val width = bounds.width() + val height = bounds.height() + animation.initialize(width, height, width, height) +} 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 deleted file mode 100644 index 5254ff466123..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.back; - -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -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.annotation.Nullable; -import android.app.Activity; -import android.content.Context; -import android.graphics.Color; -import android.graphics.Rect; -import android.os.RemoteException; -import android.util.FloatProperty; -import android.view.Choreographer; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.WindowManager.LayoutParams; -import android.view.animation.Animation; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Transformation; -import android.window.BackEvent; -import android.window.BackMotionEvent; -import android.window.BackNavigationInfo; -import android.window.BackProgressAnimator; -import android.window.IOnBackInvokedCallback; - -import com.android.internal.R; -import com.android.internal.dynamicanimation.animation.SpringAnimation; -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 javax.inject.Inject; - -/** Class that handle customized close activity transition animation. */ -@ShellMainThread -public class CustomizeActivityAnimation extends ShellBackAnimation { - private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); - private final BackAnimationRunner mBackAnimationRunner; - private final float mCornerRadius; - private final SurfaceControl.Transaction mTransaction; - private final BackAnimationBackground mBackground; - private RemoteAnimationTarget mEnteringTarget; - private RemoteAnimationTarget mClosingTarget; - private IRemoteAnimationFinishedCallback mFinishCallback; - /** Duration of post animation after gesture committed. */ - private static final int POST_ANIMATION_DURATION = 250; - - private static final int SCALE_FACTOR = 1000; - private final SpringAnimation mProgressSpring; - private float mLatestProgress = 0.0f; - - private static final float TARGET_COMMIT_PROGRESS = 0.5f; - - private final float[] mTmpFloat9 = new float[9]; - private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); - - final CustomAnimationLoader mCustomAnimationLoader; - private Animation mEnterAnimation; - private Animation mCloseAnimation; - private int mNextBackgroundColor; - final Transformation mTransformation = new Transformation(); - - private final Choreographer mChoreographer; - - @Inject - public CustomizeActivityAnimation(Context context, BackAnimationBackground background) { - this(context, background, new SurfaceControl.Transaction(), null); - } - - CustomizeActivityAnimation(Context context, BackAnimationBackground background, - SurfaceControl.Transaction transaction, Choreographer choreographer) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); - mBackground = background; - mBackAnimationRunner = new BackAnimationRunner( - new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY); - mCustomAnimationLoader = new CustomAnimationLoader(context); - - mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); - mProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction; - mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance(); - } - - private float getLatestProgress() { - return mLatestProgress * SCALE_FACTOR; - } - private void setLatestProgress(float value) { - mLatestProgress = value / SCALE_FACTOR; - applyTransformTransaction(mLatestProgress); - } - - private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP = - new FloatProperty<>("enter") { - @Override - public void setValue(CustomizeActivityAnimation anim, float value) { - anim.setLatestProgress(value); - } - - @Override - public Float get(CustomizeActivityAnimation object) { - return object.getLatestProgress(); - } - }; - - // The target will lose focus when alpha == 0, so keep a minimum value for it. - private static float keepMinimumAlpha(float transAlpha) { - return Math.max(transAlpha, 0.005f); - } - - private static void initializeAnimation(Animation animation, Rect bounds) { - final int width = bounds.width(); - final int height = bounds.height(); - animation.initialize(width, height, width, height); - } - - private void startBackAnimation() { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); - return; - } - initializeAnimation(mCloseAnimation, mClosingTarget.localBounds); - initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds); - - // Draw background with task background color. - if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) { - mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - mNextBackgroundColor == Color.TRANSPARENT - ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor() - : mNextBackgroundColor, - mTransaction); - } - } - - private void applyTransformTransaction(float progress) { - if (mClosingTarget == null || mEnteringTarget == null) { - return; - } - applyTransform(mClosingTarget.leash, progress, mCloseAnimation); - applyTransform(mEnteringTarget.leash, progress, mEnterAnimation); - mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); - mTransaction.apply(); - } - - private void applyTransform(SurfaceControl leash, float progress, Animation animation) { - mTransformation.clear(); - animation.getTransformationAt(progress, mTransformation); - mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9); - mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha())); - mTransaction.setCornerRadius(leash, mCornerRadius); - } - - void finishAnimation() { - if (mCloseAnimation != null) { - mCloseAnimation.reset(); - mCloseAnimation = null; - } - if (mEnterAnimation != null) { - mEnterAnimation.reset(); - mEnterAnimation = null; - } - if (mEnteringTarget != null) { - mEnteringTarget.leash.release(); - mEnteringTarget = null; - } - if (mClosingTarget != null) { - mClosingTarget.leash.release(); - mClosingTarget = null; - } - if (mBackground != null) { - mBackground.removeBackground(mTransaction); - } - mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); - mTransaction.apply(); - mTransformation.clear(); - mLatestProgress = 0; - mNextBackgroundColor = Color.TRANSPARENT; - if (mFinishCallback != null) { - try { - mFinishCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.printStackTrace(); - } - mFinishCallback = null; - } - mProgressSpring.animateToFinalPosition(0); - mProgressSpring.skipToEnd(); - } - - void onGestureProgress(@NonNull BackEvent backEvent) { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - return; - } - - final float progress = backEvent.getProgress(); - - float springProgress = (progress > 0.1f - ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f) - : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; - - mProgressSpring.animateToFinalPosition(springProgress); - } - - static float mapLinear(float x, float a1, float a2, float b1, float b2) { - return b1 + (x - a1) * (b2 - b1) / (a2 - a1); - } - - void onGestureCommitted() { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - finishAnimation(); - return; - } - mProgressSpring.cancel(); - - // Enter phase 2 of the animation - final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f) - .setDuration(POST_ANIMATION_DURATION); - valueAnimator.setInterpolator(mDecelerateInterpolator); - valueAnimator.addUpdateListener(animation -> { - float progress = (float) animation.getAnimatedValue(); - applyTransformTransaction(progress); - }); - - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finishAnimation(); - } - }); - valueAnimator.start(); - } - - /** Load customize animation before animation start. */ - @Override - public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) { - if (animationInfo == null) { - return false; - } - final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo); - if (result != null) { - mCloseAnimation = result.mCloseAnimation; - mEnterAnimation = result.mEnterAnimation; - mNextBackgroundColor = result.mBackgroundColor; - return true; - } - return false; - } - - @Override - public BackAnimationRunner getRunner() { - return mBackAnimationRunner; - } - - private final class Callback extends IOnBackInvokedCallback.Default { - @Override - public void onBackStarted(BackMotionEvent backEvent) { - mProgressAnimator.onBackStarted(backEvent, - CustomizeActivityAnimation.this::onGestureProgress); - } - - @Override - public void onBackProgressed(@NonNull BackMotionEvent backEvent) { - mProgressAnimator.onBackProgressed(backEvent); - } - - @Override - public void onBackCancelled() { - mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::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 customize animation."); - for (RemoteAnimationTarget a : apps) { - if (a.mode == MODE_CLOSING) { - mClosingTarget = a; - } - if (a.mode == MODE_OPENING) { - mEnteringTarget = a; - } - } - if (mCloseAnimation == null || mEnterAnimation == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, - "No animation loaded, should choose cross-activity animation?"); - } - - startBackAnimation(); - mFinishCallback = finishedCallback; - } - - @Override - public void onAnimationCancelled() { - finishAnimation(); - } - } - - - static final class AnimationLoadResult { - Animation mCloseAnimation; - Animation mEnterAnimation; - int mBackgroundColor; - } - - /** - * Helper class to load custom animation. - */ - static class CustomAnimationLoader { - final TransitionAnimation mTransitionAnimation; - - CustomAnimationLoader(Context context) { - mTransitionAnimation = new TransitionAnimation( - context, false /* debug */, "CustomizeBackAnimation"); - } - - /** - * Load both enter and exit animation for the close activity transition. - * Note that the result is only valid if the exit animation has set and loaded success. - * If the entering animation has not set(i.e. 0), here will load the default entering - * animation for it. - * - * @param animationInfo The information of customize animation, which can be set from - * {@link Activity#overrideActivityTransition} and/or - * {@link LayoutParams#windowAnimations} - */ - AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) { - if (animationInfo.getPackageName().isEmpty()) { - return null; - } - final Animation close = loadAnimation(animationInfo, false); - if (close == null) { - return null; - } - final Animation open = loadAnimation(animationInfo, true); - AnimationLoadResult result = new AnimationLoadResult(); - result.mCloseAnimation = close; - result.mEnterAnimation = open; - result.mBackgroundColor = animationInfo.getCustomBackground(); - return result; - } - - /** - * Load enter or exit animation from CustomAnimationInfo - * @param animationInfo The information for customize animation. - * @param enterAnimation true when load for enter animation, false for exit animation. - * @return Loaded animation. - */ - @Nullable - Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, - boolean enterAnimation) { - Animation a = null; - // Activity#overrideActivityTransition has higher priority than windowAnimations - // Try to get animation from Activity#overrideActivityTransition - if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0) - || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) { - a = mTransitionAnimation.loadAppTransitionAnimation( - animationInfo.getPackageName(), - enterAnimation ? animationInfo.getCustomEnterAnim() - : animationInfo.getCustomExitAnim()); - } else if (animationInfo.getWindowAnimations() != 0) { - // try to get animation from LayoutParams#windowAnimations - a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(), - animationInfo.getWindowAnimations(), enterAnimation - ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation, - false /* translucent */); - } - // Only allow to load default animation for opening target. - if (a == null && enterAnimation) { - a = loadDefaultOpenAnimation(); - } - if (a != null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a); - } else { - ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded"); - } - return a; - } - - private Animation loadDefaultOpenAnimation() { - return mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_activityCloseEnterAnimation, - false /* translucent */); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt new file mode 100644 index 000000000000..f33c5b9bd183 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt @@ -0,0 +1,81 @@ +/* + * 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.back + +import android.content.Context +import android.view.Choreographer +import android.view.SurfaceControl +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.annotations.ShellMainThread +import javax.inject.Inject +import kotlin.math.max + +/** Class that defines cross-activity animation. */ +@ShellMainThread +class DefaultCrossActivityBackAnimation +@Inject +constructor( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +) : + CrossActivityBackAnimation( + context, + background, + rootTaskDisplayAreaOrganizer, + SurfaceControl.Transaction(), + Choreographer.getInstance() + ) { + + private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN + private val enteringStartOffset = + context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) + override val allowEnteringYShift = true + + override fun preparePreCommitEnteringRectMovement() { + // 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) + } + + override fun onGestureCommitted() { + // 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 in the superclass + startClosingRect.set(currentClosingRect) + startEnteringRect.set(currentEnteringRect) + targetEnteringRect.set(backAnimRect) + targetClosingRect.set(backAnimRect) + targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f) + super.onGestureCommitted() + } + + override fun onPostCommitProgress(linearProgress: Float) { + super.onPostCommitProgress(linearProgress) + val closingAlpha = max(1f - linearProgress * 2, 0f) + val progress = postCommitInterpolator.getInterpolation(linearProgress) + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + applyTransaction() + } +} 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..9cd193b0f74c 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; @@ -41,11 +42,16 @@ public abstract class ShellBackAnimation { public abstract BackAnimationRunner getRunner(); /** - * Prepare the next animation with customized animation. + * Prepare the next animation. * * @return true if this type of back animation should override the default. */ - public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) { + public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, + int letterboxColor) { 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..6fafa75e2f70 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,17 +136,32 @@ 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 if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) { if (mCustomizeActivityAnimation != null && mCustomizeActivityAnimation.prepareNextAnimation( - backNavigationInfo.getCustomAnimationInfo())) { + backNavigationInfo.getCustomAnimationInfo(), 0)) { mAnimationDefinition.get(type).resetWaitingAnimation(); mAnimationDefinition.set( BackNavigationInfo.TYPE_CROSS_ACTIVITY, mCustomizeActivityAnimation.getRunner()); + } else if (mDefaultCrossActivityAnimation != null) { + mDefaultCrossActivityAnimation.prepareNextAnimation(null, + backNavigationInfo.getLetterboxColor()); } } BackAnimationRunner runner = mAnimationDefinition.get(type); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java deleted file mode 100644 index 8f04f126960c..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ /dev/null @@ -1,240 +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 android.annotation.FloatRange; -import android.os.SystemProperties; -import android.util.MathUtils; -import android.view.MotionEvent; -import android.view.RemoteAnimationTarget; -import android.window.BackEvent; -import android.window.BackMotionEvent; - -import java.io.PrintWriter; - -/** - * Helper class to record the touch location for gesture and generate back events. - */ -class TouchTracker { - private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP = - "persist.wm.debug.predictive_back_linear_distance"; - private static final int LINEAR_DISTANCE = SystemProperties - .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1); - private float mLinearDistance = LINEAR_DISTANCE; - private float mMaxDistance; - private float mNonLinearFactor; - /** - * Location of the latest touch event - */ - private float mLatestTouchX; - private float mLatestTouchY; - private boolean mTriggerBack; - - /** - * Location of the initial touch event of the back gesture. - */ - private float mInitTouchX; - private float mInitTouchY; - private float mLatestVelocityX; - private float mLatestVelocityY; - private float mStartThresholdX; - private int mSwipeEdge; - private TouchTrackerState mState = TouchTrackerState.INITIAL; - - void update(float touchX, float touchY, float velocityX, float velocityY) { - /** - * If back was previously cancelled but the user has started swiping in the forward - * direction again, restart back. - */ - if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT) - || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { - mStartThresholdX = touchX; - if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX) - || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) { - mInitTouchX = mStartThresholdX; - } - } - mLatestTouchX = touchX; - mLatestTouchY = touchY; - mLatestVelocityX = velocityX; - mLatestVelocityY = velocityY; - } - - void setTriggerBack(boolean triggerBack) { - if (mTriggerBack != triggerBack && !triggerBack) { - mStartThresholdX = mLatestTouchX; - } - mTriggerBack = triggerBack; - } - - boolean getTriggerBack() { - return mTriggerBack; - } - - void setState(TouchTrackerState state) { - mState = state; - } - - boolean isInInitialState() { - return mState == TouchTrackerState.INITIAL; - } - - boolean isActive() { - return mState == TouchTrackerState.ACTIVE; - } - - boolean isFinished() { - return mState == TouchTrackerState.FINISHED; - } - - void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { - mInitTouchX = touchX; - mInitTouchY = touchY; - mLatestTouchX = touchX; - mLatestTouchY = touchY; - mSwipeEdge = swipeEdge; - mStartThresholdX = mInitTouchX; - } - - /** Update the start location used to compute the progress - * to the latest touch location. - */ - void updateStartLocation() { - mInitTouchX = mLatestTouchX; - mInitTouchY = mLatestTouchY; - mStartThresholdX = mInitTouchX; - } - - void reset() { - mInitTouchX = 0; - mInitTouchY = 0; - mStartThresholdX = 0; - mTriggerBack = false; - mState = TouchTrackerState.INITIAL; - mSwipeEdge = BackEvent.EDGE_LEFT; - } - - BackMotionEvent createStartEvent(RemoteAnimationTarget target) { - return new BackMotionEvent( - /* touchX = */ mInitTouchX, - /* touchY = */ mInitTouchY, - /* progress = */ 0, - /* velocityX = */ 0, - /* velocityY = */ 0, - /* triggerBack = */ mTriggerBack, - /* swipeEdge = */ mSwipeEdge, - /* departingAnimationTarget = */ target); - } - - BackMotionEvent createProgressEvent() { - float progress = getProgress(mLatestTouchX); - return createProgressEvent(progress); - } - - /** - * Progress value computed from the touch position. - * - * @param touchX the X touch position of the {@link MotionEvent}. - * @return progress value - */ - @FloatRange(from = 0.0, to = 1.0) - float getProgress(float touchX) { - // If back is committed, progress is the distance between the last and first touch - // point, divided by the max drag distance. Otherwise, it's the distance between - // the last touch point and the starting threshold, divided by max drag distance. - // The starting threshold is initially the first touch location, and updated to - // the location everytime back is restarted after being cancelled. - float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; - float distance; - if (mSwipeEdge == BackEvent.EDGE_LEFT) { - distance = touchX - startX; - } else { - distance = startX - touchX; - } - float deltaX = Math.max(0f, distance); - float linearDistance = mLinearDistance; - float maxDistance = getMaxDistance(); - maxDistance = maxDistance == 0 ? 1 : maxDistance; - float progress; - if (linearDistance < maxDistance) { - // Up to linearDistance it behaves linearly, then slowly reaches 1f. - - // maxDistance is composed of linearDistance + nonLinearDistance - float nonLinearDistance = maxDistance - linearDistance; - float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor; - - boolean isLinear = deltaX <= linearDistance; - if (isLinear) { - progress = deltaX / initialTarget; - } else { - float nonLinearDeltaX = deltaX - linearDistance; - float nonLinearProgress = nonLinearDeltaX / nonLinearDistance; - float currentTarget = MathUtils.lerp( - /* start = */ initialTarget, - /* stop = */ maxDistance, - /* amount = */ nonLinearProgress); - progress = deltaX / currentTarget; - } - } else { - // Always linear behavior. - progress = deltaX / maxDistance; - } - return MathUtils.constrain(progress, 0, 1); - } - - /** - * Maximum distance in pixels. - * Progress is considered to be completed (1f) when this limit is exceeded. - */ - float getMaxDistance() { - return mMaxDistance; - } - - BackMotionEvent createProgressEvent(float progress) { - return new BackMotionEvent( - /* touchX = */ mLatestTouchX, - /* touchY = */ mLatestTouchY, - /* progress = */ progress, - /* velocityX = */ mLatestVelocityX, - /* velocityY = */ mLatestVelocityY, - /* triggerBack = */ mTriggerBack, - /* swipeEdge = */ mSwipeEdge, - /* departingAnimationTarget = */ null); - } - - public void setProgressThresholds(float linearDistance, float maxDistance, - float nonLinearFactor) { - if (LINEAR_DISTANCE >= 0) { - mLinearDistance = LINEAR_DISTANCE; - } else { - mLinearDistance = linearDistance; - } - mMaxDistance = maxDistance; - mNonLinearFactor = nonLinearFactor; - } - - void dump(PrintWriter pw, String prefix) { - pw.println(prefix + "TouchTracker state:"); - pw.println(prefix + " mState=" + mState); - pw.println(prefix + " mTriggerBack=" + mTriggerBack); - } - - enum TouchTrackerState { - INITIAL, ACTIVE, FINISHED - } - -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index a67821b7e819..f9a1d940c734 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -82,8 +82,8 @@ public class BadgedImageView extends ConstraintLayout { private BubbleViewProvider mBubble; private BubblePositioner mPositioner; - private boolean mOnLeft; - + private boolean mBadgeOnLeft; + private boolean mDotOnLeft; private DotRenderer mDotRenderer; private DotRenderer.DrawParams mDrawParams; private int mDotColor; @@ -153,7 +153,8 @@ public class BadgedImageView extends ConstraintLayout { public void hideDotAndBadge(boolean onLeft) { addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK); - mOnLeft = onLeft; + mBadgeOnLeft = onLeft; + mDotOnLeft = onLeft; hideBadge(); } @@ -185,7 +186,7 @@ public class BadgedImageView extends ConstraintLayout { mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; - mDrawParams.leftAlign = mOnLeft; + mDrawParams.leftAlign = mDotOnLeft; mDrawParams.scale = mDotScale; mDotRenderer.draw(canvas, mDrawParams); @@ -255,7 +256,7 @@ public class BadgedImageView extends ConstraintLayout { * Whether decorations (badges or dots) are on the left. */ boolean getDotOnLeft() { - return mOnLeft; + return mDotOnLeft; } /** @@ -263,7 +264,7 @@ public class BadgedImageView extends ConstraintLayout { */ float[] getDotCenter() { float[] dotPosition; - if (mOnLeft) { + if (mDotOnLeft) { dotPosition = mDotRenderer.getLeftDotPosition(); } else { dotPosition = mDotRenderer.getRightDotPosition(); @@ -288,22 +289,26 @@ public class BadgedImageView extends ConstraintLayout { /** Sets the position of the dot and badge, animating them out and back in if requested. */ void animateDotBadgePositions(boolean onLeft) { - mOnLeft = onLeft; - - if (onLeft != getDotOnLeft() && shouldDrawDot()) { - animateDotScale(0f /* showDot */, () -> { - invalidate(); - animateDotScale(1.0f, null /* after */); - }); + if (onLeft != getDotOnLeft()) { + if (shouldDrawDot()) { + animateDotScale(0f /* showDot */, () -> { + mDotOnLeft = onLeft; + invalidate(); + animateDotScale(1.0f, null /* after */); + }); + } else { + mDotOnLeft = onLeft; + } } + mBadgeOnLeft = onLeft; // TODO animate badge showBadge(); - } /** Sets the position of the dot and badge. */ void setDotBadgeOnLeft(boolean onLeft) { - mOnLeft = onLeft; + mBadgeOnLeft = onLeft; + mDotOnLeft = onLeft; invalidate(); showBadge(); } @@ -358,7 +363,7 @@ public class BadgedImageView extends ConstraintLayout { } int translationX; - if (mOnLeft) { + if (mBadgeOnLeft) { translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth()); } else { translationX = 0; 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 ea59715bc246..38c344322a30 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 @@ -87,6 +87,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -101,13 +102,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; @@ -169,6 +171,8 @@ public class BubbleController implements ConfigurationChangeListener, * the pointer might need to be updated. */ void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); + /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */ + void bubbleOverflowChanged(boolean hasBubbles); } private final Context mContext; @@ -454,8 +458,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; } } @@ -597,13 +600,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). @@ -713,6 +709,41 @@ 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); + } + } + + /** + * Animate bubble bar to the given location. The location change is transient. It does not + * update the state of the bubble bar. + * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. + */ + public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + if (canShowAsBubbleBar()) { + mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation); + } + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -1134,16 +1165,50 @@ public class BubbleController implements ConfigurationChangeListener, } /** - * Update expanded state when a single bubble is dragged in Launcher. + * A bubble is being dragged in Launcher. + * Will be called only when bubble bar is expanded. + * + * @param bubbleKey key of the bubble being dragged + */ + public void startBubbleDrag(String bubbleKey) { + if (mBubbleData.getSelectedBubble() != null) { + mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false); + } + if (mBubbleStateListener != null) { + boolean overflow = BubbleOverflow.KEY.equals(bubbleKey); + Rect rect = new Rect(); + mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(), + overflow, rect); + BubbleBarUpdate update = new BubbleBarUpdate(); + update.expandedViewDropTargetSize = new Point(rect.width(), rect.height()); + mBubbleStateListener.onBubbleStateChange(update); + } + } + + /** + * A bubble is no longer being dragged in Launcher. And was released in given location. * Will be called only when bubble bar is expanded. - * @param bubbleKey key of the bubble to collapse/expand - * @param isBeingDragged whether the bubble is being dragged + * + * @param location location where bubble was released */ - public void onBubbleDrag(String bubbleKey, boolean isBeingDragged) { - if (mBubbleData.getSelectedBubble() != null - && mBubbleData.getSelectedBubble().getKey().equals(bubbleKey)) { - // Should collapse/expand only if equals to selected bubble. - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ !isBeingDragged); + public void stopBubbleDrag(BubbleBarLocation location) { + mBubblePositioner.setBubbleBarLocation(location); + if (mBubbleData.getSelectedBubble() != null) { + mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); + } + } + + /** + * A bubble was dragged and is released in dismiss target in Launcher. + * + * @param bubbleKey key of the bubble being dragged to dismiss target + */ + public void dragBubbleToDismiss(String bubbleKey) { + String selectedBubbleKey = mBubbleData.getSelectedBubbleKey(); + removeBubble(bubbleKey, Bubbles.DISMISS_USER_GESTURE); + if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) { + // We did not remove the selected bubble. Expand it again + mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); } } @@ -1184,7 +1249,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()); @@ -1227,8 +1292,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); @@ -1239,20 +1303,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) { @@ -1323,43 +1388,45 @@ public class BubbleController implements ConfigurationChangeListener, } String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user); - Log.i(TAG, "showOrHideAppBubble, key= " + appBubbleKey + " stackVisibility= " - + (mStackView != null ? mStackView.getVisibility() : " null ") - + " statusBarShade=" + mIsStatusBarShade); PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); - if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; + if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey); + ProtoLog.d(WM_SHELL_BUBBLES, + "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s " + + "statusBarShade=%s", + appBubbleKey, existingAppBubble, + (mStackView != null ? mStackView.getVisibility() : "null"), + mIsStatusBarShade); + if (existingAppBubble != null) { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); if (isStackExpanded()) { if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) { + ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey); // App bubble is expanded, lets collapse - Log.i(TAG, " showOrHideAppBubble, selected bubble is app bubble, collapsing"); collapseStack(); } else { + ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey); // App bubble is not selected, select it - Log.i(TAG, " showOrHideAppBubble, expanded, selecting existing app bubble"); mBubbleData.setSelectedBubble(existingAppBubble); } } else { + ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey); // 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 Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey); if (b != null) { // It's in the overflow, so remove it & reinflate - Log.i(TAG, " showOrHideAppBubble, expanding app bubble from overflow"); - mBubbleData.removeOverflowBubble(b); + mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); } else { // App bubble does not exist, lets add and expand it - Log.i(TAG, " showOrHideAppBubble, creating and expanding app bubble"); b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); } + ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey); b.setShouldAutoExpand(true); inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } @@ -1797,6 +1864,15 @@ public class BubbleController implements ConfigurationChangeListener, } } + + @Override + public void bubbleOverflowChanged(boolean hasBubbles) { + if (Flags.enableOptionalBubbleOverflow()) { + if (mStackView != null) { + mStackView.showOverflow(hasBubbles); + } + } + } }; /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */ @@ -1829,6 +1905,11 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void bubbleOverflowChanged(boolean hasBubbles) { + // Nothing to do for our views, handled by launcher / in the bubble bar. + } + + @Override public void suppressionChanged(Bubble bubble, boolean isSuppressed) { if (mLayerView != null) { // TODO (b/273316505) handle suppression changes, although might not need to @@ -1867,7 +1948,7 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" + " expanded=%b selectionChanged=%b selected=%s" - + " suppressed=%s unsupressed=%s shouldShowEducation=%b", + + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", update.addedBubble != null ? update.addedBubble.getKey() : "null", !update.removedBubbles.isEmpty(), update.updatedBubble != null ? update.updatedBubble.getKey() : "null", @@ -1876,13 +1957,17 @@ public class BubbleController implements ConfigurationChangeListener, update.selectedBubble != null ? update.selectedBubble.getKey() : "null", update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", - update.shouldShowEducation); + update.shouldShowEducation, update.showOverflowChanged); ensureBubbleViewsAndWindowCreated(); // Lazy load overflow bubbles from disk loadOverflowBubblesFromDisk(); + if (update.showOverflowChanged) { + mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty()); + } + // If bubbles in the overflow have a dot, make sure the overflow shows a dot updateOverflowButtonDot(); @@ -2239,15 +2324,19 @@ public class BubbleController implements ConfigurationChangeListener, private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener; private final Bubbles.BubbleStateListener mBubbleListener = new Bubbles.BubbleStateListener() { + @Override + public void onBubbleStateChange(BubbleBarUpdate update) { + Bundle b = new Bundle(); + b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); + b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); + mListener.call(l -> l.onBubbleStateChange(b)); + } - @Override - public void onBubbleStateChange(BubbleBarUpdate update) { - Bundle b = new Bundle(); - b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); - b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); - mListener.call(l -> l.onBubbleStateChange(b)); - } - }; + @Override + public void animateBubbleBarLocation(BubbleBarLocation location) { + mListener.call(l -> l.animateBubbleBarLocation(location)); + } + }; IBubblesImpl(BubbleController controller) { mController = controller; @@ -2282,12 +2371,6 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void removeBubble(String key) { - mMainExecutor.execute( - () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE)); - } - - @Override public void removeAllBubbles() { mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE)); } @@ -2298,8 +2381,18 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void onBubbleDrag(String bubbleKey, boolean isBeingDragged) { - mMainExecutor.execute(() -> mController.onBubbleDrag(bubbleKey, isBeingDragged)); + public void startBubbleDrag(String bubbleKey) { + mMainExecutor.execute(() -> mController.startBubbleDrag(bubbleKey)); + } + + @Override + public void stopBubbleDrag(BubbleBarLocation location) { + mMainExecutor.execute(() -> mController.stopBubbleDrag(location)); + } + + @Override + public void dragBubbleToDismiss(String key) { + mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key)); } @Override @@ -2307,6 +2400,20 @@ 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); + if (mLayerView != null) mLayerView.updateExpandedView(); + }); + } } private class BubblesImpl implements Bubbles { @@ -2617,6 +2724,15 @@ public class BubbleController implements ConfigurationChangeListener, () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged( sensitiveNotificationProtectionActive)); } + + @Override + public boolean canShowBubbleNotification() { + // in bubble bar mode, when the IME is visible we can't animate new bubbles. + if (BubbleController.this.isShowingAsBubbleBar()) { + return !BubbleController.this.mBubblePositioner.getIsImeVisible(); + } + return true; + } } /** 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..26483c8428c6 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 @@ -77,6 +77,7 @@ public class BubbleData { boolean suppressedSummaryChanged; boolean expanded; boolean shouldShowEducation; + boolean showOverflowChanged; @Nullable BubbleViewProvider selectedBubble; @Nullable Bubble addedBubble; @Nullable Bubble updatedBubble; @@ -109,7 +110,8 @@ public class BubbleData { || suppressedBubble != null || unsuppressedBubble != null || suppressedSummaryChanged - || suppressedSummaryGroup != null; + || suppressedSummaryGroup != null + || showOverflowChanged; } void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { @@ -157,6 +159,8 @@ public class BubbleData { bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey()); } } + bubbleBarUpdate.showOverflowChanged = showOverflowChanged; + bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); return bubbleBarUpdate; } @@ -165,7 +169,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 +259,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) { @@ -321,6 +327,14 @@ public class BubbleData { return mSelectedBubble; } + /** + * Returns the key of the selected bubble, or null if no bubble is selected. + */ + @Nullable + public String getSelectedBubbleKey() { + return mSelectedBubble != null ? mSelectedBubble.getKey() : null; + } + public BubbleOverflow getOverflow() { return mOverflow; } @@ -363,6 +377,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(); @@ -395,6 +422,9 @@ public class BubbleData { if (bubbleToReturn != null) { // Promoting from overflow mOverflowBubbles.remove(bubbleToReturn); + if (mOverflowBubbles.isEmpty()) { + mStateChange.showOverflowChanged = true; + } } else if (mPendingBubbles.containsKey(key)) { // Update while it was pending bubbleToReturn = mPendingBubbles.get(key); @@ -482,19 +512,6 @@ public class BubbleData { } /** - * Explicitly removes a bubble from the overflow, if it exists. - * - * @param bubble the bubble to remove. - */ - public void removeOverflowBubble(Bubble bubble) { - if (bubble == null) return; - if (mOverflowBubbles.remove(bubble)) { - mStateChange.removedOverflowBubble = bubble; - dispatchPendingChanges(); - } - } - - /** * Adds a group key indicating that the summary for this group should be suppressed. * * @param groupKey the group key of the group whose summary should be suppressed. @@ -668,7 +685,6 @@ public class BubbleData { if (indexToRemove == -1) { if (hasOverflowBubbleWithKey(key) && shouldRemoveHiddenBubble) { - Bubble b = getOverflowBubbleWithKey(key); ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel overflow bubble=%s", key); if (b != null) { @@ -678,6 +694,7 @@ public class BubbleData { mOverflowBubbles.remove(b); mStateChange.bubbleRemoved(b, reason); mStateChange.removedOverflowBubble = b; + mStateChange.showOverflowChanged = mOverflowBubbles.isEmpty(); } if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) { Bubble b = getSuppressedBubbleWithKey(key); @@ -777,6 +794,9 @@ public class BubbleData { } ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey()); mLogger.logOverflowAdd(bubble, reason); + if (mOverflowBubbles.isEmpty()) { + mStateChange.showOverflowChanged = true; + } mOverflowBubbles.remove(bubble); mOverflowBubbles.add(0, bubble); mStateChange.addedOverflowBubble = bubble; @@ -1216,9 +1236,7 @@ public class BubbleData { public void dump(PrintWriter pw) { pw.println("BubbleData state:"); pw.print(" selected: "); - pw.println(mSelectedBubble != null - ? mSelectedBubble.getKey() - : "null"); + pw.println(getSelectedBubbleKey()); pw.print(" expanded: "); pw.println(mExpanded); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 74f087b6d8f8..4e8afccee40f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -68,6 +68,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; @@ -446,6 +447,8 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.setVisibility(GONE); } else { mTaskView = bubbleTaskView.getTaskView(); + // reset the insets that might left after TaskView is shown in BubbleBarExpandedView + mTaskView.setCaptionInsets(null); bubbleTaskView.setDelegateListener(mTaskViewListener); // set a fixed width so it is not recalculated as part of a rotation. the width will be @@ -668,6 +671,11 @@ public class BubbleExpandedView extends LinearLayout { } } + /** Sets the alpha for the pointer. */ + public void setPointerAlpha(float alpha) { + mPointerView.setAlpha(alpha); + } + /** * Get alpha from underlying {@code TaskView} if this view is for a bubble. * Or get alpha for the overflow view if this view is for overflow. @@ -698,12 +706,14 @@ public class BubbleExpandedView extends LinearLayout { } } - /** - * Sets the alpha of the background and the pointer view. - */ + /** Sets the alpha of the background. */ public void setBackgroundAlpha(float alpha) { - mPointerView.setAlpha(alpha); - setAlpha(alpha); + if (Flags.enableNewBubbleAnimations()) { + setAlpha(alpha); + } else { + mPointerView.setAlpha(alpha); + setAlpha(alpha); + } } /** 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/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java index 633b01bde4ca..18e04d14c71b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -44,6 +44,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ContrastColorUtil; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import java.util.ArrayList; @@ -195,7 +196,9 @@ public class BubbleOverflowContainerView extends LinearLayout { } void updateEmptyStateVisibility() { - mEmptyState.setVisibility(mOverflowBubbles.isEmpty() ? View.VISIBLE : View.GONE); + boolean showEmptyState = mOverflowBubbles.isEmpty() + && !Flags.enableOptionalBubbleOverflow(); + mEmptyState.setVisibility(showEmptyState ? View.VISIBLE : View.GONE); mRecyclerView.setVisibility(mOverflowBubbles.isEmpty() ? View.GONE : View.VISIBLE); } 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..a35a004cdace 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 @@ -75,6 +76,7 @@ public class BubblePositioner { private int mBubblePaddingTop; private int mBubbleOffscreenAmount; private int mStackOffset; + private int mBubbleElevation; private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; @@ -95,6 +97,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) { @@ -145,11 +148,13 @@ public class BubblePositioner { mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 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); @@ -319,6 +324,11 @@ public class BubblePositioner { return 0; } + /** Returns whether the IME is visible. */ + public boolean getIsImeVisible() { + return mImeVisible; + } + /** Sets whether the IME is visible. **/ public void setImeVisible(boolean visible, int height) { mImeVisible = visible; @@ -659,6 +669,29 @@ public class BubblePositioner { } /** + * Returns the z translation a specific bubble should use. When expanded we keep a slight + * translation to ensure proper ordering when animating to / from collapsed state. When + * collapsed, only the top two bubbles appear so only their shadows show. + */ + public float getZTranslation(int index, boolean isOverflow, boolean isExpanded) { + if (isOverflow) { + return 0f; // overflow is lowest + } + return isExpanded + // When expanded use minimal amount to keep order + ? getMaxBubbles() - index + // When collapsed, only the top two bubbles have elevation + : index < NUM_VISIBLE_WHEN_RESTING + ? (getMaxBubbles() * mBubbleElevation) - index + : 0; + } + + /** The elevation to use for bubble UI elements. */ + public int getBubbleElevation() { + return mBubbleElevation; + } + + /** * @return whether the stack is considered on the left side of the screen. */ public boolean isStackOnLeft(PointF currentStackPosition) { @@ -797,14 +830,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 +870,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 +919,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..9fabd4247670 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 @@ -80,9 +80,9 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.Flags; 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 +95,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 +450,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 +479,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 */, @@ -530,7 +537,8 @@ public class BubbleStackView extends FrameLayout private OnClickListener mBubbleClickListener = new OnClickListener() { @Override public void onClick(View view) { - mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging. + // If the touch ended in a click, we're no longer dragging. + onDraggingEnded(); // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust @@ -664,7 +672,7 @@ public class BubbleStackView extends FrameLayout // First, see if the magnetized object consumes the event - if so, we shouldn't move the // bubble since it's stuck to the target. if (!passEventToMagnetizedObject(ev)) { - updateBubbleShadows(true /* showForAllBubbles */); + updateBubbleShadows(true /* isExpanded */); if (mBubbleData.isExpanded()) { mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); @@ -713,7 +721,7 @@ public class BubbleStackView extends FrameLayout mDismissView.hide(); } - mIsDraggingStack = false; + onDraggingEnded(); // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); @@ -856,6 +864,7 @@ public class BubbleStackView extends FrameLayout } }; + private boolean mShowingOverflow; private BubbleOverflow mBubbleOverflow; private StackEducationView mStackEduView; private StackEducationView.Manager mStackEducationViewManager; @@ -884,18 +893,17 @@ public class BubbleStackView extends FrameLayout mMainExecutor = mainExecutor; mManager = bubbleStackViewManager; + mPositioner = bubblePositioner; mBubbleData = data; mSysuiProxyProvider = sysuiProxyProvider; Resources res = getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); - mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mBubbleElevation = mPositioner.getBubbleElevation(); mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); - int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - mPositioner = bubblePositioner; final TypedArray ta = mContext.obtainStyledAttributes( new int[]{android.R.attr.dialogCornerRadius}); @@ -928,12 +936,12 @@ public class BubbleStackView extends FrameLayout mBubbleContainer = new PhysicsAnimationLayout(context); mBubbleContainer.setActiveController(mStackAnimationController); - mBubbleContainer.setElevation(elevation); + mBubbleContainer.setElevation(mBubbleElevation); mBubbleContainer.setClipChildren(false); addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mExpandedViewContainer = new FrameLayout(context); - mExpandedViewContainer.setElevation(elevation); + mExpandedViewContainer.setElevation(mBubbleElevation); mExpandedViewContainer.setClipChildren(false); addView(mExpandedViewContainer); @@ -986,18 +994,12 @@ public class BubbleStackView extends FrameLayout mBubbleOverflow = mBubbleData.getOverflow(); - resetOverflowView(); - mBubbleContainer.addView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() /* index */, - new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), - mPositioner.getBubbleSize())); - updateOverflow(); - mBubbleOverflow.getIconView().setOnClickListener((View v) -> { - mBubbleData.setShowingOverflow(true); - mBubbleData.setSelectedBubble(mBubbleOverflow); - mBubbleData.setExpanded(true); - }); - + if (Flags.enableOptionalBubbleOverflow()) { + showOverflow(mBubbleData.hasOverflowBubbles()); + } else { + mShowingOverflow = true; // if the flags not on this is always true + setUpOverflow(); + } mScrim = new View(getContext()); mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); mScrim.setBackgroundDrawable(new ColorDrawable( @@ -1019,6 +1021,7 @@ public class BubbleStackView extends FrameLayout WindowManager.class))); onDisplaySizeChanged(); mExpandedAnimationController.updateResources(); + mExpandedAnimationController.onOrientationChanged(); mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); @@ -1089,6 +1092,7 @@ public class BubbleStackView extends FrameLayout } else { maybeShowStackEdu(); } + onDraggingEnded(); }); animate() @@ -1146,6 +1150,14 @@ public class BubbleStackView extends FrameLayout } /** + * Reset state related to dragging. + */ + private void onDraggingEnded() { + mIsDraggingStack = false; + mMagnetizedObject = null; + } + + /** * Sets whether or not the stack should become temporarily invisible by moving off the side of * the screen. * @@ -1204,6 +1216,19 @@ public class BubbleStackView extends FrameLayout } }; + private void setUpOverflow() { + resetOverflowView(); + mBubbleContainer.addView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() /* index */, + new FrameLayout.LayoutParams(mBubbleSize, mBubbleSize)); + updateOverflow(); + mBubbleOverflow.getIconView().setOnClickListener((View v) -> { + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleOverflow); + mBubbleData.setExpanded(true); + }); + } + private void setUpDismissView() { if (mDismissView != null) { removeView(mDismissView); @@ -1442,24 +1467,56 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateFontSize(); } } - if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { + if (mShowingOverflow && mBubbleOverflow != null + && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateFontSize(); } } void updateLocale() { - if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { + if (mShowingOverflow && mBubbleOverflow != null + && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateLocale(); } } private void updateOverflow() { mBubbleOverflow.update(); - mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() - 1 /* index */); + if (mShowingOverflow) { + mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() - 1 /* index */); + } updateOverflowVisibility(); } + private void updateOverflowVisibility() { + mBubbleOverflow.setVisible(mShowingOverflow + && (mIsExpanded || mBubbleData.isShowingOverflow()) + ? VISIBLE + : GONE); + } + + private void updateOverflowDotVisibility(boolean expanding) { + if (mShowingOverflow && mBubbleOverflow.showDot()) { + mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> { + mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE); + }); + } + } + + /** Sets whether the overflow should be visible or not. */ + public void showOverflow(boolean showOverflow) { + if (!Flags.enableOptionalBubbleOverflow()) return; + if (mShowingOverflow != showOverflow) { + mShowingOverflow = showOverflow; + if (showOverflow) { + setUpOverflow(); + } else if (mBubbleOverflow != null) { + resetOverflowView(); + } + } + } + /** * Handle theme changes. */ @@ -1519,7 +1576,10 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateDimensions(); } } - mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); + if (mShowingOverflow) { + mBubbleOverflow.getIconView().setLayoutParams( + new LayoutParams(mBubbleSize, mBubbleSize)); + } mExpandedAnimationController.updateResources(); mStackAnimationController.updateResources(); mDismissView.updateResources(); @@ -1683,7 +1743,7 @@ public class BubbleStackView extends FrameLayout bubble.getIconView().setContentDescription(getResources().getString( R.string.bubble_content_description_single, titleStr, appName)); } else { - final int moreCount = mBubbleContainer.getChildCount() - 1; + final int moreCount = getBubbleCount(); bubble.getIconView().setContentDescription(getResources().getString( R.string.bubble_content_description_stack, titleStr, appName, moreCount)); @@ -1736,7 +1796,8 @@ public class BubbleStackView extends FrameLayout View bubbleOverflowIconView = mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null; - if (bubbleOverflowIconView != null && !mBubbleData.getBubbles().isEmpty()) { + if (mShowingOverflow && bubbleOverflowIconView != null + && !mBubbleData.getBubbles().isEmpty()) { Bubble lastBubble = mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1); View lastBubbleIconView = lastBubble.getIconView(); @@ -1854,7 +1915,7 @@ public class BubbleStackView extends FrameLayout bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); bubble.getIconView().setOnClickListener(mBubbleClickListener); bubble.getIconView().setOnTouchListener(mBubbleTouchListener); - updateBubbleShadows(false /* showForAllBubbles */); + updateBubbleShadows(mIsExpanded); animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); @@ -1912,20 +1973,6 @@ public class BubbleStackView extends FrameLayout } } - private void updateOverflowVisibility() { - mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow()) - ? VISIBLE - : GONE); - } - - private void updateOverflowDotVisibility(boolean expanding) { - if (mBubbleOverflow.showDot()) { - mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> { - mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE); - }); - } - } - // via BubbleData.Listener void updateBubble(Bubble bubble) { animateInFlyoutForBubble(bubble); @@ -1957,7 +2004,7 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded || isExpansionAnimating()) { reorder.run(); updateBadges(false /* setBadgeForCollapsedStack */); - updateZOrder(); + updateBubbleShadows(true /* isExpanded */); } else { List<View> bubbleViews = bubbles.stream() .map(b -> b.getIconView()).collect(Collectors.toList()); @@ -2200,7 +2247,7 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.addView(bubble.getIconView(), index, new LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); - updateBubbleShadows(false /* showForAllBubbles */); + updateBubbleShadows(mIsExpanded); requestUpdate(); } } @@ -2333,9 +2380,9 @@ public class BubbleStackView extends FrameLayout beforeExpandedViewAnimation(); showScrim(true, null /* runnable */); - updateZOrder(); - updateBadges(false /* setBadgeForCollapsedStack */); + updateBubbleShadows(mIsExpanded); mBubbleContainer.setActiveController(mExpandedAnimationController); + updateBadges(false /* setBadgeForCollapsedStack */); updateOverflowVisibility(); updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { @@ -2351,9 +2398,9 @@ public class BubbleStackView extends FrameLayout } else { index = getBubbleIndex(mExpandedBubble); } - PointF p = mPositioner.getExpandedBubbleXY(index, getState()); + PointF bubbleXY = mPositioner.getExpandedBubbleXY(index, getState()); final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, - mPositioner.showBubblesVertically() ? p.y : p.x); + mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); mExpandedViewContainer.setTranslationX(0f); mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); @@ -2364,40 +2411,42 @@ public class BubbleStackView extends FrameLayout ? mStackAnimationController.getStackPosition().y : mStackAnimationController.getStackPosition().x; final float bubbleWillBeAt = showVertically - ? p.y - : p.x; + ? bubbleXY.y + : bubbleXY.x; final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); // Wait for the path animation target to reach its end, and add a small amount of extra time // if the bubble is moving a lot horizontally. - long startDelay = 0L; + final long startDelay; // Should not happen since we lay out before expanding, but just in case... if (getWidth() > 0) { startDelay = (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f + (distanceAnimated / getWidth()) * 30); + } else { + startDelay = 0L; } // Set the pivot point for the scale, so the expanded view animates out from the bubble. if (showVertically) { float pivotX; if (mStackOnLeftOrWillBe) { - pivotX = p.x + mBubbleSize + mExpandedViewPadding; + pivotX = bubbleXY.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = p.x - mExpandedViewPadding; + pivotX = bubbleXY.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, pivotX, - p.y + mBubbleSize / 2f); + bubbleXY.y + mBubbleSize / 2f); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - p.x + mBubbleSize / 2f, - p.y + mBubbleSize + mExpandedViewPadding); + bubbleXY.x + mBubbleSize / 2f, + bubbleXY.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -2474,15 +2523,17 @@ 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); updateOverflowVisibility(); + animateShadows(); }); final Runnable after = () -> { @@ -2493,7 +2544,6 @@ public class BubbleStackView extends FrameLayout mManageEduView.hide(); } - updateZOrder(); updateBadges(true /* setBadgeForCollapsedStack */); afterExpandedViewAnimation(); if (previouslySelected != null) { @@ -2501,7 +2551,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 @@ -3327,19 +3378,23 @@ public class BubbleStackView extends FrameLayout * Updates whether each of the bubbles should show shadows. When collapsed & resting, only the * visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything * shows a shadow. When an individual bubble is dragged out, it should show a shadow. - */ - private void updateBubbleShadows(boolean showForAllBubbles) { - int bubbleCount = getBubbleCount(); - for (int i = 0; i < bubbleCount; i++) { - final float z = (mPositioner.getMaxBubbles() * mBubbleElevation) - i; - BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); - boolean isDraggedOut = mMagnetizedObject != null + * The bubble overflow is a special case and never has a shadow as it's ordered below the + * rest of the bubbles and isn't visible unless the stack is expanded. + * + * @param isExpanded whether the stack will be expanded or not when the shadows are applied. + */ + private void updateBubbleShadows(boolean isExpanded) { + final int childCount = mBubbleContainer.getChildCount(); + for (int i = 0; i < childCount; i++) { + final BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); + final boolean isOverflow = BubbleOverflow.KEY.equals(bv.getKey()); + final boolean isDraggedOut = mMagnetizedObject != null && mMagnetizedObject.getUnderlyingObject().equals(bv); - if (showForAllBubbles || isDraggedOut) { - bv.setZ(z); + if (isDraggedOut) { + // If it's dragged out, it's above all the other bubbles + bv.setZ((mPositioner.getMaxBubbles() * mBubbleElevation) + 1); } else { - final float tz = i < NUM_VISIBLE_WHEN_RESTING ? z : 0f; - bv.setZ(tz); + bv.setZ(mPositioner.getZTranslation(i, isOverflow, isExpanded)); } } } @@ -3360,16 +3415,6 @@ public class BubbleStackView extends FrameLayout } } - private void updateZOrder() { - int bubbleCount = getBubbleCount(); - for (int i = 0; i < bubbleCount; i++) { - BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); - bv.setZ(i < NUM_VISIBLE_WHEN_RESTING - ? (mPositioner.getMaxBubbles() * mBubbleElevation) - i - : 0f); - } - } - private void updateBadges(boolean setBadgeForCollapsedStack) { int bubbleCount = getBubbleCount(); for (int i = 0; i < bubbleCount; i++) { @@ -3414,8 +3459,9 @@ public class BubbleStackView extends FrameLayout * @return the number of bubbles in the stack view. */ public int getBubbleCount() { - // Subtract 1 for the overflow button that is always in the bubble container. - return mBubbleContainer.getChildCount() - 1; + final int childCount = mBubbleContainer.getChildCount(); + // Subtract 1 for the overflow button if it's showing. + return mShowingOverflow ? childCount - 1 : childCount; } /** 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..1d053f9aab35 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,9 @@ 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.BubbleBarLocation; 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; @@ -296,6 +297,15 @@ public interface Bubbles { boolean sensitiveNotificationProtectionActive); /** + * Determines whether Bubbles can show notifications. + * + * <p>Normally bubble notifications are shown by Bubbles, but in some cases the bubble + * notification is suppressed and should be shown by the Notifications pipeline as regular + * notifications. + */ + boolean canShowBubbleNotification(); + + /** * A listener to be notified of bubble state changes, used by launcher to render bubbles in * its process. */ @@ -304,6 +314,12 @@ public interface Bubbles { * Called when the bubbles state changes. */ void onBubbleStateChange(BubbleBarUpdate update); + + /** + * Called when bubble bar should temporarily be animated to a new location. + * Does not result in a state change. + */ + void animateBubbleBarLocation(BubbleBarLocation location); } /** Listener to find out about stack expansion / collapse events. */ 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..1eff149f2e91 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 @@ -32,14 +33,19 @@ interface IBubbles { oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3; - oneway void removeBubble(in String key) = 4; + oneway void dragBubbleToDismiss(in String key) = 4; oneway void removeAllBubbles() = 5; oneway void collapseBubbles() = 6; - oneway void onBubbleDrag(in String key, in boolean isBeingDragged) = 7; + oneway void startBubbleDrag(in String key) = 7; oneway void showUserEducation(in int positionX, in int positionY) = 8; + oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; + + oneway void setBubbleBarBounds(in Rect bubbleBarBounds) = 10; + + oneway void stopBubbleDrag(in BubbleBarLocation location) = 11; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index e48f8d5f1c84..14d29cd887bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -15,8 +15,9 @@ */ package com.android.wm.shell.bubbles; -import android.os.Bundle; +import android.os.Bundle; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; /** * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. */ @@ -26,4 +27,10 @@ oneway interface IBubblesListener { * Called when the bubbles state changes. */ void onBubbleStateChange(in Bundle update); + + /** + * Called when bubble bar should temporarily be animated to a new location. + * Does not result in a state change. + */ + void animateBubbleBarLocation(in BubbleBarLocation location); }
\ 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..f925eaef2c77 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; @@ -356,7 +356,6 @@ public class ExpandedAnimationController MagnetizedObject.MagnetListener listener) { mLayout.cancelAnimationsOnView(bubble); - bubble.setTranslationZ(Short.MAX_VALUE); mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>( mLayout.getContext(), bubble, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { @@ -460,6 +459,7 @@ public class ExpandedAnimationController /** * Snaps a bubble back to its position within the bubble row, and animates the rest of the * bubbles to accommodate it if it was previously dragged out past the threshold. + * Only happens while the stack is expanded. */ public void snapBubbleBack(View bubbleView, float velX, float velY) { if (mLayout == null) { @@ -467,10 +467,14 @@ public class ExpandedAnimationController } final int index = mLayout.indexOfChild(bubbleView); final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); + // overflow is not draggable so it's never the overflow + final float zTranslation = mPositioner.getZTranslation(index, + false /* isOverflow */, + true /* isExpanded */); animationForChildAtIndex(index) - .position(p.x, p.y) + .position(p.x, p.y, zTranslation) .withPositionStartVelocities(velX, velY) - .start(() -> bubbleView.setTranslationZ(0f) /* after */); + .start(); mMagnetizedBubbleDraggingOut = null; @@ -509,6 +513,7 @@ public class ExpandedAnimationController return Sets.newHashSet( DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.TRANSLATION_Z, DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y, DynamicAnimation.ALPHA); @@ -614,6 +619,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..06305f02e41c 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. @@ -417,7 +419,8 @@ public class PhysicsAnimationLayout extends FrameLayout { // be animating in this case, even if the physics animations haven't been started yet. final boolean isTranslation = property.equals(DynamicAnimation.TRANSLATION_X) - || property.equals(DynamicAnimation.TRANSLATION_Y); + || property.equals(DynamicAnimation.TRANSLATION_Y) + || property.equals(DynamicAnimation.TRANSLATION_Z); if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { return true; } @@ -493,6 +496,8 @@ public class PhysicsAnimationLayout extends FrameLayout { return "TRANSLATION_X"; } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { return "TRANSLATION_Y"; + } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) { + return "TRANSLATION_Z"; } else if (property.equals(DynamicAnimation.SCALE_X)) { return "SCALE_X"; } else if (property.equals(DynamicAnimation.SCALE_Y)) { @@ -596,6 +601,8 @@ public class PhysicsAnimationLayout extends FrameLayout { return R.id.translation_x_dynamicanimation_tag; } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { return R.id.translation_y_dynamicanimation_tag; + } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) { + return R.id.translation_z_dynamicanimation_tag; } else if (property.equals(DynamicAnimation.SCALE_X)) { return R.id.scale_x_dynamicanimation_tag; } else if (property.equals(DynamicAnimation.SCALE_Y)) { @@ -761,6 +768,12 @@ public class PhysicsAnimationLayout extends FrameLayout { return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); } + /** Animate the view's translationZ value to the provided value. */ + public PhysicsPropertyAnimator translationZ(float translationZ, Runnable... endActions) { + mPathAnimator = null; // We aren't using the path anymore if we're translating. + return property(DynamicAnimation.TRANSLATION_Z, translationZ, endActions); + } + /** Set the view's translationX value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator translationX( float from, float to, Runnable... endActions) { @@ -783,13 +796,14 @@ public class PhysicsAnimationLayout extends FrameLayout { /** * Animate the view's translationX and translationY values, and call the end actions only - * once both TRANSLATION_X and TRANSLATION_Y animations have completed. + * once both TRANSLATION_X, TRANSLATION_Y and TRANSLATION_Z animations have completed. */ - public PhysicsPropertyAnimator position( - float translationX, float translationY, Runnable... endActions) { + public PhysicsPropertyAnimator position(float translationX, float translationY, + float translationZ, Runnable... endActions) { mPositionEndActions = endActions; translationX(translationX); - return translationY(translationY); + translationY(translationY); + return translationZ(translationZ); } /** @@ -843,10 +857,13 @@ public class PhysicsAnimationLayout extends FrameLayout { private void clearTranslationValues() { mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); + mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Z); mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); + mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Z); mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); + mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Z); } /** Animate the view's scaleX value to the provided value. */ @@ -937,15 +954,19 @@ public class PhysicsAnimationLayout extends FrameLayout { }, propertiesArray); } - // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X - // and TRANSLATION_Y animations ending, and call them once both have finished. + // If we used position-specific end actions, we'll need to listen for TRANSLATION_X + // TRANSLATION_Y and TRANSLATION_Z animations ending, and call them once both have + // finished. if (mPositionEndActions != null) { final SpringAnimation translationXAnim = getSpringAnimationFromView(DynamicAnimation.TRANSLATION_X, mView); final SpringAnimation translationYAnim = getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView); - final Runnable waitForBothXAndY = () -> { - if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) { + final SpringAnimation translationZAnim = + getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Z, mView); + final Runnable waitForXYZ = () -> { + if (!translationXAnim.isRunning() && !translationYAnim.isRunning() + && !translationZAnim.isRunning()) { if (mPositionEndActions != null) { for (Runnable callback : mPositionEndActions) { callback.run(); @@ -957,9 +978,11 @@ public class PhysicsAnimationLayout extends FrameLayout { }; mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X, - new Runnable[]{waitForBothXAndY}); + new Runnable[]{waitForXYZ}); mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y, - new Runnable[]{waitForBothXAndY}); + new Runnable[]{waitForXYZ}); + mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Z, + new Runnable[]{waitForXYZ}); } if (mPathAnimator != null) { @@ -970,9 +993,10 @@ public class PhysicsAnimationLayout extends FrameLayout { for (DynamicAnimation.ViewProperty property : properties) { // Don't start translation animations if we're using a path animator, the update // listeners added to that animator will take care of that. - if (mPathAnimator != null - && (property.equals(DynamicAnimation.TRANSLATION_X) - || property.equals(DynamicAnimation.TRANSLATION_Y))) { + boolean isTranslationProperty = property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y) + || property.equals(DynamicAnimation.TRANSLATION_Z); + if (mPathAnimator != null && isTranslationProperty) { return; } @@ -1004,6 +1028,7 @@ public class PhysicsAnimationLayout extends FrameLayout { if (mPathAnimator != null) { animatedProperties.add(DynamicAnimation.TRANSLATION_X); animatedProperties.add(DynamicAnimation.TRANSLATION_Y); + animatedProperties.add(DynamicAnimation.TRANSLATION_Z); } return animatedProperties; 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..a51ac633ad86 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,11 +129,14 @@ class BubbleBarExpandedViewDragController( } override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { + isStuckToDismiss = false finishDrag() } private fun finishDrag() { if (!isStuckToDismiss) { + pinController.onDragEnd() + dragListener.onReleased(inDismiss = false) animationHelper.animateToRestPosition() dismissView.hide() } @@ -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..123cc7e9d488 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,8 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; -import com.android.wm.shell.R; +import androidx.annotation.NonNull; + import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -42,6 +43,9 @@ 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.BaseBubblePinController; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.DismissView; import kotlin.Unit; @@ -68,6 +72,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 +117,21 @@ public class BubbleBarLayerView extends FrameLayout setUpDismissView(); + mBubbleExpandedViewPinController = new BubbleExpandedViewPinController( + context, this, mPositioner); + mBubbleExpandedViewPinController.setListener( + new BaseBubblePinController.LocationChangeListener() { + @Override + public void onChange(@NonNull BubbleBarLocation bubbleBarLocation) { + mBubbleController.animateBubbleBarLocation(bubbleBarLocation); + } + + @Override + public void onRelease(@NonNull BubbleBarLocation location) { + mBubbleController.setBubbleBarLocation(location); + } + }); + setOnClickListener(view -> hideMenuOrCollapse()); } @@ -155,12 +175,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 +221,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 +341,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 */ @@ -342,22 +356,17 @@ public class BubbleBarLayerView extends FrameLayout } /** Updates the expanded view size and position. */ - private void updateExpandedView() { - if (mExpandedView == null) return; + public void updateExpandedView() { + 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 +395,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..651bf022e07d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt @@ -0,0 +1,93 @@ +/* + * 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.Point +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +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({ positioner.availableRect.let { Point(it.width(), it.height()) } }) { + + private var dropTargetView: View? = null + private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } + + private val exclRectWidth: Float by lazy { + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_width) + } + + private val exclRectHeight: Float by lazy { + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_height) + } + + override fun getExclusionRectWidth(): Float { + return exclRectWidth + } + + override fun getExclusionRectHeight(): Float { + return exclRectHeight + } + + 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 + positioner.getBubbleBarExpandedViewBounds( + location.isOnLeft(view.isLayoutRtl), + false /* isOverflowExpanded */, + tempRect + ) + view.updateLayoutParams<FrameLayout.LayoutParams> { + width = tempRect.width() + height = tempRect.height() + } + view.x = tempRect.left.toFloat() + view.y = tempRect.top.toFloat() + } +} 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..da414cc9ae70 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, @@ -388,9 +389,6 @@ public class SystemWindows { public void dispatchDragEvent(DragEvent event) {} @Override - public void updatePointerIcon(float x, float y) {} - - @Override public void dispatchWindowShown() {} @Override @@ -408,5 +406,10 @@ public class SystemWindows { // ignore } } + + @Override + public void dumpWindow(ParcelFileDescriptor pfd) { + + } } } 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..e514f9d70599 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt @@ -0,0 +1,196 @@ +/* + * 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.Point +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 val screenSizeProvider: () -> Point) { + + 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 + screenCenterX = screenSizeProvider.invoke().x / 2 + dismissZone = getExclusionRect() + } + + /** 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 + listener?.onRelease(if (onLeft) LEFT else RIGHT) + } + + /** + * [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 width for exclusion rect where dismiss takes over drag */ + protected abstract fun getExclusionRectWidth(): Float + /** Get height for exclusion rect where dismiss takes over drag */ + protected abstract fun getExclusionRectHeight(): Float + + /** 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 getExclusionRect(): RectF { + val rect = RectF(0f, 0f, getExclusionRectWidth(), getExclusionRectHeight()) + // Center it around the bottom center of the screen + val screenBottom = screenSizeProvider.invoke().y + rect.offsetTo(screenCenterX - rect.width() / 2, screenBottom - rect.height()) + return rect + } + + 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 has been dragged to a new [BubbleBarLocation]. And the drag is still in + * progress. + * + * 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) {} + + /** + * Bubble bar has been released in the [BubbleBarLocation]. + * + * @param location final location of the bubble bar once drag is released + */ + fun onRelease(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..ec3c6013e544 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.common.bubbles; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; @@ -33,6 +34,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 +48,12 @@ public class BubbleBarUpdate implements Parcelable { public String suppressedBubbleKey; @Nullable public String unsupressedBubbleKey; + @Nullable + public BubbleBarLocation bubbleBarLocation; + @Nullable + public Point expandedViewDropTargetSize; + public boolean showOverflowChanged; + public boolean showOverflow; // This is only populated if bubbles have been removed. public List<RemovedBubble> removedBubbles = new ArrayList<>(); @@ -56,10 +64,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(); @@ -71,10 +86,16 @@ public class BubbleBarUpdate implements Parcelable { suppressedBubbleKey = parcel.readString(); unsupressedBubbleKey = parcel.readString(); removedBubbles = parcel.readParcelableList(new ArrayList<>(), - RemovedBubble.class.getClassLoader()); + RemovedBubble.class.getClassLoader(), RemovedBubble.class); parcel.readStringList(bubbleKeysInOrder); currentBubbleList = parcel.readParcelableList(new ArrayList<>(), - BubbleInfo.class.getClassLoader()); + BubbleInfo.class.getClassLoader(), BubbleInfo.class); + bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(), + BubbleBarLocation.class); + expandedViewDropTargetSize = parcel.readParcelable(Point.class.getClassLoader(), + Point.class); + showOverflowChanged = parcel.readBoolean(); + showOverflow = parcel.readBoolean(); } /** @@ -89,12 +110,17 @@ public class BubbleBarUpdate implements Parcelable { || !bubbleKeysInOrder.isEmpty() || suppressedBubbleKey != null || unsupressedBubbleKey != null - || !currentBubbleList.isEmpty(); + || !currentBubbleList.isEmpty() + || bubbleBarLocation != null + || showOverflowChanged; } + @NonNull @Override public String toString() { - return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + return "BubbleBarUpdate{" + + " initialState=" + initialState + + " expandedChanged=" + expandedChanged + " expanded=" + expanded + " selectedBubbleKey=" + selectedBubbleKey + " shouldShowEducation=" + shouldShowEducation @@ -105,6 +131,10 @@ public class BubbleBarUpdate implements Parcelable { + " removedBubbles=" + removedBubbles + " bubbles=" + bubbleKeysInOrder + " currentBubbleList=" + currentBubbleList + + " bubbleBarLocation=" + bubbleBarLocation + + " expandedViewDropTargetSize=" + expandedViewDropTargetSize + + " showOverflowChanged=" + showOverflowChanged + + " showOverflow=" + showOverflow + " }"; } @@ -115,6 +145,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,14 +157,28 @@ public class BubbleBarUpdate implements Parcelable { parcel.writeParcelableList(removedBubbles, flags); parcel.writeStringList(bubbleKeysInOrder); parcel.writeParcelableList(currentBubbleList, flags); + parcel.writeParcelable(bubbleBarLocation, flags); + parcel.writeParcelable(expandedViewDropTargetSize, flags); + parcel.writeBoolean(showOverflowChanged); + parcel.writeBoolean(showOverflow); + } + + /** + * Create update for initial set of values. + * <p> + * Used when bubble bar is newly created. + */ + public static BubbleBarUpdate createInitialState() { + return new BubbleBarUpdate(true); } @NonNull public static final Creator<BubbleBarUpdate> CREATOR = - new Creator<BubbleBarUpdate>() { + new Creator<>() { public BubbleBarUpdate createFromParcel(Parcel source) { return new BubbleBarUpdate(source); } + public BubbleBarUpdate[] newArray(int size) { return new BubbleBarUpdate[size]; } 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/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS new file mode 100644 index 000000000000..08c70314973e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS @@ -0,0 +1,6 @@ +# WM shell sub-module bubble owner +madym@google.com +atsjenk@google.com +liranb@google.com +sukeshram@google.com +mpodolian@google.com 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/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index b87c2f6ebad5..7ceaaea3962f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -125,6 +125,7 @@ public class PipBoundsState { private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); + private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -297,7 +298,12 @@ public class PipBoundsState { /** Set the PIP aspect ratio. */ public void setAspectRatio(float aspectRatio) { - mAspectRatio = aspectRatio; + if (Float.compare(mAspectRatio, aspectRatio) != 0) { + mAspectRatio = aspectRatio; + for (Consumer<Float> callback : mOnAspectRatioChangedCallbacks) { + callback.accept(mAspectRatio); + } + } } /** Get the PIP aspect ratio. */ @@ -527,6 +533,23 @@ public class PipBoundsState { mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback); } + /** Adds callback to listen on aspect ratio change. */ + public void addOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (!mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.add(onAspectRatioChangedCallback); + onAspectRatioChangedCallback.accept(mAspectRatio); + } + } + + /** Removes callback to listen on aspect ratio change. */ + public void removeOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.remove(onAspectRatioChangedCallback); + } + } + public LauncherState getLauncherState() { return mLauncherState; } 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/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt index 1e30d8feb132..579a7943829e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -16,11 +16,14 @@ package com.android.wm.shell.common.pip import android.app.ActivityTaskManager +import android.app.AppGlobals import android.app.RemoteAction import android.app.WindowConfiguration import android.content.ComponentName import android.content.Context +import android.content.pm.PackageManager import android.os.RemoteException +import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Pair @@ -135,7 +138,24 @@ object PipUtils { } } + private var isPip2ExperimentEnabled: Boolean? = null + + /** + * Returns true if PiP2 implementation should be used. Besides the trunk stable flag, + * system property can be used to override this read only flag during development. + * It's currently limited to phone form factor, i.e., not enabled on ARC / TV. + */ @JvmStatic - val isPip2ExperimentEnabled: Boolean - get() = Flags.enablePip2Implementation() + fun isPip2ExperimentEnabled(): Boolean { + if (isPip2ExperimentEnabled == null) { + val isArc = AppGlobals.getPackageManager().hasSystemFeature( + "org.chromium.arc", 0) + val isTv = AppGlobals.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_LEANBACK, 0) + isPip2ExperimentEnabled = SystemProperties.getBoolean( + "persist.wm_shell.pip2", false) || + (Flags.enablePip2Implementation() && !isArc && !isTv) + } + return isPip2ExperimentEnabled as Boolean + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 424e5fa23615..2234041b8c9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -185,7 +185,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { nextTarget = snapAlgorithm.getDismissStartTarget(); } if (nextTarget != null) { - mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget); + mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget); return true; } return super.performAccessibilityAction(host, action, args); @@ -345,9 +345,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mMoving = true; } if (mMoving) { - final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos; mLastDraggingPosition = position; - mSplitLayout.updateDivideBounds(position, true /* shouldUseParallaxEffect */); + mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */); } break; case MotionEvent.ACTION_UP: @@ -363,7 +363,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { final float velocity = isLeftRightSplit ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); - final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos; final DividerSnapAlgorithm.SnapTarget snapTarget = mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */); mSplitLayout.snapToTarget(position, snapTarget); @@ -472,12 +472,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mInteractive = interactive; mHideHandle = hideHandle; if (!mInteractive && mHideHandle && mMoving) { - final int position = mSplitLayout.getDividePosition(); - mSplitLayout.flingDividePosition( + final int position = mSplitLayout.getDividerPosition(); + mSplitLayout.flingDividerPosition( mLastDraggingPosition, position, mSplitLayout.FLING_RESIZE_DURATION, - () -> mSplitLayout.setDividePosition(position, true /* applyLayoutChange */)); + () -> mSplitLayout.setDividerPosition(position, true /* applyLayoutChange */)); mMoving = false; } releaseTouching(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 3de8004b57fc..de016d3ae400 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -18,6 +18,7 @@ package com.android.wm.shell.common.split; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; @@ -118,7 +119,7 @@ public class SplitDecorManager extends WindowlessWindowManager { } /** Inflates split decor surface on the root surface. */ - public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) { + public void inflate(Context context, SurfaceControl rootLeash) { if (mIconLeash != null && mViewHost != null) { return; } @@ -137,13 +138,12 @@ public class SplitDecorManager extends WindowlessWindowManager { final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); - lp.width = rootBounds.width(); - lp.height = rootBounds.height(); + lp.width = mIconSize; + lp.height = mIconSize; lp.token = new Binder(); lp.setTitle(TAG); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports - // TRUSTED_OVERLAY for windowless window without input channel. + lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; mViewHost.setView(rootLayout, lp); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index d261e2435b5f..8331654839c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -78,7 +78,7 @@ import java.util.function.Consumer; /** * Records and handles layout of splits. Helps to calculate proper bounds when configuration or - * divide position changes. + * divider position changes. */ public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { private static final String TAG = "SplitLayout"; @@ -278,7 +278,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); } - int getDividePosition() { + int getDividerPosition() { return mDividerPosition; } @@ -489,20 +489,20 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void setDividerAtBorder(boolean start) { final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position : mDividerSnapAlgorithm.getDismissEndTarget().position; - setDividePosition(pos, false /* applyLayoutChange */); + setDividerPosition(pos, false /* applyLayoutChange */); } /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. */ - void updateDivideBounds(int position, boolean shouldUseParallaxEffect) { + void updateDividerBounds(int position, boolean shouldUseParallaxEffect) { updateBounds(position); mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, mSurfaceEffectPolicy.mParallaxOffset.y, shouldUseParallaxEffect); } - void setDividePosition(int position, boolean applyLayoutChange) { + void setDividerPosition(int position, boolean applyLayoutChange) { mDividerPosition = position; updateBounds(mDividerPosition); if (applyLayoutChange) { @@ -511,14 +511,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** - * Updates divide position and split bounds base on the ratio within root bounds. Falls back + * Updates divider position and split bounds base on the ratio within root bounds. Falls back * to middle position if the provided SnapTarget is not supported. */ public void setDivideRatio(@PersistentSnapPosition int snapPosition) { final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget( snapPosition); - setDividePosition(snapTarget != null + setDividerPosition(snapTarget != null ? snapTarget.position : mDividerSnapAlgorithm.getMiddleTarget().position, false /* applyLayoutChange */); @@ -546,24 +546,24 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** - * Sets new divide position and updates bounds correspondingly. Notifies listener if the new + * Sets new divider position and updates bounds correspondingly. Notifies listener if the new * target indicates dismissing split. */ public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { switch (snapTarget.snapPosition) { case SNAP_TO_START_AND_DISMISS: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; case SNAP_TO_END_AND_DISMISS: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; default: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, - () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */)); + flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */)); break; } } @@ -615,19 +615,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void flingDividerToDismiss(boolean toEnd, int reason) { final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position : mDividerSnapAlgorithm.getDismissStartTarget().position; - flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION, + flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION, () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); } /** Fling divider from current position to center position. */ public void flingDividerToCenter() { final int pos = mDividerSnapAlgorithm.getMiddleTarget().position; - flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION, - () -> setDividePosition(pos, true /* applyLayoutChange */)); + flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION, + () -> setDividerPosition(pos, true /* applyLayoutChange */)); } @VisibleForTesting - void flingDividePosition(int from, int to, int duration, + void flingDividerPosition(int from, int to, int duration, @Nullable Runnable flingFinishedCallback) { if (from == to) { if (flingFinishedCallback != null) { @@ -647,7 +647,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .setDuration(duration); mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mDividerFlingAnimator.addUpdateListener( - animation -> updateDivideBounds( + animation -> updateDividerBounds( (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */) ); mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { 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 f989991ab004..713d04bce4e8 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/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 5359e9faec3d..bfac24b81d2f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -20,7 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java index a0986fa601f2..2b0bd3272ed2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -16,10 +16,10 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import android.annotation.IdRes; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.content.Context; import android.util.AttributeSet; import android.view.View; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 15c6cbc3f1c4..3ab1fad2b203 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -16,16 +16,16 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; import android.graphics.Rect; @@ -81,7 +81,12 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; - mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; + if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { + // Don't show the SCM button for freeform tasks + mHasSizeCompat &= !taskInfo.isFreeform(); + } + mCameraCompatControlState = + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; mOnRestartButtonClicked = onRestartButtonClicked; @@ -135,7 +140,12 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; - mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; + if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { + // Don't show the SCM button for freeform tasks + mHasSizeCompat &= !taskInfo.isFreeform(); + } + mCameraCompatControlState = + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { return false; 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..991fbafed296 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; @@ -75,7 +72,6 @@ import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; import com.android.wm.shell.desktopmode.DesktopMode; -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.displayareahelper.DisplayAreaHelper; @@ -91,6 +87,12 @@ 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.DesktopModeStatus; +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,7 @@ 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.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -326,7 +328,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()); } // @@ -673,6 +676,22 @@ public abstract class WMShellBaseModule { return new TaskViewTransitions(transitions); } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract MixedTransitionHandler optionalMixedTransitionHandler(); + + @WMSingleton + @Provides + static Optional<MixedTransitionHandler> provideMixedTransitionHandler( + @DynamicOverride Optional<MixedTransitionHandler> mixedTransitionHandler + ) { + if (mixedTransitionHandler.isPresent()) { + return mixedTransitionHandler; + } + return Optional.empty(); + } + // // Keyguard transitions (optional feature) // @@ -683,10 +702,12 @@ public abstract class WMShellBaseModule { ShellInit shellInit, ShellController shellController, Transitions transitions, + TaskStackListenerImpl taskStackListener, @ShellMainThread Handler mainHandler, @ShellMainThread ShellExecutor mainExecutor) { return new KeyguardTransitionHandler( - shellInit, shellController, transitions, mainHandler, mainExecutor); + shellInit, shellController, transitions, taskStackListener, mainHandler, + mainExecutor); } @WMSingleton @@ -846,8 +867,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); } // @@ -868,13 +891,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopTasksController> providesDesktopTasksController( + static Optional<DesktopTasksController> providesDesktopTasksController(Context context, @DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. return desktopTasksController.flatMap((lazy)-> { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } return Optional.empty(); @@ -887,13 +910,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository( + static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context, @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. return desktopModeTaskRepository.flatMap((lazy)-> { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } return Optional.empty(); @@ -930,6 +953,7 @@ public abstract class WMShellBaseModule { Optional<OneHandedController> oneHandedControllerOptional, Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, Optional<ActivityEmbeddingController> activityEmbeddingOptional, + Optional<MixedTransitionHandler> mixedTransitionHandler, Transitions transitions, StartingWindowController startingWindow, ProtoLogController protoLogController, 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..fb0a1ab3062e 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 @@ -29,6 +29,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -52,14 +53,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.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.DesktopTasksLimiter; +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 +76,10 @@ 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.DesktopModeStatus; +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; @@ -82,6 +87,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.HomeTransitionObserver; +import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -215,7 +221,7 @@ public abstract class WMShellModule { Transitions transitions, Optional<DesktopTasksController> desktopTasksController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, mainExecutor, @@ -239,6 +245,7 @@ public abstract class WMShellModule { mainChoreographer, taskOrganizer, displayController, + rootTaskDisplayAreaOrganizer, syncQueue, transitions); } @@ -271,8 +278,8 @@ public abstract class WMShellModule { ShellInit init = FreeformComponents.isFreeformEnabled(context) ? shellInit : null; - return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository, - windowDecorViewModel); + return new FreeformTaskListener(context, init, shellTaskOrganizer, + desktopModeTaskRepository, windowDecorViewModel); } @WMSingleton @@ -367,8 +374,9 @@ public abstract class WMShellModule { // @WMSingleton + @DynamicOverride @Provides - static DefaultMixedHandler provideDefaultMixedHandler( + static MixedTransitionHandler provideMixedTransitionHandler( ShellInit shellInit, Optional<SplitScreenController> splitScreenOptional, @Nullable PipTransitionController pipTransitionController, @@ -509,25 +517,45 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor - ) { + @ShellMainThread ShellExecutor mainExecutor, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, mainExecutor); + dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, + recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter); } @WMSingleton @Provides + static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter( + Context context, + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + ShellTaskOrganizer shellTaskOrganizer) { + if (!DesktopModeStatus.canEnterDesktopMode(context) + || !Flags.enableDesktopWindowingTaskLimit()) { + return Optional.empty(); + } + return Optional.of( + new DesktopTasksLimiter( + transitions, desktopModeTaskRepository, shellTaskOrganizer)); + } + + + @WMSingleton + @Provides static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler( Context context, Transitions transitions, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DragToDesktopTransitionHandler(context, transitions, rootTaskDisplayAreaOrganizer); } @@ -535,7 +563,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( - Transitions transitions) { + Transitions transitions, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new EnterDesktopTaskTransitionHandler(transitions); } @@ -562,6 +591,37 @@ public abstract class WMShellModule { return new DesktopModeTaskRepository(); } + @WMSingleton + @Provides + static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( + Context context, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + Transitions transitions, + ShellInit shellInit + ) { + return desktopModeTaskRepository.flatMap(repository -> + Optional.of(new DesktopTasksTransitionObserver( + context, repository, transitions, shellInit)) + ); + } + + @WMSingleton + @Provides + static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( + Context context, + ShellInit shellInit, + Transitions transitions, + DesktopModeEventLogger desktopModeEventLogger) { + return new DesktopModeLoggerTransitionObserver( + context, shellInit, transitions, desktopModeEventLogger); + } + + @WMSingleton + @Provides + static DesktopModeEventLogger provideDesktopModeEventLogger() { + return new DesktopModeEventLogger(); + } + // // Drag and drop // @@ -602,7 +662,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - DefaultMixedHandler defaultMixedHandler) { + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java index 795bc1a7113b..d2895b149b2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java @@ -16,9 +16,9 @@ package com.android.wm.shell.dagger.back; -import com.android.wm.shell.back.CrossActivityBackAnimation; import com.android.wm.shell.back.CrossTaskBackAnimation; -import com.android.wm.shell.back.CustomizeActivityAnimation; +import com.android.wm.shell.back.CustomCrossActivityBackAnimation; +import com.android.wm.shell.back.DefaultCrossActivityBackAnimation; import com.android.wm.shell.back.ShellBackAnimation; import com.android.wm.shell.back.ShellBackAnimationRegistry; @@ -47,7 +47,7 @@ public interface ShellBackAnimationModule { @Binds @ShellBackAnimation.CrossActivity ShellBackAnimation bindCrossActivityShellBackAnimation( - CrossActivityBackAnimation crossActivityBackAnimation); + DefaultCrossActivityBackAnimation defaultCrossActivityBackAnimation); /** Default cross task back animation */ @Binds @@ -59,5 +59,5 @@ public interface ShellBackAnimationModule { @Binds @ShellBackAnimation.CustomizeActivity ShellBackAnimation provideCustomizeActivityShellBackAnimation( - CustomizeActivityAnimation customizeActivityAnimation); + CustomCrossActivityBackAnimation customCrossActivityBackAnimation); } 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..01364d1de279 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,29 @@ 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.TaskStackListenerImpl; 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.pip2.phone.PipTransitionState; +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,9 +70,12 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, - @NonNull PipScheduler pipScheduler) { + PipTouchHandler pipTouchHandler, + @NonNull PipScheduler pipScheduler, + @NonNull PipTransitionState pipStackListenerController) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); + pipBoundsState, null, pipBoundsAlgorithm, pipScheduler, + pipStackListenerController); } @WMSingleton @@ -78,6 +89,9 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -85,7 +99,7 @@ public abstract class Pip2Module { return Optional.ofNullable(PipController.create( context, shellInit, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, - mainExecutor)); + taskStackListener, shellTaskOrganizer, pipTransitionState, mainExecutor)); } } @@ -93,8 +107,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, + PipTransitionState pipTransitionState) { + return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState); } @WMSingleton @@ -108,4 +123,47 @@ 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 PipTransitionState pipTransitionState, + @NonNull PipScheduler pipScheduler, + @NonNull SizeSpecSource sizeSpecSource, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, + pipBoundsState, pipTransitionState, pipScheduler, sizeSpecSource, pipMotionHelper, + floatingContentCoordinator, pipUiEventLogger, mainExecutor, + pipPerfHintControllerOptional); + } + + @WMSingleton + @Provides + static PipMotionHelper providePipMotionHelper(Context context, + PipBoundsState pipBoundsState, PhonePipMenuController menuController, + PipSnapAlgorithm pipSnapAlgorithm, + FloatingContentCoordinator floatingContentCoordinator, + PipScheduler pipScheduler, + Optional<PipPerfHintController> pipPerfHintControllerOptional, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTransitionState pipTransitionState) { + return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, + floatingContentCoordinator, pipScheduler, pipPerfHintControllerOptional, + pipBoundsAlgorithm, pipTransitionState); + } + + @WMSingleton + @Provides + static PipTransitionState providePipStackListenerController() { + return new PipTransitionState(); + } } 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..0b7a3e838e88 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -0,0 +1,353 @@ +/* + * 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.content.Context +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.DesktopModeStatus +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( + context: Context, + 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.canEnterDesktopMode(context)) { + 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/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 7c8fcbb16711..7e0234ef8546 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 @@ -44,9 +47,11 @@ class DesktopModeTaskRepository { */ val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), - var stashed: Boolean = false + val minimizedTasks: ArraySet<Int> = ArraySet(), ) + // 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 @@ -87,10 +94,8 @@ class DesktopModeTaskRepository { visibleTasksListeners[visibleTasksListener] = executor displayData.keyIterator().forEach { displayId -> val visibleTasksCount = getVisibleTaskCount(displayId) - val stashed = isStashed(displayId) executor.execute { visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) - visibleTasksListener.onStashedChanged(displayId, stashed) } } } @@ -195,6 +200,22 @@ class DesktopModeTaskRepository { } } + /** Return whether the given Task is minimized. */ + fun isMinimizedTask(taskId: Int): Boolean { + return displayData.valueIterator().asSequence().any { data -> + data.minimizedTasks.contains(taskId) + } + } + + /** + * 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] */ @@ -203,6 +224,25 @@ class DesktopModeTaskRepository { } /** + * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks + * are visible. + */ + fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 + + /** + * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, + * ordered from front to back. + */ + fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> { + val activeTasks = getActiveTasks(displayId) + val allTasksInZOrder = getFreeformTasksInZOrder() + return activeTasks + // Don't show already minimized Tasks + .filter { taskId -> !isMinimizedTask(taskId) } + .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) } + } + + /** * Get a list of freeform tasks, ordered from top-bottom (top at index 0). */ // TODO(b/278084491): pass in display id @@ -226,16 +266,26 @@ 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) if (visible) { displayData.getOrCreate(displayId).visibleTasks.add(taskId) + unminimizeTask(displayId, taskId) } else { displayData[displayId]?.visibleTasks?.remove(taskId) } val newCount = getVisibleTaskCount(displayId) + // Check if count changed if (prevCount != newCount) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, @@ -244,10 +294,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", @@ -291,6 +337,24 @@ class DesktopModeTaskRepository { freeformTasksInZOrder.add(0, taskId) } + /** Mark a Task as minimized. */ + fun minimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: minimize Task: display=%d, task=%d", + displayId, taskId) + displayData.getOrCreate(displayId).minimizedTasks.add(taskId) + } + + /** Mark a Task as non-minimized. */ + fun unminimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d", + displayId, taskId) + displayData[displayId]?.minimizedTasks?.remove(taskId) + } + /** * Remove the task from the ordered list. */ @@ -301,9 +365,10 @@ class DesktopModeTaskRepository { taskId ) freeformTasksInZOrder.remove(taskId) + boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() + "DesktopTaskRepo: remaining freeform tasks: %s", freeformTasksInZOrder.toDumpString(), ) } @@ -332,30 +397,17 @@ class DesktopModeTaskRepository { } /** - * Update stashed status on display with id [displayId] + * Removes and returns the bounds saved before maximizing the given task. */ - fun setStashed(displayId: Int, stashed: Boolean) { - val data = displayData.getOrCreate(displayId) - val oldValue = data.stashed - data.stashed = stashed - if (oldValue != stashed) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: mark stashed=%b displayId=%d", - stashed, - displayId - ) - visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onStashedChanged(displayId, stashed) } - } - } + fun removeBoundsBeforeMaximize(taskId: Int): Rect? { + return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) } /** - * Check if display with id [displayId] has desktop tasks stashed + * Saves the bounds of the given task before maximizing. */ - fun isStashed(displayId: Int): Boolean { - return displayData[displayId]?.stashed ?: false + fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) { + boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) } internal fun dump(pw: PrintWriter, prefix: String) { @@ -373,7 +425,6 @@ class DesktopModeTaskRepository { pw.println("${prefix}Display $displayId:") pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}") pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}") - pw.println("${innerPrefix}stashed=${data.stashed}") } } @@ -395,11 +446,6 @@ class DesktopModeTaskRepository { * Called when the desktop changes the number of visible freeform tasks. */ fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {} - - /** - * Called when the desktop stashed status changes. - */ - fun onStashedChanged(displayId: Int, stashed: Boolean) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt new file mode 100644 index 000000000000..aa11a7d8a663 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt @@ -0,0 +1,100 @@ +/* + * 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.util.Log +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.wm.shell.dagger.WMSingleton +import javax.inject.Inject + +/** + * Log Aster UIEvents for desktop windowing mode. + */ +@WMSingleton +class DesktopModeUiEventLogger @Inject constructor( + private val mUiEventLogger: UiEventLogger, + private val mInstanceIdSequence: InstanceIdSequence +) { + /** + * Logs an event for a CUI, on a particular package. + * + * @param uid The user id associated with the package the user is interacting with + * @param packageName The name of the package the user is interacting with + * @param event The event type to generate + */ + fun log(uid: Int, packageName: String, event: DesktopUiEventEnum) { + if (packageName.isEmpty() || uid < 0) { + Log.d(TAG, "Skip logging since package name is empty or bad uid") + return + } + mUiEventLogger.log(event, uid, packageName) + } + + /** + * Retrieves a new instance id for a new interaction. + */ + fun getNewInstanceId(): InstanceId = mInstanceIdSequence.newInstanceId() + + /** + * Logs an event as part of a particular CUI, on a particular package. + * + * @param instanceId The id identifying an interaction, potentially taking place across multiple + * surfaces. There should be a new id generated for each distinct CUI. + * @param uid The user id associated with the package the user is interacting with + * @param packageName The name of the package the user is interacting with + * @param event The event type to generate + */ + fun logWithInstanceId( + instanceId: InstanceId, + uid: Int, + packageName: String, + event: DesktopUiEventEnum + ) { + if (packageName.isEmpty() || uid < 0) { + Log.d(TAG, "Skip logging since package name is empty or bad uid") + return + } + mUiEventLogger.logWithInstanceId(event, uid, packageName, instanceId) + } + + companion object { + /** + * Enums for logging desktop windowing mode UiEvents. + */ + enum class DesktopUiEventEnum(private val mId: Int) : UiEventLogger.UiEventEnum { + + @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the edge") + DESKTOP_WINDOW_EDGE_DRAG_RESIZE(1721), + + @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the corner") + DESKTOP_WINDOW_CORNER_DRAG_RESIZE(1722), + + @UiEvent(doc = "Tap on the window header maximize button in desktop windowing mode") + DESKTOP_WINDOW_MAXIMIZE_BUTTON_TAP(1723), + + @UiEvent(doc = "Double tap on window header to maximize it in desktop windowing mode") + DESKTOP_WINDOW_HEADER_DOUBLE_TAP_TO_MAXIMIZE(1724); + + override fun getId(): Int = mId + } + + private const val TAG = "DesktopModeUiEventLogger" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt new file mode 100644 index 000000000000..6da37419737b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -0,0 +1,173 @@ +/* + * 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("DesktopModeUtils") + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.content.pm.ActivityInfo.isFixedOrientationLandscape +import android.content.pm.ActivityInfo.isFixedOrientationPortrait +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.graphics.Rect +import android.os.SystemProperties +import android.util.Size +import com.android.wm.shell.common.DisplayLayout + + +val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties + .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f + +val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int = SystemProperties + .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25) + + +/** + * Calculates the initial bounds required for an application to fill a scale of the display bounds + * without any letterboxing. This is done by taking into account the applications fullscreen size, + * aspect ratio, orientation and resizability to calculate an area this is compatible with the + * applications previous configuration. + */ +fun calculateInitialBounds( + displayLayout: DisplayLayout, + taskInfo: RunningTaskInfo, + scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE +): Rect { + val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height()) + val appAspectRatio = calculateAspectRatio(taskInfo) + val idealSize = calculateIdealSize(screenBounds, scale) + // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated. + // Instead default to the desired initial bounds. + val topActivityInfo = taskInfo.topActivityInfo + ?: return positionInScreen(idealSize, screenBounds) + + val initialSize: Size = when (taskInfo.configuration.orientation) { + ORIENTATION_LANDSCAPE -> { + if (taskInfo.isResizeable) { + if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) { + // Respect apps fullscreen width + Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height) + } else { + idealSize + } + } else { + maximumSizeMaintainingAspectRatio(taskInfo, idealSize, + appAspectRatio) + } + } + ORIENTATION_PORTRAIT -> { + val customPortraitWidthForLandscapeApp = screenBounds.width() - + (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2) + if (taskInfo.isResizeable) { + if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { + // Respect apps fullscreen height and apply custom app width + Size(customPortraitWidthForLandscapeApp, + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight) + } else { + idealSize + } + } else { + if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { + // Apply custom app width and calculate maximum size + maximumSizeMaintainingAspectRatio( + taskInfo, + Size(customPortraitWidthForLandscapeApp, idealSize.height), + appAspectRatio) + } else { + maximumSizeMaintainingAspectRatio(taskInfo, idealSize, + appAspectRatio) + } + } + } + else -> { + idealSize + } + } + + return positionInScreen(initialSize, screenBounds) +} + +/** + * Calculates the largest size that can fit in a given area while maintaining a specific aspect + * ratio. + */ +private fun maximumSizeMaintainingAspectRatio( + taskInfo: RunningTaskInfo, + targetArea: Size, + aspectRatio: Float +): Size { + val targetHeight = targetArea.height + val targetWidth = targetArea.width + val finalHeight: Int + val finalWidth: Int + if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) { + val tempWidth = (targetHeight / aspectRatio).toInt() + if (tempWidth <= targetWidth) { + finalHeight = targetHeight + finalWidth = tempWidth + } else { + finalWidth = targetWidth + finalHeight = (finalWidth * aspectRatio).toInt() + } + } else { + val tempWidth = (targetHeight * aspectRatio).toInt() + if (tempWidth <= targetWidth) { + finalHeight = targetHeight + finalWidth = tempWidth + } else { + finalWidth = targetWidth + finalHeight = (finalWidth / aspectRatio).toInt() + } + } + return Size(finalWidth, finalHeight) +} + +/** + * Calculates the aspect ratio of an activity from its fullscreen bounds. + */ +private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { + if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { + val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth + val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight + return maxOf(appLetterboxWidth, appLetterboxHeight) / + minOf(appLetterboxWidth, appLetterboxHeight).toFloat() + } + val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f + return maxOf(appBounds.height(), appBounds.width()) / + minOf(appBounds.height(), appBounds.width()).toFloat() +} + +/** + * Calculates the desired initial bounds for applications in desktop windowing. This is done as a + * scale of the screen bounds. + */ +private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size { + val width = (screenBounds.width() * scale).toInt() + val height = (screenBounds.height() * scale).toInt() + return Size(width, height) +} + +/** + * Adjusts bounds to be positioned in the middle of the screen. + */ +private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect { + // TODO(b/325240051): Position apps with bottom heavy offset + val heightOffset = (screenBounds.height() - desiredSize.height) / 2 + val widthOffset = (screenBounds.width() - desiredSize.width) / 2 + return Rect(widthOffset, heightOffset, + desiredSize.width + widthOffset, desiredSize.height + heightOffset) +} 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..2dc4573b4921 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,13 +40,16 @@ 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 import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread +import com.android.internal.annotations.VisibleForTesting 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 +63,18 @@ 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.DesktopModeStatus +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,9 +84,12 @@ 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.isFullscreen import java.io.PrintWriter +import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer @@ -101,10 +111,12 @@ 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, - @ShellMainThread private val mainExecutor: ShellExecutor + @ShellMainThread private val mainExecutor: ShellExecutor, + private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, DragAndDropController.DragAndDropListener { @@ -112,7 +124,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 +151,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 @@ -148,12 +159,16 @@ class DesktopTasksController( com.android.wm.shell.R.dimen.desktop_mode_transition_area_width ) + /** Task id of the task currently being dragged from fullscreen/split. */ + val draggingTaskId + get() = dragToDesktopTransitionHandler.draggingTaskId + private var recentsAnimationRunning = false private lateinit var splitScreenController: SplitScreenController init { desktopMode = DesktopModeImpl() - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback({ onInit() }, this) } } @@ -161,8 +176,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() }, @@ -186,6 +204,11 @@ class DesktopTasksController( dragAndDropController.addListener(this) } + @VisibleForTesting + fun getVisualIndicator(): DesktopModeVisualIndicator? { + return visualIndicator + } + fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) @@ -205,7 +228,7 @@ class DesktopTasksController( bringDesktopAppsToFront(displayId, wct) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - // TODO(b/255649902): ensure remote transition is supplied once state is introduced + // TODO(b/309014605): ensure remote transition is supplied once state is introduced val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT val handler = remoteTransition?.let { OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) @@ -218,41 +241,13 @@ class DesktopTasksController( } } - /** - * Stash desktop tasks on display with id [displayId]. - * - * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps - * launched in this state will be added to the desktop. Existing desktop tasks will be brought - * back to front during the launch. - */ - fun stashDesktopApps(displayId: Int) { - if (DesktopModeStatus.isStashingEnabled()) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps") - desktopModeTaskRepository.setStashed(displayId, true) - } - } - - /** - * Clear the stashed state for the given display - */ - fun hideStashedDesktopApps(displayId: Int) { - if (DesktopModeStatus.isStashingEnabled()) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: hideStashedApps displayId=%d", - displayId - ) - desktopModeTaskRepository.setStashed(displayId, false) - } - } - /** Get number of tasks that are marked as visible */ fun getVisibleTaskCount(displayId: Int): Int { return desktopModeTaskRepository.getVisibleTaskCount(displayId) } /** 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 +261,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 +302,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", @@ -312,11 +325,13 @@ class DesktopTasksController( ) exitSplitIfApplicable(wct, task) // Bring other apps to front first - bringDesktopAppsToFront(task.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) addMoveToDesktopChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.moveToDesktop(wct) + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -354,10 +369,13 @@ class DesktopTasksController( val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) moveHomeTaskToFront(wct) - bringDesktopAppsToFront(taskInfo.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask( + taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) wct.setBounds(taskInfo.token, freeformBounds) - dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + transition?.let { addPendingMinimizeTransition(it, taskToMinimize) } } /** @@ -370,9 +388,26 @@ class DesktopTasksController( fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { val wct = WindowContainerTransaction() wct.setBounds(taskInfo.token, Rect()) + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED) 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 +417,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. */ @@ -401,7 +430,9 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() wct.setBounds(task.token, Rect()) - addMoveToSplitChanges(wct, task) + // Rather than set windowing mode to multi-window at task level, set it to + // undefined and inherit from split stage. + wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) } else { @@ -412,10 +443,12 @@ class DesktopTasksController( private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) { splitScreenController.prepareExitSplitScreen( - wct, - splitScreenController.getStageOfTask(taskInfo.taskId), - EXIT_REASON_DESKTOP_MODE + wct, + splitScreenController.getStageOfTask(taskInfo.taskId), + EXIT_REASON_DESKTOP_MODE ) + splitScreenController.transitionHandler + ?.onSplitToDesktop() } } @@ -443,7 +476,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() @@ -465,8 +502,10 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -489,8 +528,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 +555,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 +582,11 @@ class DesktopTasksController( } } - /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */ + /** + * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the + * stable bounds) and a free floating state (either the last saved bounds if available or the + * default bounds otherwise). + */ fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return @@ -543,11 +594,25 @@ 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 { + if (Flags.enableWindowingDynamicInitialBounds()){ + destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo)) + } 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 +630,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()) } /** @@ -624,19 +690,44 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } - private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) { - 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) + private fun bringDesktopAppsToFrontBeforeShowingNewTask( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int + ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) + + private fun bringDesktopAppsToFront( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int? = null + ): RunningTaskInfo? { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", + newTaskIdInFront ?: "null") + + if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Add translucent wallpaper activity to show the wallpaper underneath + addWallpaperActivity(wct) + } else { + // Move home to front + moveHomeTaskToFront(wct) + } - val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() - activeTasks - // Sort descending as the top task is at index 0. It should be ordered to top last - .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } - .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } - .forEach { task -> wct.reorder(task.token, true /* onTop */) } + val nonMinimizedTasksOrderedFrontToBack = + desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId) + // If we're adding a new Task we might need to minimize an old one + val taskToMinimize: RunningTaskInfo? = + if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { + desktopTasksLimiter.get().getTaskToMinimizeIfNeeded( + nonMinimizedTasksOrderedFrontToBack, newTaskIdInFront) + } else { null } + nonMinimizedTasksOrderedFrontToBack + // If there is a Task to minimize, let it stay behind the Home Task + .filter { taskId -> taskId != taskToMinimize?.taskId } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .reversed() // Start from the back so the front task is brought forward last + .forEach { task -> wct.reorder(task.token, true /* onTop */) } + return taskToMinimize } private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { @@ -646,7 +737,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 +804,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 +843,13 @@ class DesktopTasksController( val result = triggerTask?.let { task -> when { - // If display has tasks stashed, handle as stashed launch - desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) + request.type == TRANSIT_TO_BACK -> handleBackNavigation(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, transition) // Check if freeform task should be updated - task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) + task.isFreeform -> handleFreeformTaskLaunch(task, transition) else -> { null } @@ -767,10 +882,23 @@ class DesktopTasksController( .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } - private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun shouldLaunchAsModal(task: TaskInfo) = + 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, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch freeform task to fullscreen oon transition" + @@ -781,13 +909,23 @@ class DesktopTasksController( addMoveToFullscreenChanges(wct, task) } } + // Desktop Mode is showing and we're launching a new Task - we might need to minimize + // a Task. + val wct = WindowContainerTransaction() + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + if (taskToMinimize != null) { + addPendingMinimizeTransition(transition, taskToMinimize) + return wct + } return null } - private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleFullscreenTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch fullscreen task to freeform on transition" + @@ -796,35 +934,54 @@ class DesktopTasksController( ) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) + // Desktop Mode is already showing and we're launching a new Task - we might need to + // minimize another Task. + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + addPendingMinimizeTransition(transition, taskToMinimize) } } return null } - private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: launch apps with stashed on transition taskId=%d", - task.taskId - ) - val wct = WindowContainerTransaction() - bringDesktopAppsToFront(task.displayId, wct) - addMoveToDesktopChanges(wct, task) - desktopModeTaskRepository.setStashed(task.displayId, false) - 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 displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + 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 { WINDOWING_MODE_FREEFORM } + if (Flags.enableWindowingDynamicInitialBounds()) { + wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo)) + } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) if (isDesktopDensityOverrideSet()) { @@ -836,8 +993,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 { @@ -858,28 +1016,72 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - // Explicitly setting multi-window at task level interferes with animations. - // Let task inherit windowing mode once transition is complete instead. - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED) + // This windowing mode is to get the transition animation started; once we complete + // split select, we will change windowing mode to undefined and inherit from split stage. + // Going to undefined here causes task to flicker to the top left. + // Cancelling the split select flow will revert it to fullscreen. + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ + private fun addAndGetMinimizeChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newTaskInfo: RunningTaskInfo + ): RunningTaskInfo? { + if (!desktopTasksLimiter.isPresent) return null + return desktopTasksLimiter.get().addAndGetMinimizeTaskChangesIfNeeded( + displayId, wct, newTaskInfo) + } + + private fun addPendingMinimizeTransition( + transition: IBinder, + taskToMinimize: RunningTaskInfo? + ) { + if (taskToMinimize == null) return + desktopTasksLimiter.ifPresent { + it.addPendingMinimizeChange( + transition, taskToMinimize.displayId, taskToMinimize.taskId) + } + } + + /** 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 +1129,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 +1151,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 +1201,30 @@ 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 = getVisualIndicator() ?: return + val indicatorType = indicator + .updateIndicatorType(inputCoordinates, taskInfo.windowingMode) + when (indicatorType) { + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + if (Flags.enableWindowingDynamicInitialBounds()) { + finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo)) + } else { + 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 +1290,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 +1336,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 +1347,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. */ @@ -1133,16 +1375,6 @@ class DesktopTasksController( l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount) } } - - override fun onStashedChanged(displayId: Int, stashed: Boolean) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "IDesktopModeImpl: onStashedChanged display=%d stashed=%b", - displayId, - stashed - ) - remoteListener.call { l -> l.onStashedChanged(displayId, stashed) } - } } init { @@ -1174,25 +1406,25 @@ class DesktopTasksController( ) { c -> c.showDesktopApps(displayId, remoteTransition) } } - override fun stashDesktopApps(displayId: Int) { + override fun showDesktopApp(taskId: Int) { ExecutorUtils.executeRemoteCallWithTaskPermission( controller, - "stashDesktopApps" - ) { c -> c.stashDesktopApps(displayId) } + "showDesktopApp" + ) { c -> c.moveTaskToFront(taskId) } } - override fun hideStashedDesktopApps(displayId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "hideStashedDesktopApps" - ) { c -> c.hideStashedDesktopApps(displayId) } + override fun stashDesktopApps(displayId: Int) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: stashDesktopApps is deprecated" + ) } - override fun showDesktopApp(taskId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "showDesktopApp" - ) { c -> c.moveTaskToFront(taskId) } + override fun hideStashedDesktopApps(displayId: Int) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: hideStashedDesktopApps is deprecated" + ) } override fun getVisibleTaskCount(displayId: Int): Int { @@ -1224,6 +1456,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 { @@ -1233,7 +1472,7 @@ class DesktopTasksController( @JvmField val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties - .getInt("persist.wm.debug.freeform_initial_bounds_scale", 75) / 100f + .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f /** * Check if desktop density override is enabled diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt new file mode 100644 index 000000000000..0f88384ec2ac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -0,0 +1,218 @@ +/* + * 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.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.annotation.VisibleForTesting +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionObserver +import com.android.wm.shell.util.KtProtoLog + +/** + * Limits the number of tasks shown in Desktop Mode. + * + * This class should only be used if + * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true. + */ +class DesktopTasksLimiter ( + transitions: Transitions, + private val taskRepository: DesktopModeTaskRepository, + private val shellTaskOrganizer: ShellTaskOrganizer, +) { + private val minimizeTransitionObserver = MinimizeTransitionObserver() + + init { + transitions.registerObserver(minimizeTransitionObserver) + } + + private data class TaskDetails (val displayId: Int, val taskId: Int) + + // TODO(b/333018485): replace this observer when implementing the minimize-animation + private inner class MinimizeTransitionObserver : TransitionObserver { + private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + + fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { + mPendingTransitionTokensAndTasks[transition] = taskDetails + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return + + if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return + + if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: task %d is not reordered to back nor invis", + taskToMinimize.taskId) + return + } + this@DesktopTasksLimiter.markTaskMinimized( + taskToMinimize.displayId, taskToMinimize.taskId) + } + + /** + * Returns whether the given Task is being reordered to the back in the given transition, or + * is already invisible. + * + * <p> This check can be used to double-check that a task was indeed minimized before + * marking it as such. + */ + private fun isTaskReorderedToBackOrInvisible( + info: TransitionInfo, + taskDetails: TaskDetails + ): Boolean { + val taskChange = info.changes.find { change -> + change.taskInfo?.taskId == taskDetails.taskId } + if (taskChange == null) { + return !taskRepository.isVisibleTask(taskDetails.taskId) + } + return taskChange.mode == TRANSIT_TO_BACK + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + mPendingTransitionTokensAndTasks[playing] = taskToTransfer + } + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: transition %s finished", transition) + mPendingTransitionTokensAndTasks.remove(transition) + } + } + + /** + * Mark a task as minimized, this should only be done after the corresponding transition has + * finished so we don't minimize the task if the transition fails. + */ + private fun markTaskMinimized(displayId: Int, taskId: Int) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: marking %d as minimized", taskId) + taskRepository.minimizeTask(displayId, taskId) + } + + /** + * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task + * limit. + * + * @param transition the transition that the minimize-transition will be appended to, or null if + * the transition will be started later. + * @return the ID of the minimized task, or null if no task is being minimized. + */ + fun addAndGetMinimizeTaskChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newFrontTaskInfo: RunningTaskInfo, + ): RunningTaskInfo? { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", + newFrontTaskInfo.taskId) + val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( + taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId), + newFrontTaskInfo.taskId) + val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) + if (taskToMinimize != null) { + wct.reorder(taskToMinimize.token, false /* onTop */) + return taskToMinimize + } + return null + } + + /** + * Add a pending minimize transition change, to update the list of minimized apps once the + * transition goes through. + */ + fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) { + minimizeTransitionObserver.addPendingTransitionToken( + transition, TaskDetails(displayId, taskId)) + } + + /** + * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop + * Mode. + */ + fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit() + + /** + * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and + * 2. a new task placed in front of all the others. + */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int>, + newTaskIdInFront: Int + ): RunningTaskInfo? { + return getTaskToMinimizeIfNeeded( + createOrderedTaskListWithGivenTaskInFront( + visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront)) + } + + /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int> + ): RunningTaskInfo? { + if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: no need to minimize; tasks below limit") + // No need to minimize anything + return null + } + val taskToMinimize = + shellTaskOrganizer.getRunningTaskInfo( + visibleFreeformTaskIdsOrderedFrontToBack.last()) + if (taskToMinimize == null) { + KtProtoLog.e( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: taskToMinimize == null") + return null + } + return taskToMinimize + } + + private fun createOrderedTaskListWithGivenTaskInFront( + existingTaskIdsOrderedFrontToBack: List<Int>, + newTaskId: Int + ): List<Int> { + return listOf(newTaskId) + + existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId } + } + + @VisibleForTesting + fun getTransitionObserver(): TransitionObserver { + return minimizeTransitionObserver + } +}
\ No newline at end of file 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..dae75f90e3ae --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -0,0 +1,96 @@ +/* + * 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.content.Context +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.shared.DesktopModeStatus +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( + context: Context, + private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val transitions: Transitions, + shellInit: ShellInit +) : Transitions.TransitionObserver { + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS && + DesktopModeStatus.canEnterDesktopMode(context)) { + 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..e5e435da48b2 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 @@ -6,6 +6,7 @@ import android.animation.RectEvaluator import android.animation.ValueAnimator import android.app.ActivityOptions import android.app.ActivityOptions.SourceInfo +import android.app.ActivityTaskManager.INVALID_TASK_ID import android.app.PendingIntent import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT import android.app.PendingIntent.FLAG_MUTABLE @@ -15,6 +16,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 @@ -25,6 +27,9 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import com.android.wm.shell.RootTaskDisplayAreaOrganizer +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.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.splitscreen.SplitScreenController @@ -67,7 +72,7 @@ class DragToDesktopTransitionHandler( .addCategory(Intent.CATEGORY_HOME) private var dragToDesktopStateListener: DragToDesktopStateListener? = null - private var splitScreenController: SplitScreenController? = null + private lateinit var splitScreenController: SplitScreenController private var transitionState: TransitionState? = null private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener @@ -75,6 +80,9 @@ class DragToDesktopTransitionHandler( val inProgress: Boolean get() = transitionState != null + /** The task id of the task currently being dragged from fullscreen/split. */ + val draggingTaskId: Int + get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID /** Sets a listener to receive callback about events during the transition animation. */ fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) { dragToDesktopStateListener = listener @@ -124,15 +132,19 @@ 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) transitionState = if (isSplitTask(taskId)) { + val otherTask = getOtherSplitTask(taskId) ?: throw IllegalStateException( + "Expected split task to have a counterpart." + ) TransitionState.FromSplit( draggedTaskId = taskId, dragAnimator = dragToDesktopAnimator, - startTransitionToken = startTransitionToken + startTransitionToken = startTransitionToken, + otherSplitTask = otherTask ) } else { TransitionState.FromFullscreen( @@ -149,20 +161,20 @@ class DragToDesktopTransitionHandler( * windowing mode changes to the dragged task. This is called when the dragged task is released * inside the desktop drop zone. */ - fun finishDragToDesktopTransition(wct: WindowContainerTransaction) { + fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { // Don't attempt to finish a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully // started. - return + return null } if (requireTransitionState().startAborted) { // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. clearState() - return + return null } - transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) } /** @@ -346,6 +358,12 @@ class DragToDesktopTransitionHandler( ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { info.changes.withIndex().forEach { (i, change) -> + // If we're exiting split, hide the remaining split task. + if (state is TransitionState.FromSplit && + change.taskInfo?.taskId == state.otherSplitTask) { + t.hide(change.leash) + startTransactionFinishT.hide(change.leash) + } if (change.mode == TRANSIT_CLOSE) { t.hide(change.leash) startTransactionFinishT.hide(change.leash) @@ -368,67 +386,49 @@ 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 +492,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 @@ -567,7 +565,18 @@ class DragToDesktopTransitionHandler( } private fun isSplitTask(taskId: Int): Boolean { - return splitScreenController?.isTaskInSplitScreen(taskId) ?: false + return splitScreenController.isTaskInSplitScreen(taskId) + } + + private fun getOtherSplitTask(taskId: Int): Int? { + val splitPos = splitScreenController.getSplitPosition(taskId) + if (splitPos == SPLIT_POSITION_UNDEFINED) return null + val otherTaskPos = if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } private fun requireTransitionState(): TransitionState { @@ -616,6 +625,7 @@ class DragToDesktopTransitionHandler( override var cancelled: Boolean = false, override var startAborted: Boolean = false, var splitRootChange: Change? = null, + var otherSplitTask: Int ) : TransitionState() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 79bb5408df82..74b8f831cdc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -78,10 +78,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition /** * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP * @param wct WindowContainerTransaction for transition + * @return the token representing the started transition */ - public void moveToDesktop(@NonNull WindowContainerTransaction wct) { + public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) { final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); mPendingTransitionTokens.add(token); + return token; } @Override 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..c36f8deb6ecc 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 @@ -28,10 +28,10 @@ interface IDesktopMode { /** Show apps on the desktop on the given display */ void showDesktopApps(int displayId, in RemoteTransition remoteTransition); - /** Stash apps on the desktop to allow launching another app from home screen */ + /** @deprecated use {@link #showDesktopApps} instead. */ void stashDesktopApps(int displayId); - /** Hide apps that may be stashed */ + /** @deprecated this is no longer supported. */ void hideStashedDesktopApps(int displayId); /** Bring task with the given id to front */ @@ -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/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 8ed87f23bf40..8ebdfdcf4731 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -25,6 +25,6 @@ interface IDesktopTaskListener { /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */ oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount); - /** Desktop task stashed status has changed. */ + /** @deprecated this is no longer supported. */ oneway void onStashedChanged(int displayId, boolean stashed); }
\ 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/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index f2bdcae31956..e0e2e706d649 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -21,14 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM; import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; -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.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -44,6 +45,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ShellTaskOrganizer.FocusListener { private static final String TAG = "FreeformTaskListener"; + private final Context mContext; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final WindowDecorViewModel mWindowDecorationViewModel; @@ -56,10 +58,12 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, } public FreeformTaskListener( + Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, WindowDecorViewModel windowDecorationViewModel) { + mContext = context; mShellTaskOrganizer = shellTaskOrganizer; mWindowDecorationViewModel = windowDecorationViewModel; mDesktopModeTaskRepository = desktopModeTaskRepository; @@ -70,7 +74,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private void onInit() { mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mShellTaskOrganizer.addFocusListener(this); } } @@ -92,9 +96,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, t.apply(); } - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, @@ -113,9 +118,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mTasks.remove(taskInfo.taskId); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (repository.removeActiveTask(taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Removing active freeform task: #%d", taskInfo.taskId); @@ -123,7 +129,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false); }); } - + mWindowDecorationViewModel.onTaskVanished(taskInfo); if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mWindowDecorationViewModel.destroyWindowDecoration(taskInfo); } @@ -137,7 +143,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mWindowDecorationViewModel.onTaskInfoChanged(taskInfo); state.mTaskInfo = taskInfo; - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { @@ -159,9 +165,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Focus Changed: #%d focused=%b", taskInfo.taskId, taskInfo.isFocused); - if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) { + if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 998728d65e6a..2626e7380163 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -161,7 +161,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); - + mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo)); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; if (mWindowDecorViewModelOptional.isPresent()) { mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo); 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..c79eef7efb61 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 @@ -20,7 +20,9 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING; @@ -44,12 +46,15 @@ import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; 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.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; 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; @@ -62,7 +67,8 @@ import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; * <p>This takes the highest priority. */ public class KeyguardTransitionHandler - implements Transitions.TransitionHandler, KeyguardChangeListener { + implements Transitions.TransitionHandler, KeyguardChangeListener, + TaskStackListenerCallback { private static final String TAG = "KeyguardTransition"; private final Transitions mTransitions; @@ -71,12 +77,14 @@ public class KeyguardTransitionHandler private final ShellExecutor mMainExecutor; private final ArrayMap<IBinder, StartedTransition> mStartedTransitions = new ArrayMap<>(); + private final TaskStackListenerImpl mTaskStackListener; /** * Local IRemoteTransition implementations registered by the keyguard service. * @see KeyguardTransitions */ private IRemoteTransition mExitTransition = null; + private IRemoteTransition mAppearTransition = null; private IRemoteTransition mOccludeTransition = null; private IRemoteTransition mOccludeByDreamTransition = null; private IRemoteTransition mUnoccludeTransition = null; @@ -87,6 +95,8 @@ public class KeyguardTransitionHandler // Last value reported by {@link KeyguardChangeListener}. private boolean mKeyguardShowing = true; + @Nullable + private WindowContainerToken mDreamToken; private final class StartedTransition { final TransitionInfo mInfo; @@ -105,18 +115,23 @@ public class KeyguardTransitionHandler @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull Transitions transitions, + @NonNull TaskStackListenerImpl taskStackListener, @NonNull Handler mainHandler, @NonNull ShellExecutor mainExecutor) { mTransitions = transitions; mShellController = shellController; mMainHandler = mainHandler; mMainExecutor = mainExecutor; + mTaskStackListener = taskStackListener; shellInit.addInitCallback(this::onInit, this); } private void onInit() { mTransitions.addHandler(this); mShellController.addKeyguardChangeListener(this); + if (dismissDreamOnKeyguardDismiss()) { + mTaskStackListener.addListener(this); + } } /** @@ -142,6 +157,11 @@ public class KeyguardTransitionHandler } @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + mDreamToken = taskInfo.getActivityType() == ACTIVITY_TYPE_DREAM ? taskInfo.token : null; + } + + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -152,26 +172,28 @@ public class KeyguardTransitionHandler // Choose a transition applicable for the changes and keyguard state. if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { - return startAnimation(mExitTransition, - "going-away", + return startAnimation(mExitTransition, "going-away", transition, info, startTransaction, finishTransaction, finishCallback); } + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) { + return startAnimation(mAppearTransition, "appearing", + transition, info, startTransaction, finishTransaction, finishCallback); + } + + // Occlude/unocclude animations are only played if the keyguard is locked. if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_OCCLUDING) != 0) { if (hasOpeningDream(info)) { - return startAnimation(mOccludeByDreamTransition, - "occlude-by-dream", + return startAnimation(mOccludeByDreamTransition, "occlude-by-dream", transition, info, startTransaction, finishTransaction, finishCallback); } else { - return startAnimation(mOccludeTransition, - "occlude", + return startAnimation(mOccludeTransition, "occlude", transition, info, startTransaction, finishTransaction, finishCallback); } } else if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { - return startAnimation(mUnoccludeTransition, - "unocclude", + return startAnimation(mUnoccludeTransition, "unocclude", transition, info, startTransaction, finishTransaction, finishCallback); } } @@ -271,6 +293,13 @@ public class KeyguardTransitionHandler @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { + if (dismissDreamOnKeyguardDismiss() + && (request.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0 + && mDreamToken != null) { + // Dismiss the dream in the same transaction, so that it isn't visible once the device + // is unlocked. + return new WindowContainerTransaction().removeTask(mDreamToken); + } return null; } @@ -334,11 +363,13 @@ public class KeyguardTransitionHandler @Override public void register( IRemoteTransition exitTransition, + IRemoteTransition appearTransition, IRemoteTransition occludeTransition, IRemoteTransition occludeByDreamTransition, IRemoteTransition unoccludeTransition) { mMainExecutor.execute(() -> { mExitTransition = exitTransition; + mAppearTransition = appearTransition; mOccludeTransition = occludeTransition; mOccludeByDreamTransition = occludeByDreamTransition; mUnoccludeTransition = unoccludeTransition; 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..b7245b91f36c 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 @@ -35,6 +35,7 @@ public interface KeyguardTransitions { */ default void register( @NonNull IRemoteTransition unlockTransition, + @NonNull IRemoteTransition appearTransition, @NonNull IRemoteTransition occludeTransition, @NonNull IRemoteTransition occludeByDreamTransition, @NonNull IRemoteTransition unoccludeTransition) {} 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/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS index ec09827fa4d1..afddfab99a2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS @@ -1,3 +1,2 @@ # WM shell sub-module pip owner hwwang@google.com -mateuszc@google.com 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..a749019046f8 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; @@ -39,7 +39,7 @@ public interface Pip { * @param isSysUiStateValid Is SysUI state valid or not. * @param flag Current SysUI state. */ - default void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { + default void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) { } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4c477373c32c..eb845db409e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -743,11 +743,6 @@ public class PipAnimationController { .alpha(tx, leash, 1f) .round(tx, leash, shouldApplyCornerRadius()) .shadow(tx, leash, shouldApplyShadowRadius()); - // TODO(b/178632364): this is a work around for the black background when - // entering PiP in button navigation mode. - if (isInPipDirection(direction)) { - tx.setWindowCrop(leash, getStartValue()); - } tx.show(leash); tx.apply(); } 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 a9013b9c4fd6..e1657f99639d 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,6 +91,7 @@ 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; @@ -597,6 +597,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } + if (mPipTransitionState.isEnteringPip() + && !mPipTransitionState.getInSwipePipToHomeTransition()) { + // If we are still entering PiP with Shell playing enter animation, jump-cut to + // the end of the enter animation and reschedule exitPip to run after enter-PiP + // has finished its transition and allowed the client to draw in PiP mode. + mPipTransitionController.end(() -> { + exitPip(animationDurationMs, requestEnterSplit); + }); + return; + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -843,7 +854,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipUiEventLoggerLogger.log(uiEventEnum); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "onTaskAppeared: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); + "onTaskAppeared: %s, state=%s, taskId=%s", mTaskInfo.topActivity, + mPipTransitionState, mTaskInfo.taskId); if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); @@ -1976,6 +1988,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 d60f5a631044..2082756feda7 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 @@ -43,7 +43,6 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; -import android.animation.Animator; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.TaskInfo; @@ -348,9 +347,16 @@ public class PipTransition extends PipTransitionController { @Override public void end() { - Animator animator = mPipAnimationController.getCurrentAnimator(); - if (animator != null && animator.isRunning()) { - animator.end(); + end(null); + } + + @Override + public void end(@Nullable Runnable onTransitionEnd) { + if (mPipAnimationController.isAnimating()) { + mPipAnimationController.getCurrentAnimator().end(); + } + if (onTransitionEnd != null) { + onTransitionEnd.run(); } } @@ -818,8 +824,11 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull TaskInfo taskInfo) { startTransaction.apply(); - finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), - mPipDisplayLayoutState.getDisplayBounds()); + final TransitionInfo.Change pipChange = findCurrentPipTaskChange(info); + if (pipChange == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "removePipImmediately is called without pip change"); + } mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(null); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 5584f238e131..7730285c86c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -54,7 +54,6 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; /** * Responsible supplying PiP Transitions. @@ -125,12 +124,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start resizing Pip transition/animation. - * - * @param onFinishResizeCallback callback guaranteed to execute when animation ends and - * client completes any potential draws upon WM state updates. */ - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { // Default implementation does nothing. } @@ -305,6 +300,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void end() { } + /** + * End the currently-playing PiP animation. + * + * @param onTransitionEnd callback to run upon finishing the playing transition. + */ + public void end(@Nullable Runnable onTransitionEnd) { + } + /** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */ public void startHighPerfSession() {} 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..85f9194ac804 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 @@ -843,7 +847,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } - private void onSystemUiStateChanged(boolean isValidState, int flag) { + private void onSystemUiStateChanged(boolean isValidState, long flag) { mTouchHandler.onSystemUiStateChanged(isValidState); } @@ -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); } @@ -1190,7 +1195,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { + public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) { mMainExecutor.execute(() -> { PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag); }); 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/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index c1adfffce074..d8ac8e948a97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -219,6 +219,7 @@ public class PipTouchHandler { mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), this::onAccessibilityShowMenu, this::updateMovementBounds, this::animateToUnStashedState, mainExecutor); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); // TODO(b/181599115): This should really be initializes as part of the pip controller, but // until all PIP implementations derive from the controller, just initialize the touch handler 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/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS index 6dabb3bf6f9a..79d1793819f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS @@ -1,4 +1,3 @@ # WM shell sub-module pip owner hwwang@google.com -mateuszc@google.com gabiyev@google.com 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..a12882f56eb7 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 @@ -16,29 +16,40 @@ package com.android.wm.shell.pip2.phone; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; import android.view.InsetsState; import android.view.SurfaceControl; import androidx.annotation.BinderThread; +import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.Preconditions; +import com.android.wm.shell.R; +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.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.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.IPip; import com.android.wm.shell.common.pip.IPipAnimationListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -54,18 +65,49 @@ import com.android.wm.shell.sysui.ShellInit; * Manages the picture-in-picture (PIP) UI and states for Phones. */ public class PipController implements ConfigurationChangeListener, + PipTransitionState.PipTransitionStateChangedListener, DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); + private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds"; + private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; + + 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 TaskStackListenerImpl mTaskStackListener; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final PipTransitionState mPipTransitionState; + 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); - 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; + /** + * Notifies the listener that user leaves PiP by tapping on the expand button. + */ + void onExpandPip(); + } private PipController(Context context, ShellInit shellInit, @@ -76,6 +118,9 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -85,6 +130,10 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipDisplayLayoutState = pipDisplayLayoutState; mPipScheduler = pipScheduler; + mTaskStackListener = taskStackListener; + mShellTaskOrganizer = shellTaskOrganizer; + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); mMainExecutor = mainExecutor; if (PipUtils.isPip2ExperimentEnabled()) { @@ -92,14 +141,31 @@ 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, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, + 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, taskStackListener, shellTaskOrganizer, pipTransitionState, + mainExecutor); } private void onInit() { @@ -109,7 +175,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 +188,61 @@ 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); - } + mShellController.addConfigurationChangeListener(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); + mTaskStackListener.addListener(new TaskStackListenerCallback() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { + return; + } + mPipScheduler.scheduleExitPipViaExpand(); + } + }); } 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 +263,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 +281,61 @@ 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. + Bundle extra = new Bundle(); + extra.putParcelable(SWIPE_TO_PIP_OVERLAY, overlay); + extra.putParcelable(SWIPE_TO_PIP_APP_BOUNDS, appBounds); + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP, extra); + 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(); + } + mPipRecentsAnimationListener.onPipAnimationStarted(); + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + if (newState == PipTransitionState.SWIPING_TO_PIP) { + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + SurfaceControl overlay = extra.getParcelable( + SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); + Rect appBounds = extra.getParcelable( + SWIPE_TO_PIP_APP_BOUNDS, Rect.class); + + Preconditions.checkState(appBounds != null, + "App bounds can't be null for " + mPipTransitionState); + mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); + } else if (newState == PipTransitionState.ENTERED_PIP) { + if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipTransitionState.resetSwipePipToHomeState(); + } + } + } + + // + // 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 +344,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 +375,7 @@ public class PipController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + mListener.unregister(); } @Override @@ -257,7 +416,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..b757b00f16dd --- /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 + */ + 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..be10151ca5aa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -0,0 +1,777 @@ +/* + * 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.Bundle; +import android.os.Debug; +import android.view.SurfaceControl; + +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.PipBoundsAlgorithm; +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, + PipTransitionState.PipTransitionStateChangedListener { + private static final String TAG = "PipMotionHelper"; + private static final String FLING_BOUNDS_CHANGE = "fling_bounds_change"; + 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 @NonNull PipBoundsAlgorithm mPipBoundsAlgorithm; + private @NonNull PipScheduler mPipScheduler; + private @NonNull PipTransitionState mPipTransitionState; + 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; + + /** + * Set to true if bounds change transition has been scheduled from PipMotionHelper. + */ + private boolean mWaitingForBoundsChangeTransition = 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, PipScheduler pipScheduler, + Optional<PipPerfHintController> pipPerfHintControllerOptional, + PipBoundsAlgorithm pipBoundsAlgorithm, PipTransitionState pipTransitionState) { + mContext = context; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipScheduler = pipScheduler; + mMenuController = menuController; + mSnapAlgorithm = snapAlgorithm; + mFloatingContentCoordinator = floatingContentCoordinator; + mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mResizePipUpdateListener = (target, values) -> { + if (mPipBoundsState.getMotionBoundsState().isInMotion()) { + mPipScheduler.scheduleUserResizePip( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + } + }; + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); + } + + 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); + mPipScheduler.scheduleUserResizePip(toBounds); + } + } 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, + mPipBoundsAlgorithm.getMovementBounds(getBounds()).left, + mPipBoundsAlgorithm.getMovementBounds(getBounds()).right); + mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsAlgorithm.getMovementBounds(getBounds()).top, + mPipBoundsAlgorithm.getMovementBounds(getBounds()).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()) { + // do not schedule resize if PiP is dismissing, which may cause app re-open to + // mBounds instead of its normal bounds. + Bundle extra = new Bundle(); + extra.putBoolean(FLING_BOUNDS_CHANGE, true); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + return; + } + settlePipBoundsAfterPhysicsAnimation(true /* animatingAfter */); + 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())) { + mPipScheduler.scheduleAnimateResizePip(toBounds); + } + } + + /** + * 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); + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, + @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + if (!extra.getBoolean(FLING_BOUNDS_CHANGE)) break; + + // If touch is turned off and we are in a fling animation, schedule a transition. + mWaitingForBoundsChangeTransition = true; + mPipScheduler.scheduleAnimateResizePip( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + if (!mWaitingForBoundsChangeTransition) break; + + // If bounds change transition was scheduled from this class, handle leash updates. + mWaitingForBoundsChangeTransition = false; + SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); + startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, + destinationBounds.left, destinationBounds.top); + startTx.apply(); + + // All motion operations have actually finished, so make bounds cache updates. + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + cleanUpHighPerfSessionMaybe(); + + // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + break; + case PipTransitionState.EXITING_PIP: + // We need to force finish any local animators if about to leave PiP, to avoid + // breaking the state (e.g. leashes are cleaned up upon exit). + if (!mPipBoundsState.getMotionBoundsState().isInMotion()) break; + cancelPhysicsAnimation(); + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + } + } + + private void settlePipBoundsAfterPhysicsAnimation(boolean animatingAfter) { + if (!animatingAfter) { + // 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. Only set the final + // bounds state and clear motion bounds completely if the whole animation is over. + mPipBoundsState.setBounds(mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + } + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + mDismissalPending = false; + } + + /** + * 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..b55a41d8808f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -0,0 +1,571 @@ +/* + * 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.Bundle; +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.SurfaceControl; +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 implements + PipTransitionState.PipTransitionStateChangedListener { + + 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 static final String RESIZE_BOUNDS_CHANGE = "resize_bounds_change"; + + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipBoundsState mPipBoundsState; + private final PipTouchState mPipTouchState; + private final PipScheduler mPipScheduler; + private final PipTransitionState mPipTransitionState; + 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 boolean mWaitingForBoundsChangeTransition = 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, + PipScheduler pipScheduler, + PipTransitionState pipTransitionState, + 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; + mPipScheduler = pipScheduler; + + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); + + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + mPhonePipMenuController = menuActivityController; + mPipUiEventLogger = pipUiEventLogger; + mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + // mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + mPipBoundsState.setBounds(rect); + 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 resizing isn't 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 (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 (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(); + } + + 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); + + mPipScheduler.scheduleUserResizePip(mLastResizeBounds, mAngle); + 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()) { + resetState(); + } + if (!mOngoingPinchToResize) { + return; + } + 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); + + // Update the transition state to schedule a resize transition. + Bundle extra = new Bundle(); + extra.putBoolean(RESIZE_BOUNDS_CHANGE, true); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); + } + + 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); + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + if (!extra.getBoolean(RESIZE_BOUNDS_CHANGE)) break; + mWaitingForBoundsChangeTransition = true; + mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + if (!mWaitingForBoundsChangeTransition) break; + + // If bounds change transition was scheduled from this class, handle leash updates. + mWaitingForBoundsChangeTransition = false; + + SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); + startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, + destinationBounds.left, destinationBounds.top); + startTx.apply(); + + // All motion operations have actually finished, so make bounds cache updates. + cleanUpHighPerfSessionMaybe(); + + // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + + mUpdateResizeBoundsCallback.accept(destinationBounds); + break; + } + } + + /** + * 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..49475077211f 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 @@ -24,23 +24,24 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Matrix; import android.graphics.Rect; import android.view.SurfaceControl; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.function.Consumer; /** * Scheduler for Shell initiated PiP transitions and animations. @@ -52,20 +53,10 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; private final ShellExecutor mMainExecutor; + private final PipTransitionState mPipTransitionState; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; - // pinned PiP task's WC token - @Nullable - private WindowContainerToken mPipTaskToken; - - // pinned PiP task's leash - @Nullable - private SurfaceControl mPinnedTaskLeash; - - // true if Launcher has started swipe PiP to home animation - private boolean mInSwipePipToHomeTransition; - /** * 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 +92,14 @@ public class PipScheduler { } } - public PipScheduler(Context context, PipBoundsState pipBoundsState, - ShellExecutor mainExecutor) { + public PipScheduler(Context context, + PipBoundsState pipBoundsState, + ShellExecutor mainExecutor, + PipTransitionState pipTransitionState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; + mPipTransitionState = pipTransitionState; if (PipUtils.isPip2ExperimentEnabled()) { // temporary broadcast receiver to initiate exit PiP via expand @@ -115,29 +109,25 @@ public class PipScheduler { } } - void setPipTransitionController(PipTransitionController pipTransitionController) { - mPipTransitionController = pipTransitionController; + ShellExecutor getMainExecutor() { + return mMainExecutor; } - void setPinnedTaskLeash(SurfaceControl pinnedTaskLeash) { - mPinnedTaskLeash = pinnedTaskLeash; - } - - void setPipTaskToken(@Nullable WindowContainerToken pipTaskToken) { - mPipTaskToken = pipTaskToken; + void setPipTransitionController(PipTransitionController pipTransitionController) { + mPipTransitionController = pipTransitionController; } @Nullable private WindowContainerTransaction getExitPipViaExpandTransaction() { - if (mPipTaskToken == null) { + if (mPipTransitionState.mPipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); // final expanded bounds to be inherited from the parent - wct.setBounds(mPipTaskToken, null); + wct.setBounds(mPipTransitionState.mPipTaskToken, null); // if we are hitting a multi-activity case // windowing mode change will reparent to original host task - wct.setWindowingMode(mPipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED); return wct; } @@ -162,25 +152,47 @@ public class PipScheduler { /** * Animates resizing of the pinned stack given the duration. */ - public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) { - if (mPipTaskToken == null) { + public void scheduleAnimateResizePip(Rect toBounds) { + if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTaskToken, toBounds); - mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); + wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + mPipTransitionController.startResizeTransition(wct); } - void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { - mInSwipePipToHomeTransition = true; + /** + * Directly perform a scaled matrix transformation on the leash. This will not perform any + * {@link WindowContainerTransaction}. + */ + public void scheduleUserResizePip(Rect toBounds) { + scheduleUserResizePip(toBounds, 0f /* degrees */); } - boolean isInSwipePipToHomeTransition() { - return mInSwipePipToHomeTransition; - } + /** + * Directly perform a scaled matrix transformation on the leash. This will not perform any + * {@link WindowContainerTransaction}. + * + * @param degrees the angle to rotate the bounds to. + */ + public void scheduleUserResizePip(Rect toBounds, float degrees) { + if (toBounds.isEmpty()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Attempted to user resize PIP to empty bounds, aborting.", TAG); + return; + } + SurfaceControl leash = mPipTransitionState.mPinnedTaskLeash; + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + + Matrix transformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; + final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width(); + + transformTensor.setScale(scale, scale); + transformTensor.postTranslate(toBounds.left, toBounds.top); + transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY()); - void onExitPip() { - mPipTaskToken = null; - mPinnedTaskLeash = null; + tx.setMatrix(leash, transformTensor, mMatrixTmp); + tx.apply(); } } 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..319d1999a272 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -0,0 +1,1123 @@ +/* + * 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.INPUT_CONSUMER_PIP; + +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.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Bundle; +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.WindowManagerGlobal; +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 implements PipTransitionState.PipTransitionStateChangedListener { + + 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 PipTransitionState mPipTransitionState; + @NonNull private final PipScheduler mPipScheduler; + @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; + private PipInputConsumer mPipInputConsumer; + + // 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 PipTransitionState pipTransitionState, + @NonNull PipScheduler pipScheduler, + @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; + + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged); + mPipScheduler = pipScheduler; + 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, mPipScheduler, mPipTransitionState, + this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, + mPipPerfHintController); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); + + 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(); + + mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), + INPUT_CONSUMER_PIP, mMainExecutor); + mPipInputConsumer.setInputListener(this::handleTouchEvent); + mPipInputConsumer.setRegistrationListener(this::onRegistrationChanged); + + 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); + mPipInputConsumer.registerInputConsumer(); + } + + void onActivityUnpinned() { + // Clean up state after the last PiP activity is removed + mPipDismissTargetHandler.cleanUpDismissTarget(); + mFloatingContentCoordinator.onContentRemoved(mMotionHelper); + mPipResizeGestureHandler.onActivityUnpinned(); + mPipInputConsumer.unregisterInputConsumer(); + } + + 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; + } + + // Ignore the motion event When the entry animation is waiting to be started + if (!mTouchState.isUserInteracting() && mPipTaskOrganizer.isEntryScheduled()) { + 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(mPipTransitionState.mPinnedTaskLeash); + + // 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); + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, + @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.ENTERED_PIP: + onActivityPinned(); + mTouchState.setAllowInputEvents(true); + break; + case PipTransitionState.EXITED_PIP: + mTouchState.setAllowInputEvents(false); + onActivityUnpinned(); + break; + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + mTouchState.setAllowInputEvents(false); + break; + case PipTransitionState.CHANGED_PIP_BOUNDS: + mTouchState.setAllowInputEvents(true); + break; + } + } + + /** + * 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..7dddd2748f83 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,18 +17,24 @@ 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; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -38,28 +44,51 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; -import com.android.wm.shell.R; +import com.android.internal.util.Preconditions; import com.android.wm.shell.ShellTaskOrganizer; 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; -import java.util.function.Consumer; - /** * Implementation of transitions for PiP on phone. */ -public class PipTransition extends PipTransitionController { +public class PipTransition extends PipTransitionController implements + PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = PipTransition.class.getSimpleName(); + // Used when for ENTERING_PIP state update. + private static final String PIP_TASK_TOKEN = "pip_task_token"; + private static final String PIP_TASK_LEASH = "pip_task_leash"; + + // Used for PiP CHANGING_BOUNDS state update. + static final String PIP_START_TX = "pip_start_tx"; + static final String PIP_FINISH_TX = "pip_finish_tx"; + static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds"; + + /** + * 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; + + // + // Dependencies + // + private final Context mContext; private final PipScheduler mPipScheduler; - @Nullable - private WindowContainerToken mPipTaskToken; + private final PipTransitionState mPipTransitionState; + + // + // Transition tokens + // + @Nullable private IBinder mEnterTransition; @Nullable @@ -67,7 +96,16 @@ public class PipTransition extends PipTransitionController { @Nullable private IBinder mResizeTransition; - private Consumer<Rect> mFinishResizeCallback; + // + // Internal state and relevant cached info + // + + @Nullable + private WindowContainerToken mPipTaskToken; + @Nullable + private SurfaceControl mPipLeash; + @Nullable + private Transitions.TransitionFinishCallback mFinishCallback; public PipTransition( Context context, @@ -77,13 +115,16 @@ public class PipTransition extends PipTransitionController { PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipScheduler pipScheduler) { + PipScheduler pipScheduler, + PipTransitionState pipTransitionState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); mContext = context; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); } @Override @@ -93,6 +134,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition collection stage lifecycle hooks + // + @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { @@ -106,13 +151,11 @@ public class PipTransition extends PipTransitionController { } @Override - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { if (wct == null) { return; } mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); - mFinishResizeCallback = onFinishResizeCallback; } @Nullable @@ -135,6 +178,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition playing stage lifecycle hooks + // + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -150,9 +197,21 @@ 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 we are in swipe PiP to Home transition we are ENTERING_PIP as a jumpcut transition + // is being carried out. + TransitionInfo.Change pipChange = getPipChange(info); + + // If there is no PiP change, exit this transition handler and potentially try others. + if (pipChange == null) return false; + + Bundle extra = new Bundle(); + extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer()); + extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash()); + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP, extra); + + if (mPipTransitionState.isInSwipePipToHomeTransition()) { // If this is the second transition as a part of swipe PiP to home cuj, // handle this transition as a special case with no-op animation. return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction, @@ -168,14 +227,23 @@ public class PipTransition extends PipTransitionController { finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } + + if (isRemovePipTransition(info)) { + return removePipImmediately(info, startTransaction, finishTransaction, finishCallback); + } return false; } + // + // Animation schedulers and entry points + // + private boolean startResizeAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -185,31 +253,27 @@ public class PipTransition extends PipTransitionController { return false; } SurfaceControl pipLeash = pipChange.getLeash(); - Rect destinationBounds = pipChange.getEndAbsBounds(); // Even though the final bounds and crop are applied with finishTransaction since // this is a visible change, we still need to handle the app draw coming in. Snapshot // covering app draw during collection will be removed by startTransaction. So we make - // the crop equal to the final bounds and then scale the leash back to starting bounds. + // the crop equal to the final bounds and then let the current + // animator scale the leash back to starting bounds. + // Note: animator is responsible for applying the startTx but NOT finishTx. startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(), pipChange.getEndAbsBounds().height()); - startTransaction.setScale(pipLeash, - (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), - (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); - startTransaction.apply(); - finishTransaction.setScale(pipLeash, - (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), - (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); - - // We are done with the transition, but will continue animating leash to final bounds. - finishCallback.onTransitionFinished(null); - - // Animate the pip leash with the new buffer - final int duration = mContext.getResources().getInteger( - R.integer.config_pipResizeAnimationDuration); // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator. - startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration); + // Classes interested in continuing the animation would subscribe to this state update + // getting info such as endBounds, startTx, and finishTx as an extra Bundle once + // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS. + Bundle extra = new Bundle(); + extra.putParcelable(PIP_START_TX, startTransaction); + extra.putParcelable(PIP_FINISH_TX, finishTransaction); + extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds()); + + mFinishCallback = finishCallback; + mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra); return true; } @@ -221,17 +285,85 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipScheduler.setInSwipePipToHomeTransition(false); - mPipTaskToken = pipChange.getContainer(); + WindowContainerToken pipTaskToken = pipChange.getContainer(); + SurfaceControl pipLeash = pipChange.getLeash(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + if (pipTaskToken == null || pipLeash == null) { + return false; + } + PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; + Rect srcRectHint = params.getSourceRectHint(); + Rect startBounds = pipChange.getStartAbsBounds(); + Rect destinationBounds = pipChange.getEndAbsBounds(); + + WindowContainerTransaction finishWct = new WindowContainerTransaction(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + + 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( + mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); + SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); + + startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) + .setScale(pipLeash, scaleX, scaleY) + .setWindowCrop(pipLeash, startBounds) + .reparent(overlayLeash, pipLeash) + .setLayer(overlayLeash, Integer.MAX_VALUE); + + // Overlay needs to be adjusted once a new draw comes in resetting surface transform. + tx.setScale(overlayLeash, 1f, 1f); + tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f, + (destinationBounds.height() - overlaySize) / 2f); + } startTransaction.apply(); - finishCallback.onTransitionFinished(null); + + tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), + this::onClientDrawAtTransitionEnd); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); + + // 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 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(mPipTransitionState.getSwipePipToHomeOverlay()); + tx.apply(); + + // We have fully completed enter-PiP animation after the overlay is gone. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + } + }); + animator.addUpdateListener(animation -> { + float alpha = (float) animation.getAnimatedValue(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply(); + }); + animator.start(); + } + private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -240,12 +372,11 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); + // TODO: b/275910498 Use a new implementation of the PiP animator here. finishCallback.onTransitionFinished(null); return true; } @@ -258,10 +389,8 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); finishCallback.onTransitionFinished(null); @@ -273,11 +402,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(); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); 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); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + return true; + } + + // + // Various helpers to resolve transition requests and infos + // + @Nullable private TransitionInfo.Change getPipChange(TransitionInfo info) { for (TransitionInfo.Change change : info.getChanges()) { @@ -303,6 +447,7 @@ public class PipTransition extends PipTransitionController { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); + wct.deferConfigToTransitionEnd(pipTask.token); return wct; } @@ -334,14 +479,71 @@ public class PipTransition extends PipTransitionController { && info.getChanges().size() == 1; } - /** - * TODO: b/275910498 Use a new implementation of the PiP animator here. - */ - private void startResizeAnimation(SurfaceControl leash, Rect startBounds, - Rect endBounds, int duration) {} + private boolean isRemovePipTransition(@NonNull TransitionInfo info) { + if (mPipTransitionState.mPipTaskToken == null) { + // PiP removal makes sense if enter-PiP has cached a valid pinned task token. + return false; + } + TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.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; + } + + // + // Miscellaneous callbacks and listeners + // + + private void onClientDrawAtTransitionEnd() { + if (mPipTransitionState.getSwipePipToHomeOverlay() != null) { + startOverlayFadeoutAnimation(); + } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { + // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint, + // and then we get a signal on client finishing its draw after the transition + // has ended, then we have fully entered PiP. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + } + } - private void onExitPip() { - mPipTaskToken = null; - mPipScheduler.onExitPip(); + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.ENTERING_PIP: + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + mPipTransitionState.mPipTaskToken = extra.getParcelable( + PIP_TASK_TOKEN, WindowContainerToken.class); + mPipTransitionState.mPinnedTaskLeash = extra.getParcelable( + PIP_TASK_LEASH, SurfaceControl.class); + boolean hasValidTokenAndLeash = mPipTransitionState.mPipTaskToken != null + && mPipTransitionState.mPinnedTaskLeash != null; + + Preconditions.checkState(hasValidTokenAndLeash, + "Unexpected bundle for " + mPipTransitionState); + break; + case PipTransitionState.EXITED_PIP: + mPipTransitionState.mPipTaskToken = null; + mPipTransitionState.mPinnedTaskLeash = null; + break; + case PipTransitionState.CHANGED_PIP_BOUNDS: + // Note: this might not be the end of the animation, rather animator just finished + // adjusting startTx and finishTx and is ready to finishTransition(). The animator + // can still continue playing the leash into the destination bounds after. + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null); + mFinishCallback = null; + } + break; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java new file mode 100644 index 000000000000..8204d41a9833 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -0,0 +1,283 @@ +/* + * 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.pip2.phone; + +import android.annotation.IntDef; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Contains the state relevant to carry out or probe the status of PiP transitions. + * + * <p>Existing and new PiP components can subscribe to PiP transition related state changes + * via <code>PipTransitionStateChangedListener</code>.</p> + * + * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering. + * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs + * to some other class <code>Bar</code>, a special care must be given when manipulating state A in + * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of + * the class <code>Bar</code>.</p> + * + * <p>Hence, the recommended usage for classes who want to subscribe to + * <code>PipTransitionState</code> changes is to manipulate only their own internal state or + * <code>PipTransitionState</code> state.</p> + * + * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should + * just be moved to <code>PipTransitionState</code> and become a shared state + * between Foo and Bar.</p> + * + * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code> + * receives a <code>Bundle</code> extra object that can be optionally set via + * <code>setState(state, extra)</code>. This can be used to resolve extra information to update + * relevant internal or <code>PipTransitionState</code> state. However, each listener + * needs to check for whether the extra passed is correct for a particular state, + * and throw an <code>IllegalStateException</code> otherwise.</p> + */ +public class PipTransitionState { + public static final int UNDEFINED = 0; + + // State for Launcher animating the swipe PiP to home animation. + public static final int SWIPING_TO_PIP = 1; + + // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation. + public static final int ENTERING_PIP = 2; + + // State for app finishing drawing in PiP mode as a final step in enter PiP flow. + public static final int ENTERED_PIP = 3; + + // State to indicate we have scheduled a PiP bounds change transition. + public static final int SCHEDULED_BOUNDS_CHANGE = 4; + + // State for the start of playing a transition to change PiP bounds. At this point, WM Core + // is aware of the new PiP bounds, but Shell might still be continuing animating. + public static final int CHANGING_PIP_BOUNDS = 5; + + // State for finishing animating into new PiP bounds after resize is complete. + public static final int CHANGED_PIP_BOUNDS = 6; + + // State for starting exiting PiP. + public static final int EXITING_PIP = 7; + + // State for finishing exit PiP flow. + public static final int EXITED_PIP = 8; + + private static final int FIRST_CUSTOM_STATE = 1000; + + private int mPrevCustomState = FIRST_CUSTOM_STATE; + + @IntDef(prefix = { "TRANSITION_STATE_" }, value = { + UNDEFINED, + SWIPING_TO_PIP, + ENTERING_PIP, + ENTERED_PIP, + SCHEDULED_BOUNDS_CHANGE, + CHANGING_PIP_BOUNDS, + CHANGED_PIP_BOUNDS, + EXITING_PIP, + EXITED_PIP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionState {} + + @TransitionState + private int mState; + + // + // Swipe up to enter PiP related state + // + + // true if Launcher has started swipe PiP to home animation + private boolean mInSwipePipToHomeTransition; + + // 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 + private final Rect mSwipePipToHomeAppBounds = new Rect(); + + // + // Tokens and leashes + // + + // pinned PiP task's WC token + @Nullable + WindowContainerToken mPipTaskToken; + + // pinned PiP task's leash + @Nullable + SurfaceControl mPinnedTaskLeash; + + // Overlay leash potentially used during swipe PiP to home transition; + // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. + @Nullable + private SurfaceControl mSwipePipToHomeOverlay; + + /** + * An interface to track state updates as we progress through PiP transitions. + */ + public interface PipTransitionStateChangedListener { + + /** Reports changes in PiP transition state. */ + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra); + } + + private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>(); + + /** + * @return the state of PiP in the context of transitions. + */ + @TransitionState + public int getState() { + return mState; + } + + /** + * Sets the state of PiP in the context of transitions. + */ + public void setState(@TransitionState int state) { + setState(state, null /* extra */); + } + + /** + * Sets the state of PiP in the context of transitions + * + * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. + */ + public void setState(@TransitionState int state, @Nullable Bundle extra) { + if (state == ENTERING_PIP || state == SWIPING_TO_PIP + || state == SCHEDULED_BOUNDS_CHANGE || state == CHANGING_PIP_BOUNDS) { + // States listed above require extra bundles to be provided. + Preconditions.checkArgument(extra != null && !extra.isEmpty(), + "No extra bundle for " + stateToString(state) + " state."); + } + if (mState != state) { + dispatchPipTransitionStateChanged(mState, state, extra); + mState = state; + } + } + + private void dispatchPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra) { + mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra)); + } + + /** + * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. + */ + public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { + if (mCallbacks.contains(listener)) { + return; + } + mCallbacks.add(listener); + } + + /** + * @return true if provided {@link PipTransitionStateChangedListener} + * is registered before removing it. + */ + public boolean removePipTransitionStateChangedListener( + PipTransitionStateChangedListener listener) { + return mCallbacks.remove(listener); + } + + /** + * @return true if we have fully entered PiP. + */ + public boolean isInPip() { + return mState > ENTERING_PIP && mState < EXITING_PIP; + } + + void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash, + @NonNull Rect appBounds) { + mInSwipePipToHomeTransition = true; + if (overlayLeash != null && !appBounds.isEmpty()) { + mSwipePipToHomeOverlay = overlayLeash; + mSwipePipToHomeAppBounds.set(appBounds); + } + } + + void resetSwipePipToHomeState() { + mInSwipePipToHomeTransition = false; + mSwipePipToHomeOverlay = null; + mSwipePipToHomeAppBounds.setEmpty(); + } + + /** + * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. + */ + public boolean isInSwipePipToHomeTransition() { + return mInSwipePipToHomeTransition; + } + + /** + * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP; + * null if srcRectHint provided is valid. + */ + @Nullable + public SurfaceControl getSwipePipToHomeOverlay() { + return mSwipePipToHomeOverlay; + } + + /** + * @return app bounds used to calculate + */ + @NonNull + public Rect getSwipePipToHomeAppBounds() { + return mSwipePipToHomeAppBounds; + } + + /** + * @return a custom state solely for internal use by the caller. + */ + @TransitionState + public int getCustomState() { + return ++mPrevCustomState; + } + + private static String stateToString(int state) { + switch (state) { + case UNDEFINED: return "undefined"; + case SWIPING_TO_PIP: return "swiping_to_pip"; + case ENTERING_PIP: return "entering-pip"; + case ENTERED_PIP: return "entered-pip"; + case SCHEDULED_BOUNDS_CHANGE: return "scheduled_bounds_change"; + case CHANGING_PIP_BOUNDS: return "changing-bounds"; + case CHANGED_PIP_BOUNDS: return "changed-bounds"; + case EXITING_PIP: return "exiting-pip"; + case EXITED_PIP: return "exited-pip"; + } + throw new IllegalStateException("Unknown state: " + state); + } + + @Override + public String toString() { + return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", + stateToString(mState), mInSwipePipToHomeTransition); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index ad29d15019c5..19af3d544b36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -52,7 +52,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, - Consts.TAG_WM_SHELL), + Consts.TAG_WM_DESKTOP_MODE), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, @@ -120,6 +120,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static final String TAG_WM_SHELL = "WindowManagerShell"; private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; + private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; 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..c53e7fe00598 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.DesktopModeStatus; +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,28 @@ 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.canEnterDesktopMode(mContext) + && enableDesktopWindowingTaskbarRunningApps()); + } + @VisibleForTesting void registerRecentTasksListener(IRecentTasksListener listener) { mListener = listener; @@ -332,6 +361,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++) { @@ -341,9 +372,17 @@ public class RecentTasksController implements TaskStackListenerCallback, continue; } - if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent() + if (DesktopModeStatus.canEnterDesktopMode(mContext) + && mDesktopModeTaskRepository.isPresent() && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { + if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { + // Minimized freeform tasks should not be shown at all. + continue; + } // Freeform tasks will be added as a separate entry + if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) { + mostRecentFreeformTaskIndex = recentTasks.size(); + } freeformTasks.add(taskInfo); continue; } @@ -362,7 +401,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 +483,16 @@ public class RecentTasksController implements TaskStackListenerCallback, }); }); } + + @Override + public void setTransitionBackgroundColor(@Nullable Color color) { + mMainExecutor.execute(() -> { + if (mTransitionHandler == null) { + return; + } + mTransitionHandler.setTransitionBackgroundColor(color); + }); + } } @@ -471,6 +520,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..a7829c905c69 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,10 +22,12 @@ 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; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; import android.annotation.Nullable; @@ -35,6 +37,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; @@ -52,13 +55,17 @@ import android.window.PictureInPictureSurfaceTransaction; import android.window.TaskSnapshot; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowAnimationState; 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 +97,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 +129,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) { @@ -268,6 +285,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private IBinder mTransition = null; private boolean mKeyguardLocked = false; private boolean mWillFinishToHome = false; + private Transitions.TransitionHandler mTakeoverHandler = null; /** The animation is idle, waiting for the user to choose a task to switch to. */ private static final int STATE_NORMAL = 0; @@ -418,6 +436,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 +485,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 +561,35 @@ 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*/); + + mTakeoverHandler = mTransitions.getHandlerForTakeover(mTransition, info); + + Bundle b = new Bundle(2 /*capacity*/); b.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, mRecentTasksController.getSplitBoundsForTaskId(closingSplitTaskId)); + b.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, mTakeoverHandler != null); 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()]), @@ -560,6 +604,63 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return true; } + @Override + public void handOffAnimation( + RemoteAnimationTarget[] targets, WindowAnimationState[] states) { + mExecutor.execute(() -> { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.handOffAnimation", mInstanceId); + + if (mTakeoverHandler == null) { + Slog.e(TAG, "Tried to hand off an animation without a valid takeover " + + "handler."); + return; + } + + if (targets.length != states.length) { + Slog.e(TAG, "Tried to hand off an animation, but the number of targets " + + "(" + targets.length + ") doesn't match the number of states " + + "(" + states.length + ")"); + return; + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.handOffAnimation: got %d states for %d " + + "changes", mInstanceId, states.length, mInfo.getChanges().size()); + WindowAnimationState[] updatedStates = + new WindowAnimationState[mInfo.getChanges().size()]; + + // Ensure that the ordering of animation states is the same as that of matching + // changes in mInfo. prefixOrderIndex is set up in reverse order to that of the + // changes, so that's what we use to get to the correct ordering. + for (int i = 0; i < targets.length; i++) { + RemoteAnimationTarget target = targets[i]; + updatedStates[updatedStates.length - target.prefixOrderIndex] = states[i]; + } + + Transitions.TransitionFinishCallback finishCB = mFinishCB; + // Reset the callback here, so any stray calls that aren't coming from the new + // handler are ignored. + mFinishCB = null; + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.handOffAnimation: calling " + + "takeOverAnimation with %d states", mInstanceId, + updatedStates.length); + mTakeoverHandler.takeOverAnimation( + mTransition, mInfo, new SurfaceControl.Transaction(), + wct -> { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.handOffAnimation: finish " + + "callback", mInstanceId); + // Set the callback once again so we can finish correctly. + mFinishCB = finishCB; + finishInner(true /* toHome */, false /* userLeave */, + null /* finishCb */); + }, updatedStates); + }); + } + /** * Updates this controller when a new transition is requested mid-recents transition. */ @@ -1011,13 +1112,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 +1140,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 +1183,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 +1194,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..8df287d12cbc 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; @@ -64,7 +69,9 @@ public interface SplitScreen { default void onSplitVisibilityChanged(boolean visible) {} } - /** Callback interface for listening to requests to enter split select */ + /** + * Callback interface for listening to requests to enter split select. Used for desktop -> split + */ interface SplitSelectListener { default boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds) { @@ -72,6 +79,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); @@ -79,12 +92,33 @@ public interface SplitScreen { /** Unregisters listener that gets split screen callback. */ void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); + interface SplitInvocationListener { + /** + * Called whenever shell starts or stops the split screen animation + * @param animationRunning if {@code true} the animation has begun, if {@code false} the + * animation has finished + */ + default void onSplitAnimationInvoked(boolean animationRunning) { } + } + + /** + * Registers a {@link SplitInvocationListener} to notify when the animation to enter split + * screen has started and stopped + * + * @param executor callbacks to the listener will be executed on this executor + */ + void registerSplitAnimationListener(@NonNull SplitInvocationListener listener, + @NonNull Executor executor); + /** Called when device waking up finished. */ void onFinishedWakingUp(); /** 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..b9d70e1a599d 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; @@ -1134,6 +1166,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void registerSplitAnimationListener(@NonNull SplitInvocationListener listener, + @NonNull Executor executor) { + mStageCoordinator.registerSplitAnimationListener(listener, executor); + } + + @Override public void onFinishedWakingUp() { mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp); } @@ -1142,6 +1180,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/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 1a53a1d10dd2..6e5b7673e206 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -55,6 +55,7 @@ import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; +import java.util.concurrent.Executor; /** Manages transition animations for split-screen. */ class SplitScreenTransitions { @@ -79,6 +80,8 @@ class SplitScreenTransitions { private Transitions.TransitionFinishCallback mFinishCallback = null; private SurfaceControl.Transaction mFinishTransaction; + private SplitScreen.SplitInvocationListener mSplitInvocationListener; + private Executor mSplitInvocationListenerExecutor; SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, @NonNull Runnable onFinishCallback, StageCoordinator stageCoordinator) { @@ -353,6 +356,10 @@ class SplitScreenTransitions { + " skip to start enter split transition since it already exist. "); return null; } + if (mSplitInvocationListenerExecutor != null && mSplitInvocationListener != null) { + mSplitInvocationListenerExecutor.execute(() -> mSplitInvocationListener + .onSplitAnimationInvoked(true /*animationRunning*/)); + } final IBinder transition = mTransitions.startTransition(transitType, wct, handler); setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim); return transition; @@ -457,6 +464,7 @@ class SplitScreenTransitions { mPendingEnter.onConsumed(aborted); mPendingEnter = null; + mStageCoordinator.notifySplitAnimationFinished(); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for enter transition"); } else if (isPendingDismiss(transition)) { mPendingDismiss.onConsumed(aborted); @@ -529,6 +537,12 @@ class SplitScreenTransitions { mTransitions.getAnimExecutor().execute(va::start); } + public void registerSplitAnimListener(@NonNull SplitScreen.SplitInvocationListener listener, + @NonNull Executor executor) { + mSplitInvocationListener = listener; + mSplitInvocationListenerExecutor = executor; + } + /** Calls when the transition got consumed. */ interface TransitionConsumedCallback { void onConsumed(boolean aborted); 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 6188e08b8396..4299088a51f0 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; @@ -47,6 +48,7 @@ import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPos import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; +import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; @@ -59,6 +61,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; @@ -66,6 +69,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; +import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; @@ -156,6 +160,7 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and @@ -236,6 +241,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; private SplitRequest mSplitRequest; + /** Used to notify others of when shell is animating into split screen */ + private SplitScreen.SplitInvocationListener mSplitInvocationListener; + private Executor mSplitInvocationListenerExecutor; /** * Since StageCoordinator only coordinates MainStage and SideStage, it shouldn't support @@ -246,6 +254,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return false; } + /** NOTE: Will overwrite any previously set {@link #mSplitInvocationListener} */ + public void registerSplitAnimationListener( + @NonNull SplitScreen.SplitInvocationListener listener, @NonNull Executor executor) { + mSplitInvocationListener = listener; + mSplitInvocationListenerExecutor = executor; + mSplitTransitions.registerSplitAnimListener(listener, executor); + } + class SplitRequest { @SplitPosition int mActivatePosition; @@ -534,7 +550,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, null /* childrenToTop */, EXIT_REASON_UNKNOWN)); Log.w(TAG, splitFailureMessage("startShortcut", "side stage was not populated")); - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); } if (finishedCallback != null) { @@ -665,7 +681,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, null /* childrenToTop */, EXIT_REASON_UNKNOWN)); Log.w(TAG, splitFailureMessage("startIntentLegacy", "side stage was not populated")); - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); } if (apps != null) { @@ -1286,7 +1302,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled", "main or side stage was not populated.")); - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); } else { mSyncQueue.queue(evictWct); mSyncQueue.runInSync(t -> { @@ -1307,7 +1323,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished", "main or side stage was not populated")); - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); return; } @@ -1524,6 +1540,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()); @@ -1543,6 +1560,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", @@ -1620,6 +1638,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. */ @@ -1655,6 +1681,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 @@ -1685,6 +1721,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; @@ -1812,10 +1850,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void finishEnterSplitScreen(SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen"); mSplitLayout.update(finishT, true /* resetImePosition */); - mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash, - getMainStageBounds()); - mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash, - getSideStageBounds()); + mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash); + mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash); setDividerVisibility(true, finishT); // Ensure divider surface are re-parented back into the hierarchy at the end of the // transition. See Transition#buildFinishTransaction for more detail. @@ -2672,6 +2708,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(); @@ -2802,7 +2845,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setFreezeDividerWindow(false); final StageChangeRecord record = new StageChangeRecord(); final int transitType = info.getType(); - boolean hasEnteringPip = false; + TransitionInfo.Change pipChange = null; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); if (change.getMode() == TRANSIT_CHANGE @@ -2813,7 +2856,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (mMixedHandler.isEnteringPip(change, transitType)) { - hasEnteringPip = true; + pipChange = change; } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); @@ -2856,7 +2899,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" @@ -2865,9 +2908,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - if (hasEnteringPip) { + if (pipChange != null) { + TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange, + mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId, + getSplitItemStage(pipChange.getLastParent())); + if (pipReplacingChange != null) { + // Set an enter transition for when startAnimation gets called again + mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null, + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false); + } + mMixedHandler.animatePendingEnterPipFromSplit(transition, info, - startTransaction, finishTransaction, finishCallback); + startTransaction, finishTransaction, finishCallback, + pipReplacingChange != null); + notifySplitAnimationFinished(); return true; } @@ -2902,6 +2956,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // the transition, or synchronize task-org callbacks. } // Use normal animations. + notifySplitAnimationFinished(); return false; } else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) { // A display-change has been un-expectedly inserted into the transition. Redirect @@ -2915,6 +2970,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.update(startTransaction, true /* resetImePosition */); startTransaction.apply(); } + notifySplitAnimationFinished(); return true; } } @@ -3088,7 +3144,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pendingEnter.mRemoteHandler.onTransitionConsumed(transition, false /*aborted*/, finishT); } - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); return true; } } @@ -3117,6 +3173,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final TransitionInfo.Change finalMainChild = mainChild; final TransitionInfo.Change finalSideChild = sideChild; enterTransition.setFinishedCallback((callbackWct, callbackT) -> { + notifySplitAnimationFinished(); if (finalMainChild != null) { if (!mainNotContainOpenTask) { mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId); @@ -3402,6 +3459,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, true /* reparentLeafTaskIfRelaunch */); } + /** Call this when the animation from split screen to desktop is started. */ + public void onSplitToDesktop() { + setSplitsVisible(false); + } + /** Call this when the recents animation finishes by doing pair-to-pair switch. */ public void onRecentsPairToPairAnimationFinish(WindowContainerTransaction finishWct) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsPairToPairAnimationFinish"); @@ -3533,6 +3595,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.isLeftRightSplit()); } + private void handleUnsupportedSplitStart() { + mSplitUnsupportedToast.show(); + notifySplitAnimationFinished(); + } + + void notifySplitAnimationFinished() { + if (mSplitInvocationListener == null || mSplitInvocationListenerExecutor == null) { + return; + } + mSplitInvocationListenerExecutor.execute(() -> + mSplitInvocationListener.onSplitAnimationInvoked(false /*animationRunning*/)); + } + /** * Logs the exit of splitscreen to a specific stage. This must be called before the exit is * executed. @@ -3595,7 +3670,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!ENABLE_SHELL_TRANSITIONS) { StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); return; } @@ -3615,7 +3690,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, "app package " + taskInfo.baseActivity.getPackageName() + " does not support splitscreen, or is a controlled activity type")); if (splitScreenVisible) { - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 130babe1d8ea..0f3d6cade95a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -177,9 +177,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: task=%d taskParent=%d rootTask=%d", + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: taskId=%d taskParent=%d rootTask=%d " + + "taskActivity=%s", taskInfo.taskId, taskInfo.parentTaskId, - mRootTaskInfo != null ? mRootTaskInfo.taskId : -1); + mRootTaskInfo != null ? mRootTaskInfo.taskId : -1, + taskInfo.baseActivity); if (mRootTaskInfo == null) { mRootLeash = leash; mRootTaskInfo = taskInfo; @@ -213,13 +215,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s", + taskInfo.taskId, taskInfo.baseActivity); mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo)); if (mRootTaskInfo.taskId == taskInfo.taskId) { // Inflates split decor view only when the root task is visible. if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) { if (taskInfo.isVisible) { - mSplitDecorManager.inflate(mContext, mRootLeash, - taskInfo.configuration.windowConfiguration.getBounds()); + mSplitDecorManager.inflate(mContext, mRootLeash); } else { mSyncQueue.runInSync(t -> mSplitDecorManager.release(t)); } @@ -261,6 +264,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId); final int taskId = taskInfo.taskId; + mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo)); if (mRootTaskInfo.taskId == taskId) { mCallbacks.onRootTaskVanished(); mRootTaskInfo = null; 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/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index e419462012e3..e07e1b460168 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -45,6 +45,7 @@ import android.window.SplashScreenView; import com.android.internal.R; +import java.io.Closeable; import java.util.function.LongConsumer; /** @@ -100,7 +101,7 @@ public class SplashscreenIconDrawableFactory { * Drawable pre-drawing the scaled icon in a separate thread to increase the speed of the * final drawing. */ - private static class ImmobileIconDrawable extends Drawable { + private static class ImmobileIconDrawable extends Drawable implements Closeable { private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); private final Matrix mMatrix = new Matrix(); @@ -154,6 +155,16 @@ public class SplashscreenIconDrawableFactory { public int getOpacity() { return 1; } + + @Override + public void close() { + synchronized (mPaint) { + if (mIconBitmap != null) { + mIconBitmap.recycle(); + mIconBitmap = null; + } + } + } } /** 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..e552e6cdacf3 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 @@ -30,7 +30,6 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; @@ -54,7 +53,6 @@ import android.window.SplashScreenView; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; -import com.android.internal.R; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ContrastColorUtil; import com.android.wm.shell.common.ShellExecutor; @@ -206,7 +204,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { final SplashWindowRecord record = (SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId); if (record != null) { - record.parseAppSystemBarColor(context); // Block until we get the background color. final SplashScreenView contentView = viewSupplier.get(); if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { @@ -427,8 +424,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { private boolean mSetSplashScreen; private SplashScreenView mSplashView; - private int mSystemBarAppearance; - private boolean mDrawsSystemBarBackgrounds; SplashWindowRecord(IBinder appToken, View decorView, @StartingWindowInfo.StartingWindowType int suggestType) { @@ -448,38 +443,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { mSetSplashScreen = true; } - void parseAppSystemBarColor(Context context) { - final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); - mDrawsSystemBarBackgrounds = a.getBoolean( - R.styleable.Window_windowDrawsSystemBarBackgrounds, false); - if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - } - if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; - } - 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 +454,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..66b3553bea09 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 @@ -21,12 +21,14 @@ import static android.graphics.Color.WHITE; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; +import static com.android.window.flags.Flags.windowSessionRelayoutInfo; + 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; -import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -42,6 +44,8 @@ import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.WindowRelayoutResult; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.SnapshotDrawerUtils; import android.window.StartingWindowInfo; @@ -98,8 +102,6 @@ public class TaskSnapshotWindow { return null; } - final Point taskSize = snapshot.getTaskSize(); - final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y); final int orientation = snapshot.getOrientation(); final int displayId = runningTaskInfo.displayId; @@ -137,9 +139,16 @@ public class TaskSnapshotWindow { } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); + if (windowSessionRelayoutInfo()) { + final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames, + tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, + outRelayoutResult); + } else { + session.relayoutLegacy(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, new Bundle()); + } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); @@ -148,7 +157,7 @@ public class TaskSnapshotWindow { } SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot, - taskBounds, tmpFrames.frame, topWindowInsetsState, true /* releaseAfterDraw */); + info.taskBounds, topWindowInsetsState, true /* releaseAfterDraw */); snapshotSurface.mHasDrawn = true; snapshotSurface.reportDrawn(); @@ -214,7 +223,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/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java index fed2f34b5e0c..5c814dcc9b16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -23,7 +23,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; -import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.view.Display; @@ -77,15 +76,13 @@ class WindowlessSnapshotWindowCreator { runningTaskInfo.configuration, rootSurface); final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost( mContext, display, wlw, "WindowlessSnapshotWindowCreator"); - final Point taskSize = snapshot.getTaskSize(); - final Rect snapshotBounds = new Rect(0, 0, taskSize.x, taskSize.y); final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds(); final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; final FrameLayout rootLayout = new FrameLayout( mSplashscreenContentDrawer.createViewContextWrapper(mContext)); mViewHost.setView(rootLayout, lp); SnapshotDrawerUtils.drawSnapshotOnSurface(info, lp, wlw.mChildSurface, snapshot, - snapshotBounds, windowBounds, topWindowInsetsState, false /* releaseAfterDraw */); + windowBounds, topWindowInsetsState, false /* releaseAfterDraw */); final ActivityManager.TaskDescription taskDescription = SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo); 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/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java index 56c0d0e67cab..c886cc999216 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -42,4 +42,7 @@ public class ShellSharedConstants { public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode"; // See IDragAndDrop.aidl public static final String KEY_EXTRA_SHELL_DRAG_AND_DROP = "extra_shell_drag_and_drop"; + // See IRecentsAnimationController.aidl + public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION = + "extra_shell_can_hand_off_animation"; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index 35a1fa0a92f6..a85188a9e04d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -30,7 +30,6 @@ import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; -import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -121,6 +120,11 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mTaskViewTaskController.isUsingShellTransitions()) { + // No need for additional work as it is already taken care of during + // prepareOpenAnimation(). + return; + } onLocationChanged(); if (taskInfo.taskDescription != null) { final int bgColor = taskInfo.taskDescription.getBackgroundColor(); 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/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 196e04edbb10..11aa402aa283 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,6 +49,23 @@ import java.util.concurrent.Executor; * TaskView} to {@link TaskViewTaskController} interactions are done via direct method calls. * * The reverse communication is done via the {@link TaskViewBase} interface. + * + * <ul> + * <li>The entry point for an activity based task view is {@link + * TaskViewTaskController#startActivity(PendingIntent, Intent, ActivityOptions, Rect)}</li> + * + * <li>The entry point for an activity (represented by {@link ShortcutInfo}) based task view + * is {@link TaskViewTaskController#startShortcutActivity(ShortcutInfo, ActivityOptions, Rect)} + * </li> + * + * <li>The entry point for a root-task based task view is {@link + * TaskViewTaskController#startRootTask(ActivityManager.RunningTaskInfo, SurfaceControl, + * WindowContainerTransaction)}. + * This method is special as it doesn't create a root task and instead expects that the + * launch root task is already created and started. This method just attaches the taskInfo to + * the TaskView. + * </li> + * </ul> */ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { @@ -155,8 +173,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { * <p>The owner of this container must be allowed to access the shortcut information, * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. * - * @param shortcut the shortcut used to launch the activity. - * @param options options for the activity. + * @param shortcut the shortcut used to launch the activity. + * @param options options for the activity. * @param launchBounds the bounds (window size and position) that the activity should be * launched in, in pixels and in screen coordinates. */ @@ -183,10 +201,10 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { * Launch a new activity. * * @param pendingIntent Intent used to launch an activity. - * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} - * @param options options for the activity. - * @param launchBounds the bounds (window size and position) that the activity should be - * launched in, in pixels and in screen coordinates. + * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} + * @param options options for the activity. + * @param launchBounds the bounds (window size and position) that the activity should be + * launched in, in pixels and in screen coordinates. */ public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds) { @@ -208,6 +226,35 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } } + + /** + * Attaches the given root task {@code taskInfo} in the task view. + * + * <p> Since {@link ShellTaskOrganizer#createRootTask(int, int, + * ShellTaskOrganizer.TaskListener)} does not use the shell transitions flow, this method is + * used as an entry point for an already-created root-task in the task view. + * + * @param taskInfo the task info of the root task. + * @param leash the {@link android.content.pm.ShortcutInfo.Surface} of the root task + * @param wct The Window container work that should happen as part of this set up. + */ + public void startRootTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + @Nullable WindowContainerTransaction wct) { + if (wct == null) { + wct = new WindowContainerTransaction(); + } + // This method skips the regular flow where an activity task is launched as part of a new + // transition in taskview and then transition is intercepted using the launchcookie. + // The task here is already created and running, it just needs to be reparented, resized + // and tracked correctly inside taskview. Which is done by calling + // prepareOpenAnimationInternal() and then manually enqueuing the resulting window container + // transaction. + prepareOpenAnimationInternal(true /* newTask */, mTransaction /* startTransaction */, + null /* finishTransaction */, taskInfo, leash, wct); + mTransaction.apply(); + mTaskViewTransitions.startInstantTransition(TRANSIT_CHANGE, wct); + } + private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { final Binder launchCookie = new Binder(); mShellExecutor.execute(() -> { @@ -342,7 +389,6 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { final SurfaceControl taskLeash = mTaskLeash; handleAndNotifyTaskRemoval(mTaskInfo); - // Unparent the task when this surface is destroyed mTransaction.reparent(taskLeash, null).apply(); resetTaskInfo(); } @@ -597,6 +643,15 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { @NonNull SurfaceControl.Transaction finishTransaction, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, WindowContainerTransaction wct) { + prepareOpenAnimationInternal(newTask, startTransaction, finishTransaction, taskInfo, leash, + wct); + } + + private void prepareOpenAnimationInternal(final boolean newTask, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + WindowContainerTransaction wct) { mPendingInfo = null; mTaskInfo = taskInfo; mTaskToken = mTaskInfo.token; @@ -608,10 +663,12 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { // Also reparent on finishTransaction since the finishTransaction will reparent back // to its "original" parent by default. Rect boundsOnScreen = mTaskViewBase.getCurrentBoundsOnScreen(); - finishTransaction.reparent(mTaskLeash, mSurfaceControl) - .setPosition(mTaskLeash, 0, 0) - // TODO: maybe once b/280900002 is fixed this will be unnecessary - .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height()); + if (finishTransaction != null) { + finishTransaction.reparent(mTaskLeash, mSurfaceControl) + .setPosition(mTaskLeash, 0, 0) + // TODO: maybe once b/280900002 is fixed this will be unnecessary + .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height()); + } mTaskViewTransitions.updateBoundsState(this, boundsOnScreen); mTaskViewTransitions.updateVisibilityState(this, true /* visible */); wct.setBounds(mTaskToken, boundsOnScreen); @@ -632,6 +689,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor); } + mTaskViewBase.onTaskAppeared(mTaskInfo, mTaskLeash); if (mListener != null) { final int taskId = mTaskInfo.taskId; final ComponentName baseActivity = mTaskInfo.baseActivity; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 198ec82b5f21..e6d1b4593a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -53,7 +53,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { new ArrayMap<>(); private final ArrayList<PendingTransition> mPending = new ArrayList<>(); private final Transitions mTransitions; - private final boolean[] mRegistered = new boolean[]{ false }; + private final boolean[] mRegistered = new boolean[]{false}; /** * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be @@ -122,6 +122,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { /** * Looks through the pending transitions for a closing transaction that matches the provided * `taskView`. + * * @param taskView the pending transition should be for this. */ private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) { @@ -135,8 +136,17 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } /** + * Starts a transition outside of the handler associated with {@link TaskViewTransitions}. + */ + public void startInstantTransition(@WindowManager.TransitionType int type, + WindowContainerTransaction wct) { + mTransitions.startTransition(type, wct, null); + } + + /** * Looks through the pending transitions for a opening transaction that matches the provided * `taskView`. + * * @param taskView the pending transition should be for this. */ @VisibleForTesting @@ -152,8 +162,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { /** * Looks through the pending transitions for one matching `taskView`. + * * @param taskView the pending transition should be for this. - * @param type the type of transition it's looking for + * @param type the type of transition it's looking for */ PendingTransition findPending(TaskViewTaskController taskView, int type) { for (int i = mPending.size() - 1; i >= 0; --i) { @@ -220,7 +231,24 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { startNextTransition(); } - void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + /** Starts a new transition to make the given {@code taskView} visible. */ + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + setTaskViewVisible(taskView, visible, false /* reorder */); + } + + /** + * Starts a new transition to make the given {@code taskView} visible and optionally change + * the task order. + * + * @param taskView the task view which the visibility is being changed for + * @param visible the new visibility of the task view + * @param reorder whether to reorder the task or not. If this is {@code true}, the task will be + * reordered as per the given {@code visible}. For {@code visible = true}, task + * will be reordered to top. For {@code visible = false}, task will be reordered + * to the bottom + */ + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible, + boolean reorder) { if (mTaskViews.get(taskView) == null) return; if (mTaskViews.get(taskView).mVisible == visible) return; if (taskView.getTaskInfo() == null) { @@ -231,6 +259,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */); wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds); + if (reorder) { + wct.reorder(taskView.getTaskInfo().token, visible /* onTop */); + } PendingTransition pending = new PendingTransition( visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); mPending.add(pending); @@ -238,6 +269,22 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // visibility is reported in transition. } + /** Starts a new transition to reorder the given {@code taskView}'s task. */ + public void reorderTaskViewTask(TaskViewTaskController taskView, boolean onTop) { + if (mTaskViews.get(taskView) == null) return; + if (taskView.getTaskInfo() == null) { + // Nothing to update, task is not yet available + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(taskView.getTaskInfo().token, onTop /* onTop */); + PendingTransition pending = new PendingTransition( + onTop ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); + mPending.add(pending); + startNextTransition(); + // visibility is reported in transition. + } + void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) { TaskViewRequestedState state = mTaskViews.get(taskView); if (state == null) return; @@ -380,7 +427,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) - .setPosition(chg.getLeash(), 0, 0); + .setPosition(chg.getLeash(), 0, 0); changesHandled++; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 422a2e06a722..bcacecbd8981 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -61,9 +61,10 @@ import java.util.function.Consumer; /** * A handler for dealing with transitions involving multiple other handlers. For example: an - * activity in split-screen going into PiP. + * activity in split-screen going into PiP. Note this is provided as a handset-specific + * implementation of {@code MixedTransitionHandler}. */ -public class DefaultMixedHandler implements Transitions.TransitionHandler, +public class DefaultMixedHandler implements MixedTransitionHandler, RecentsTransitionHandler.RecentsMixedHandler { private final Transitions mPlayer; @@ -76,6 +77,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private ActivityEmbeddingController mActivityEmbeddingController; abstract static class MixedTransition { + /** Entering Pip from split, breaks split. */ static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; /** Both the display and split-state (enter/exit) is changing */ @@ -102,6 +104,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, /** Enter pip from one of the Activity Embedding windows. */ static final int TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING = 9; + /** Entering Pip from split, but replace the Pip stage instead of breaking split. */ + static final int TYPE_ENTER_PIP_REPLACE_FROM_SPLIT = 10; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; @@ -116,7 +121,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, final IBinder mTransition; protected final Transitions mPlayer; - protected final DefaultMixedHandler mMixedHandler; + protected final MixedTransitionHandler mMixedHandler; protected final PipTransitionController mPipHandler; protected final StageCoordinator mSplitHandler; protected final KeyguardTransitionHandler mKeyguardHandler; @@ -142,7 +147,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, int mInFlightSubAnimations = 0; MixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) { mType = type; mTransition = transition; @@ -483,9 +488,11 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, // TODO(b/287704263): Remove when split/mixed are reversed. public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - Transitions.TransitionFinishCallback finishCallback) { - final MixedTransition mixed = createDefaultMixedTransition( - MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition); + Transitions.TransitionFinishCallback finishCallback, boolean replacingPip) { + int type = replacingPip + ? MixedTransition.TYPE_ENTER_PIP_REPLACE_FROM_SPLIT + : MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT; + final MixedTransition mixed = createDefaultMixedTransition(type, transition); mActiveTransitions.add(mixed); Transitions.TransitionFinishCallback callback = wct -> { mActiveTransitions.remove(mixed); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index e9cd73b0df5e..0ada74937df4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -41,7 +41,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { private final ActivityEmbeddingController mActivityEmbeddingController; DefaultMixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, UnfoldTransitionHandler unfoldHandler, ActivityEmbeddingController activityEmbeddingController) { @@ -76,7 +76,12 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { info, startTransaction, finishTransaction, finishCallback); case TYPE_ENTER_PIP_FROM_SPLIT -> animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, - finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler); + finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler, + /*replacingPip*/ false); + case TYPE_ENTER_PIP_REPLACE_FROM_SPLIT -> + animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, + finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler, + /*replacingPip*/ true); case TYPE_KEYGUARD -> animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback, mKeyguardHandler, mPipHandler); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 9adb67c8a65e..2d6ba6ee7217 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -594,7 +594,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { .setName("animation-background") .setCallsite("DefaultTransitionHandler") .setColorLayer(); - final SurfaceControl backgroundSurface = colorLayerBuilder.build(); // Attaching the background surface to the transition root could unexpectedly make it // cover one of the split root tasks. To avoid this, put the background surface just @@ -605,8 +604,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (isSplitTaskInvolved) { mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder); } else { - startTransaction.reparent(backgroundSurface, info.getRootLeash()); + colorLayerBuilder.setParent(info.getRootLeash()); } + + final SurfaceControl backgroundSurface = colorLayerBuilder.build(); startTransaction.setColor(backgroundSurface, colorArray) .setLayer(backgroundSurface, -1) .show(backgroundSurface); 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/MixedTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java new file mode 100644 index 000000000000..ff429fb12c94 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.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.transition; + +/** + * Interface for a {@link Transitions.TransitionHandler} that can take the subset of transitions + * that it handles and further decompose those transitions into sub-transitions which can be + * independently delegated to separate handlers. + */ +public interface MixedTransitionHandler extends Transitions.TransitionHandler { + + // TODO(b/335685449) this currently exists purely as a marker interface for use in form-factor + // specific/sysui dagger modules. Going forward, we should define this in a meaningful + // way so as to provide a clear basis for expectations/behaviours associated with mixed + // transitions and their default handlers. + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java index 0974cd13f249..e8b01b5880fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java @@ -23,11 +23,15 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; +import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy; import android.annotation.NonNull; +import android.annotation.Nullable; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -44,8 +48,9 @@ public class MixedTransitionHelper { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler, - @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) { + @NonNull Transitions player, @NonNull MixedTransitionHandler mixedHandler, + @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler, + boolean replacingPip) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering PIP while Split-Screen is foreground."); TransitionInfo.Change pipChange = null; @@ -99,7 +104,7 @@ public class MixedTransitionHelper { // we need a separate one to send over to launcher. SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED; - if (splitHandler.isSplitScreenVisible()) { + if (splitHandler.isSplitScreenVisible() && !replacingPip) { // The non-going home case, we could be pip-ing one of the split stages and keep // showing the other for (int i = info.getChanges().size() - 1; i >= 0; --i) { @@ -115,11 +120,12 @@ public class MixedTransitionHelper { break; } } + + // Let split update internal state for dismiss. + splitHandler.prepareDismissAnimation(topStageToKeep, + EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, + finishTransaction); } - // Let split update internal state for dismiss. - splitHandler.prepareDismissAnimation(topStageToKeep, - EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, - finishTransaction); // We are trying to accommodate launcher's close animation which can't handle the // divider-bar, so if split-handler is closing the divider-bar, just hide it and @@ -152,6 +158,44 @@ public class MixedTransitionHelper { return true; } + /** + * Check to see if we're only closing split to enter pip or if we're replacing pip with + * another task. If we are replacing, this will return the change for the task we are replacing + * pip with + * + * @param info Any number of changes + * @param pipChange TransitionInfo.Change indicating the task that is being pipped + * @param splitMainStageRootId MainStage's rootTaskInfo's id + * @param splitSideStageRootId SideStage's rootTaskInfo's id + * @param lastPipSplitStage The last stage that {@param pipChange} was in + * @return The change from {@param info} that is replacing the {@param pipChange}, {@code null} + * otherwise + */ + @Nullable + public static TransitionInfo.Change getPipReplacingChange(TransitionInfo info, + TransitionInfo.Change pipChange, int splitMainStageRootId, int splitSideStageRootId, + @SplitScreen.StageType int lastPipSplitStage) { + int lastPipParentTask = -1; + if (lastPipSplitStage == STAGE_TYPE_MAIN) { + lastPipParentTask = splitMainStageRootId; + } else if (lastPipSplitStage == STAGE_TYPE_SIDE) { + lastPipParentTask = splitSideStageRootId; + } + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (change == pipChange || !isOpeningMode(change.getMode())) { + // Ignore the change/task that's going into Pip or not opening + continue; + } + + if (change.getTaskInfo().parentTaskId == lastPipParentTask) { + return change; + } + } + return null; + } + private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) { return change.getTaskInfo() != null && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 94519a0d118c..69c41675e989 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -27,6 +27,7 @@ import android.window.IRemoteTransitionFinishedCallback; import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowAnimationState; import android.window.WindowContainerTransaction; import com.android.internal.protolog.common.ProtoLog; @@ -65,30 +66,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" + " transition %s for (#%d).", mRemote, info.getDebugId()); - final IBinder.DeathRecipient remoteDied = () -> { - Log.e(Transitions.TAG, "Remote transition died, finishing"); - mMainExecutor.execute( - () -> finishCallback.onTransitionFinished(null /* wct */)); - }; - IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { - @Override - public void onTransitionFinished(WindowContainerTransaction wct, - SurfaceControl.Transaction sct) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Finished one-shot remote transition %s for (#%d).", mRemote, - info.getDebugId()); - if (mRemote.asBinder() != null) { - mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); - } - if (sct != null) { - finishTransaction.merge(sct); - } - mMainExecutor.execute(() -> { - finishCallback.onTransitionFinished(wct); - mRemote = null; - }); - } - }; + final IBinder.DeathRecipient remoteDied = createDeathRecipient(finishCallback); + IRemoteTransitionFinishedCallback cb = + createFinishedCallback(info, finishTransaction, finishCallback, remoteDied); Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread()); try { if (mRemote.asBinder() != null) { @@ -152,6 +132,51 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { } @Override + public boolean takeOverAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction transaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowAnimationState[] states) { + if (mTransition != transition) return false; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "Using registered One-shot " + + "remote transition %s to take over (#%d).", mRemote, info.getDebugId()); + + final IBinder.DeathRecipient remoteDied = createDeathRecipient(finishCallback); + IRemoteTransitionFinishedCallback cb = createFinishedCallback( + info, null /* finishTransaction */, finishCallback, remoteDied); + + Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread()); + + try { + if (mRemote.asBinder() != null) { + mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); + } + + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteStartT = + RemoteTransitionHandler.copyIfLocal(transaction, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = + remoteStartT == transaction ? info : info.localRemoteCopy(); + mRemote.getRemoteTransition().takeOverAnimation( + transition, remoteInfo, remoteStartT, cb, states); + + // Assume that remote will apply the transaction. + transaction.clear(); + return true; + } catch (RemoteException e) { + Log.e(Transitions.TAG, "Error running remote transition takeover.", e); + if (mRemote.asBinder() != null) { + mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); + } + finishCallback.onTransitionFinished(null /* wct */); + mRemote = null; + } + + return false; + } + + @Override @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { @@ -174,6 +199,41 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { } } + private IBinder.DeathRecipient createDeathRecipient( + Transitions.TransitionFinishCallback finishCallback) { + return () -> { + Log.e(Transitions.TAG, "Remote transition died, finishing"); + mMainExecutor.execute( + () -> finishCallback.onTransitionFinished(null /* wct */)); + }; + } + + private IRemoteTransitionFinishedCallback createFinishedCallback( + @NonNull TransitionInfo info, + @Nullable SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull IBinder.DeathRecipient remoteDied) { + return new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Finished one-shot remote transition %s for (#%d).", mRemote, + info.getDebugId()); + if (mRemote.asBinder() != null) { + mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); + } + if (finishTransaction != null && sct != null) { + finishTransaction.merge(sct); + } + mMainExecutor.execute(() -> { + finishCallback.onTransitionFinished(wct); + mRemote = null; + }); + } + }; + } + @Override public String toString() { return "OneShotRemoteHandler:" + mRemote.getDebugName() + ":" 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 5b402a5a7d53..9fc6702562bb 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 @@ -43,7 +43,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { private final DesktopTasksController mDesktopTasksController; RecentsMixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, RecentsTransitionHandler recentsHandler, DesktopTasksController desktopTasksController) { @@ -142,7 +142,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { && mSplitHandler.getSplitItemPosition(change.getLastParent()) != SPLIT_POSITION_UNDEFINED) { return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, - finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler); + finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler, + /*replacingPip*/ false); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 4c4c5806ea55..d6860464d055 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -16,6 +16,8 @@ package com.android.wm.shell.transition; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; + import android.annotation.NonNull; import android.annotation.Nullable; import android.os.IBinder; @@ -32,6 +34,7 @@ import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowAnimationState; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; @@ -41,7 +44,9 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; +import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; /** * Handler that deals with RemoteTransitions. It will only request to handle a transition @@ -58,6 +63,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { /** Ordered by specificity. Last filters will be checked first */ private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters = new ArrayList<>(); + private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mTakeoverFilters = + new ArrayList<>(); private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>(); @@ -70,14 +77,23 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { mFilters.add(new Pair<>(filter, remote)); } + void addFilteredForTakeover(TransitionFilter filter, RemoteTransition remote) { + handleDeath(remote.asBinder(), null /* finishCallback */); + mTakeoverFilters.add(new Pair<>(filter, remote)); + } + void removeFiltered(RemoteTransition remote) { boolean removed = false; - for (int i = mFilters.size() - 1; i >= 0; --i) { - if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) { - mFilters.remove(i); - removed = true; + for (ArrayList<Pair<TransitionFilter, RemoteTransition>> filters + : Arrays.asList(mFilters, mTakeoverFilters)) { + for (int i = filters.size() - 1; i >= 0; --i) { + if (filters.get(i).second.asBinder().equals(remote.asBinder())) { + filters.remove(i); + removed = true; + } } } + if (removed) { unhandleDeath(remote.asBinder(), null /* finishCallback */); } @@ -237,6 +253,47 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } } + @Nullable + @Override + public Transitions.TransitionHandler getHandlerForTakeover( + @NonNull IBinder transition, @NonNull TransitionInfo info) { + if (!returnAnimationFrameworkLibrary()) { + return null; + } + + for (Pair<TransitionFilter, RemoteTransition> registered : mTakeoverFilters) { + if (registered.first.matches(info)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "Found matching remote to takeover (#%d)", info.getDebugId()); + + OneShotRemoteHandler oneShot = + new OneShotRemoteHandler(mMainExecutor, registered.second); + oneShot.setTransition(transition); + return oneShot; + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "No matching remote found to takeover (#%d)", info.getDebugId()); + return null; + } + + @Override + public boolean takeOverAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction transaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowAnimationState[] states) { + Transitions.TransitionHandler handler = getHandlerForTakeover(transition, info); + if (handler == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "Take over request failed: no matching remote for (#%d)", info.getDebugId()); + return false; + } + ((OneShotRemoteHandler) handler).setTransition(transition); + return handler.takeOverAnimation(transition, info, transaction, finishCallback, states); + } + @Override @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -284,6 +341,34 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } } + void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + + pw.println(prefix + "Registered Remotes:"); + if (mFilters.isEmpty()) { + pw.println(innerPrefix + "none"); + } else { + for (Pair<TransitionFilter, RemoteTransition> entry : mFilters) { + dumpRemote(pw, innerPrefix, entry.second); + } + } + + pw.println(prefix + "Registered Takeover Remotes:"); + if (mTakeoverFilters.isEmpty()) { + pw.println(innerPrefix + "none"); + } else { + for (Pair<TransitionFilter, RemoteTransition> entry : mTakeoverFilters) { + dumpRemote(pw, innerPrefix, entry.second); + } + } + } + + private void dumpRemote(@NonNull PrintWriter pw, String prefix, RemoteTransition remote) { + pw.print(prefix); + pw.print(remote.getDebugName()); + pw.println(" (" + Integer.toHexString(System.identityHashCode(remote)) + ")"); + } + /** NOTE: binder deaths can alter the filter order */ private class RemoteDeathHandler implements IBinder.DeathRecipient { private final IBinder mRemote; 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..6224543516fa 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 @@ -31,10 +31,10 @@ import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; -import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; @@ -43,9 +43,11 @@ import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SH import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityTaskManager; +import android.app.AppGlobals; import android.app.IApplicationThread; import android.content.ContentResolver; import android.content.Context; +import android.content.pm.PackageManager; import android.database.ContentObserver; import android.os.Handler; import android.os.IBinder; @@ -58,10 +60,12 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.window.ITransitionPlayer; import android.window.RemoteTransition; +import android.window.TaskFragmentOrganizer; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; +import android.window.WindowAnimationState; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; @@ -76,10 +80,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; @@ -121,8 +128,7 @@ public class Transitions implements RemoteCallable<Transitions>, static final String TAG = "ShellTransitions"; /** Set to {@code true} to enable shell transitions. */ - public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + public static final boolean ENABLE_SHELL_TRANSITIONS = getShellTransitEnabled(); public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); @@ -177,6 +183,13 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition to resize PiP task. */ public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16; + /** + * The task fragment drag resize transition used by activity embedding. + */ + public static final int TRANSIT_TASK_FRAGMENT_DRAG_RESIZE = + // TRANSIT_FIRST_CUSTOM + 17 + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE; + private final ShellTaskOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; @@ -416,12 +429,24 @@ public class Transitions implements RemoteCallable<Transitions>, mHandlers.set(0, handler); } - /** Register a remote transition to be used when `filter` matches an incoming transition */ + /** + * Register a remote transition to be used for all operations except takeovers when `filter` + * matches an incoming transition. + */ public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); } + /** + * Register a remote transition to be used for all operations except takeovers when `filter` + * matches an incoming transition. + */ + public void registerRemoteForTakeover(@NonNull TransitionFilter filter, + @NonNull RemoteTransition remoteTransition) { + mRemoteTransitionHandler.addFilteredForTakeover(filter, remoteTransition); + } + /** Unregisters a remote transition and all associated filters */ public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.removeFiltered(remoteTransition); @@ -495,6 +520,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 +532,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; } @@ -538,15 +566,15 @@ public class Transitions implements RemoteCallable<Transitions>, final int mode = change.getMode(); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - if (isOpening - // This is for when an activity launches while a different transition is - // collecting. - || change.hasFlags(FLAG_MOVED_TO_TOP)) { + if (isOpening) { // put on top return zSplitLine + numChanges - i; - } else { + } else if (isClosing) { // put on bottom return zSplitLine - i; + } else { + // maintain relative ordering (put all changes in the animating layer) + return zSplitLine + numChanges - i; } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { if (isOpening) { @@ -1181,6 +1209,29 @@ public class Transitions implements RemoteCallable<Transitions>, } /** + * Checks whether a handler exists capable of taking over the given transition, and returns it. + * Otherwise it returns null. + */ + @Nullable + public TransitionHandler getHandlerForTakeover( + @NonNull IBinder transition, @NonNull TransitionInfo info) { + if (!returnAnimationFrameworkLibrary()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "Trying to get a handler for takeover but the flag is disabled"); + return null; + } + + for (TransitionHandler handler : mHandlers) { + TransitionHandler candidate = handler.getHandlerForTakeover(transition, info); + if (candidate != null) { + return candidate; + } + } + + return null; + } + + /** * Finish running animations (almost) immediately when a SLEEP transition comes in. We use this * as both a way to reduce unnecessary work (animations not visible while screen off) and as a * failsafe to unblock "stuck" animations (in particular remote animations). @@ -1322,6 +1373,49 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull TransitionFinishCallback finishCallback) { } /** + * Checks whether this handler is capable of taking over a transition matching `info`. + * {@link TransitionHandler#takeOverAnimation(IBinder, TransitionInfo, + * SurfaceControl.Transaction, TransitionFinishCallback, WindowAnimationState[])} is + * guaranteed to succeed if called on the handler returned by this method. + * + * Note that the handler returned by this method can either be itself, or a different one + * selected by this handler to take care of the transition on its behalf. + * + * @param transition The transition that should be taken over. + * @param info Information about the transition to be taken over. + * @return A handler capable of taking over a matching transition, or null. + */ + @Nullable + default TransitionHandler getHandlerForTakeover( + @NonNull IBinder transition, @NonNull TransitionInfo info) { + return null; + } + + /** + * Attempt to take over a running transition. This must succeed if this handler was returned + * by {@link TransitionHandler#getHandlerForTakeover(IBinder, TransitionInfo)}. + * + * @param transition The transition that should be taken over. + * @param info Information about the what is changing in the transition. + * @param transaction Contains surface changes that resulted from the transition. Any + * additional changes should be added to this transaction and committed + * inside this method. + * @param finishCallback Call this at the end of the animation, if the take-over succeeds. + * Note that this will be called instead of the callback originally + * passed to startAnimation(), so the caller should make sure all + * necessary cleanup happens here. This MUST be called on main thread. + * @param states The animation states of the transition's window at the time this method was + * called. + * @return true if the transition was taken over, false if not. + */ + default boolean takeOverAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction transaction, + @NonNull TransitionFinishCallback finishCallback, + @NonNull WindowAnimationState[] states) { + return false; + } + + /** * Potentially handles a startTransition request. * * @param transition The transition whose start is being requested. @@ -1405,6 +1499,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)); } @@ -1424,16 +1520,21 @@ public class Transitions implements RemoteCallable<Transitions>, @Override public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { - mMainExecutor.execute(() -> { - mRemoteTransitionHandler.addFiltered(filter, remoteTransition); - }); + mMainExecutor.execute( + () -> mRemoteTransitionHandler.addFiltered(filter, remoteTransition)); + } + + @Override + public void registerRemoteForTakeover(@NonNull TransitionFilter filter, + @NonNull RemoteTransition remoteTransition) { + mMainExecutor.execute(() -> mRemoteTransitionHandler.addFilteredForTakeover( + filter, remoteTransition)); } @Override public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { - mMainExecutor.execute(() -> { - mRemoteTransitionHandler.removeFiltered(remoteTransition); - }); + mMainExecutor.execute( + () -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); } } @@ -1462,17 +1563,23 @@ public class Transitions implements RemoteCallable<Transitions>, public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "registerRemote", - (transitions) -> { - transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition); - }); + (transitions) -> transitions.mRemoteTransitionHandler.addFiltered( + filter, remoteTransition)); + } + + @Override + public void registerRemoteForTakeover(@NonNull TransitionFilter filter, + @NonNull RemoteTransition remoteTransition) { + executeRemoteCallWithTaskPermission(mTransitions, "registerRemoteForTakeover", + (transitions) -> transitions.mRemoteTransitionHandler.addFilteredForTakeover( + filter, remoteTransition)); } @Override public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote", - (transitions) -> { - transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition); - }); + (transitions) -> + transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition)); } @Override @@ -1557,6 +1664,8 @@ public class Transitions implements RemoteCallable<Transitions>, pw.println(" (" + Integer.toHexString(System.identityHashCode(handler)) + ")"); } + mRemoteTransitionHandler.dump(pw, prefix); + pw.println(prefix + "Observers:"); for (TransitionObserver observer : mObservers) { pw.print(innerPrefix); @@ -1609,4 +1718,16 @@ public class Transitions implements RemoteCallable<Transitions>, } } } + + private static boolean getShellTransitEnabled() { + try { + if (AppGlobals.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE, 0)) { + return SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + } + } catch (RemoteException re) { + Log.w(TAG, "Error getting system features"); + } + return true; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java index fa331af267fa..6adbe4f7ce92 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java @@ -16,7 +16,12 @@ package com.android.wm.shell.transition.tracing; -import android.internal.perfetto.protos.PerfettoTrace; +import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT; + +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping; +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings; +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition; +import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket; import android.os.SystemClock; import android.os.Trace; import android.tracing.perfetto.DataSourceParams; @@ -44,7 +49,12 @@ public class PerfettoTransitionTracer implements TransitionTracer { public PerfettoTransitionTracer() { Producer.init(InitArguments.DEFAULTS); - mDataSource.register(DataSourceParams.DEFAULTS); + DataSourceParams params = + new DataSourceParams.Builder() + .setBufferExhaustedPolicy( + PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT) + .build(); + mDataSource.register(params); } /** @@ -72,11 +82,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { final int handlerId = getHandlerId(handler); final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, transitionId); - os.write(PerfettoTrace.ShellTransition.DISPATCH_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, transitionId); + os.write(ShellTransition.DISPATCH_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.HANDLER, handlerId); + os.write(ShellTransition.HANDLER, handlerId); os.end(token); }); } @@ -117,11 +127,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); - os.write(PerfettoTrace.ShellTransition.MERGE_REQUEST_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, mergeRequestedTransitionId); + os.write(ShellTransition.MERGE_REQUEST_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.write(ShellTransition.MERGE_TARGET, playingTransitionId); os.end(token); }); } @@ -149,11 +159,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogMerged(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); - os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, mergeRequestedTransitionId); + os.write(ShellTransition.MERGE_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.write(ShellTransition.MERGE_TARGET, playingTransitionId); os.end(token); }); } @@ -180,9 +190,9 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogAborted(int transitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, transitionId); - os.write(PerfettoTrace.ShellTransition.SHELL_ABORT_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, transitionId); + os.write(ShellTransition.SHELL_ABORT_TIME_NS, SystemClock.elapsedRealtimeNanos()); os.end(token); }); @@ -196,20 +206,18 @@ public class PerfettoTransitionTracer implements TransitionTracer { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long mappingsToken = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS); + final long mappingsToken = os.start(TracePacket.SHELL_HANDLER_MAPPINGS); for (Map.Entry<String, Integer> entry : mHandlerMapping.entrySet()) { final String handler = entry.getKey(); final int handlerId = entry.getValue(); - final long mappingEntryToken = os.start(PerfettoTrace.ShellHandlerMappings.MAPPING); - os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerId); - os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler); + final long mappingEntryToken = os.start(ShellHandlerMappings.MAPPING); + os.write(ShellHandlerMapping.ID, handlerId); + os.write(ShellHandlerMapping.NAME, handler); os.end(mappingEntryToken); } os.end(mappingsToken); - - ctx.flush(); }); } } 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..e85cb6400000 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) { @@ -107,6 +119,21 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + // A task vanishing doesn't necessarily mean the task was closed, it could also mean its + // windowing mode changed. We're only interested in closing tasks so checking whether + // its info still exists in the task organizer is one way to disambiguate. + final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null; + if (closed) { + // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition + // changes happen, but there are certain cases in which closing tasks aren't included + // in transitions, such as when a non-visible task is closed. See b/296921167. + // Destroy the decoration here in case the lack of transition missed it. + destroyWindowDecoration(taskInfo); + } + } + + @Override public void onTaskChanging( RunningTaskInfo taskInfo, SurfaceControl taskSurface, @@ -156,10 +183,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 +236,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 +279,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 +339,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 +356,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..6671391efdeb 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; @@ -190,7 +200,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mRelayoutParams.mShadowRadiusId = shadowRadiusID; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; - mRelayoutParams.mAllowCaptionInputFallthrough = false; relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo @@ -218,7 +227,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mHandler, mChoreographer, mDisplay.getDisplayId(), - 0 /* taskCornerRadius */, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -230,12 +238,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..9afb057ffbe5 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.windowdecor; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -25,17 +26,18 @@ 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 static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; -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 +73,8 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -80,10 +84,12 @@ 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.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.shared.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -119,9 +125,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 +134,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 +201,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + new SparseArray<>()); } @VisibleForTesting @@ -219,7 +224,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 +237,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 +245,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTransactionFactory = transactionFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mInputManager = mContext.getSystemService(InputManager.class); + mWindowDecorByTaskId = windowDecorByTaskId; shellInit.addInitCallback(this::onInit, this); } @@ -248,8 +255,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()); @@ -269,12 +276,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() { @Override public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { - if (visible) { + if (visible && stage != STAGE_TYPE_UNDEFINED) { DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); - if (decor != null && DesktopModeStatus.isEnabled() - && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { - mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo)); + if (decor == null || !DesktopModeStatus.canEnterDesktopMode(mContext) + || decor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { + return; } + mDesktopTasksController.moveToSplit(decor.mTaskInfo); } } }); @@ -305,6 +313,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + // A task vanishing doesn't necessarily mean the task was closed, it could also mean its + // windowing mode changed. We're only interested in closing tasks so checking whether + // its info still exists in the task organizer is one way to disambiguate. + final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null; + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Task Vanished: #%d closed=%b", taskInfo.taskId, closed); + if (closed) { + // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition + // changes happen, but there are certain cases in which closing tasks aren't included + // in transitions, such as when a non-visible task is closed. See b/296921167. + // Destroy the decoration here in case the lack of transition missed it. + destroyWindowDecoration(taskInfo); + } + } + + @Override public void onTaskChanging( RunningTaskInfo taskInfo, SurfaceControl taskSurface, @@ -340,8 +364,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 +372,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 +429,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 +443,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 +455,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 +511,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 +524,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 +575,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 +592,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 +602,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void moveTaskToFront(RunningTaskInfo taskInfo) { if (!taskInfo.isFocused) { - mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo)); + mDesktopTasksController.moveTaskToFront(taskInfo); } } @@ -584,7 +614,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { public boolean handleMotionEvent(@Nullable View v, MotionEvent e) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final RunningTaskInfo taskInfo = decoration.mTaskInfo; - if (DesktopModeStatus.isEnabled() + if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { return false; } @@ -606,7 +636,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 +646,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 +671,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 +701,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; } } @@ -764,7 +791,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) { final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { if (!mInImmersiveMode && (relevantDecor == null || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM || mTransitionDragActive)) { @@ -773,7 +800,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } handleEventOutsideCaption(ev, relevantDecor); // Prevent status bar from reacting to a caption drag. - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { if (mTransitionDragActive) { inputMonitor.pilferPointers(); } @@ -793,7 +820,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 +837,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.canEnterDesktopMode(mContext)) { + // 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,72 +934,13 @@ 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) { + // If we are mid-transition, dragged task's decor is always relevant. + final int draggedTaskId = mDesktopTasksController.getDraggingTaskId(); + if (draggedTaskId != INVALID_TASK_ID) { + return mWindowDecorByTaskId.get(draggedTaskId); + } final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null) { return null; @@ -1048,12 +1029,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && taskInfo.isFocused) { return false; } - return DesktopModeStatus.isEnabled() + if (Flags.enableDesktopWindowingModalsPolicy() + && isSingleTopActivityTranslucent(taskInfo)) { + return false; + } + return DesktopModeStatus.canEnterDesktopMode(mContext) + && !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; + && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); } private void createWindowDecoration( @@ -1078,21 +1062,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue, mRootTaskDisplayAreaOrganizer); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - 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); } @@ -1125,7 +1106,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + "DesktopModeWindowDecorViewModel"); - pw.println(innerPrefix + "DesktopModeStatus=" + DesktopModeStatus.isEnabled()); + pw.println(innerPrefix + "DesktopModeStatus=" + + DesktopModeStatus.canEnterDesktopMode(mContext)); pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive); pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay); pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); @@ -1181,12 +1163,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); } } @@ -1225,7 +1207,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final boolean inImmersiveMode = !source.isVisible(); // Calls WindowDecoration#relayout if decoration visibility needs to be updated if (inImmersiveMode != mInImmersiveMode) { - decor.relayout(decor.mTaskInfo); + if (Flags.enableDesktopWindowingImmersiveHandleHiding()) { + decor.relayout(decor.mTaskInfo); + } mInImmersiveMode = inImmersiveMode; } 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..4d4dc3c72420 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,11 +44,15 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.Trace; +import android.util.Log; +import android.util.Size; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManager; import android.widget.ImageButton; import android.window.WindowContainerTransaction; @@ -54,8 +66,8 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; 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.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; @@ -97,9 +109,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private MaximizeMenu mMaximizeMenu; private ResizeVeil mResizeVeil; - - private Drawable mAppIconDrawable; private Bitmap mAppIconBitmap; + private Bitmap mResizeVeilBitmap; + private CharSequence mAppName; private ExclusionRegionListener mExclusionRegionListener; @@ -144,13 +156,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, surfaceControlViewHostFactory); - mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; - - loadAppInfo(); } void setCaptionListeners( @@ -196,6 +205,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (isHandleMenuActive()) { mHandleMenu.relayout(startT); } @@ -207,16 +217,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); + Trace.beginSection("DesktopModeWindowDecoration#relayout-inner"); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + Trace.endSection(); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo + Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT"); mTaskOrganizer.applyTransaction(wct); + Trace.endSection(); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. + Trace.endSection(); // DesktopModeWindowDecoration#relayout return; } + if (oldRootView != mResult.mRootView) { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) { mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder( @@ -226,6 +242,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ); } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_controls_window_decor) { + loadAppInfoIfNeeded(); mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( mResult.mRootView, mOnCaptionTouchListener, @@ -244,7 +261,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin throw new IllegalArgumentException("Unexpected layout resource id"); } } + Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); mWindowDecorViewHolder.bindData(mTaskInfo); + Trace.endSection(); if (!mTaskInfo.isFocused) { closeHandleMenu(); @@ -260,37 +279,37 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateExclusionRegion(); } closeDragResizeListener(); + Trace.endSection(); // DesktopModeWindowDecoration#relayout return; } if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { closeDragResizeListener(); + Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener"); mDragResizeListener = new DragResizeInputListener( mContext, mHandler, mChoreographer, mDisplay.getDisplayId(), - mRelayoutParams.mCornerRadius, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, mSurfaceControlTransactionSupplier, mDisplayController); + Trace.endSection(); } final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .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(); } @@ -302,6 +321,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); } } + Trace.endSection(); // DesktopModeWindowDecoration#relayout } @VisibleForTesting @@ -318,11 +338,13 @@ 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 (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + // 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.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } // 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). @@ -337,6 +359,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end; controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; relayoutParams.mOccludingCaptionElements.add(controlsElement); + } else if (captionLayoutId == R.layout.desktop_mode_focused_window_decor) { + // The focused decor (fullscreen/split) does not need to handle input because input in + // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel. + relayoutParams.mInputFeatures + |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; } if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) { relayoutParams.mShadowRadiusId = taskInfo.isFocused @@ -399,7 +426,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,21 +447,50 @@ 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() { - PackageManager pm = mContext.getApplicationContext().getPackageManager(); - final IconProvider provider = new IconProvider(mContext); - mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo); - 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; - mAppName = pm.getApplicationLabel(applicationInfo); + private void loadAppInfoIfNeeded() { + // TODO(b/337370277): move this to another thread. + try { + Trace.beginSection("DesktopModeWindowDecoration#loadAppInfoIfNeeded"); + if (mAppIconBitmap != null && mAppName != null) { + return; + } + 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); + final Drawable appIconDrawable = provider.getIcon(activityInfo); + final BaseIconFactory headerIconFactory = createIconFactory(mContext, + R.dimen.desktop_mode_caption_icon_radius); + mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT); + + final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext, + R.dimen.desktop_mode_resize_veil_icon_size); + mResizeVeilBitmap = resizeVeilIconFactory + .createScaledBitmap(appIconDrawable, MODE_DEFAULT); + + final ApplicationInfo applicationInfo = activityInfo.applicationInfo; + mAppName = pm.getApplicationLabel(applicationInfo); + } finally { + Trace.endSection(); + } + } + + private BaseIconFactory createIconFactory(Context context, int dimensions) { + final Resources resources = context.getResources(); + final int densityDpi = resources.getDisplayMetrics().densityDpi; + final int iconSize = resources.getDimensionPixelSize(dimensions); + return new BaseIconFactory(context, densityDpi, iconSize); } private void closeDragResizeListener() { @@ -448,15 +505,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create the resize veil for this task. Note the veil's visibility is View.GONE by default * until a resize event calls showResizeVeil below. */ - void createResizeVeil() { - mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, - mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier); + private void createResizeVeilIfNeeded() { + if (mResizeVeil != null) return; + loadAppInfoIfNeeded(); + mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, mTaskInfo, + mTaskSurface, mSurfaceControlTransactionSupplier); } /** * Show the resize veil. */ public void showResizeVeil(Rect taskBounds) { + createResizeVeilIfNeeded(); mResizeVeil.showVeil(mTaskSurface, taskBounds); } @@ -464,6 +524,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Show the resize veil. */ public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) { + createResizeVeilIfNeeded(); mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */); } @@ -498,6 +559,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(); @@ -582,13 +644,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display handle menu window. */ void createHandleMenu() { + loadAppInfoIfNeeded(); mHandleMenu = new HandleMenu.Builder(this) .setAppIcon(mAppIconBitmap) .setAppName(mAppName) .setOnClickListener(mOnCaptionButtonClickListener) .setOnTouchListener(mOnCaptionTouchListener) .setLayoutId(mRelayoutParams.mLayoutResId) - .setWindowingButtonsVisible(DesktopModeStatus.isEnabled()) + .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) .setCaptionHeight(mResult.mCaptionHeight) .build(); mWindowDecorViewHolder.onHandleMenuOpened(); @@ -606,10 +669,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } @Override - void releaseViews() { + void releaseViews(WindowContainerTransaction wct) { closeHandleMenu(); closeMaximizeMenu(); - super.releaseViews(); + super.releaseViews(wct); } /** @@ -706,27 +769,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 +850,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 +887,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..5379ca6cd51d 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 @@ -24,13 +24,15 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; -import static com.android.input.flags.Flags.enablePointerChoreographer; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; 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 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; @@ -51,9 +54,11 @@ import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; import android.window.InputTransferToken; +import com.android.internal.protolog.common.ProtoLog; 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 +70,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 +91,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 +114,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 +150,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 +161,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 +195,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 +210,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 +237,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 +266,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 +309,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,27 +388,29 @@ 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); + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, rawX, rawY); // Increase the input sink region to cover the whole screen; this is to // prevent input and focus from going to other tasks during a drag resize. updateInputSinkRegionForDrag(mDragStartTaskBounds); result = true; + } else { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action down, but ignore event", TAG); } break; } @@ -447,7 +429,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 +436,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 +461,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) { @@ -629,14 +505,20 @@ class DragResizeInputListener implements AutoCloseable { // where views in the task can receive input events because we can't set touch regions // of input sinks to have rounded corners. if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) { - if (enablePointerChoreographer()) { - mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType), - displayId, deviceId, pointerId, mInputChannel.getToken()); - } else { - mInputManager.setPointerIconType(cursorType); - } + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: update pointer icon from %d to %d", + TAG, mLastCursorType, cursorType); + mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType), + displayId, deviceId, pointerId, mInputChannel.getToken()); 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..4f513f0a0fd8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -0,0 +1,522 @@ +/* + * 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 androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; + +import java.util.Objects; + +/** + * Geometry for a drag resize region for a particular window. + */ +final class DragResizeWindowGeometry { + // TODO(b/337264971) clean up when no longer needed + @VisibleForTesting static final boolean DEBUG = true; + // The additional width to apply to edge resize bounds just for logging when a touch is + // close. + @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15; + 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 TaskEdges mTaskEdges; + // Extra-large edge bounds for logging to help debug when an edge resize is ignored. + private final @Nullable TaskEdges mDebugTaskEdges; + + 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. + mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness); + if (DEBUG) { + mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER); + } else { + mDebugTaskEdges = null; + } + } + + /** + * 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. + if (inDebugMode()) { + // Use the larger edge sizes if we are debugging, to be able to log if we ignored a + // touch due to the size of the edge region. + mDebugTaskEdges.union(region); + } else { + mTaskEdges.union(region); + } + + 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) { + if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y) + && !mTaskEdges.contains((int) x, (int) y))) { + return CTRL_TYPE_UNDEFINED; + } + 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) + && (inDebugMode() + ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges) + : this.mTaskEdges.equals(other.mTaskEdges)); + } + + @Override + public int hashCode() { + return Objects.hash( + mTaskCornerRadius, + mTaskSize, + mResizeHandleThickness, + mFineTaskCorners, + mLargeTaskCorners, + (inDebugMode() ? mDebugTaskEdges : mTaskEdges)); + } + + private boolean inDebugMode() { + return DEBUG && mDebugTaskEdges != null; + } + + /** + * 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); + } + } + + /** + * Representation of the drag resize regions at the edges of the window. + */ + private static class TaskEdges { + private final @NonNull Rect mTopEdgeBounds; + private final @NonNull Rect mLeftEdgeBounds; + private final @NonNull Rect mRightEdgeBounds; + private final @NonNull Rect mBottomEdgeBounds; + private final @NonNull Region mRegion; + + private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) { + // Save touch areas for each edge. + mTopEdgeBounds = new Rect( + -resizeHandleThickness, + -resizeHandleThickness, + taskSize.getWidth() + resizeHandleThickness, + 0); + mLeftEdgeBounds = new Rect( + -resizeHandleThickness, + 0, + 0, + taskSize.getHeight()); + mRightEdgeBounds = new Rect( + taskSize.getWidth(), + 0, + taskSize.getWidth() + resizeHandleThickness, + taskSize.getHeight()); + mBottomEdgeBounds = new Rect( + -resizeHandleThickness, + taskSize.getHeight(), + taskSize.getWidth() + resizeHandleThickness, + taskSize.getHeight() + resizeHandleThickness); + + mRegion = new Region(); + mRegion.union(mTopEdgeBounds); + mRegion.union(mLeftEdgeBounds); + mRegion.union(mRightEdgeBounds); + mRegion.union(mBottomEdgeBounds); + } + + /** + * Returns {@code true} if the edges contain the given point. + */ + private boolean contains(int x, int y) { + return mRegion.contains(x, y); + } + + /** + * Updates the region to include all four corners. + */ + private void union(Region region) { + region.union(mTopEdgeBounds); + region.union(mLeftEdgeBounds); + region.union(mRightEdgeBounds); + region.union(mBottomEdgeBounds); + } + + @Override + public String toString() { + return "TaskEdges for the" + + " top " + mTopEdgeBounds + + " left " + mLeftEdgeBounds + + " right " + mRightEdgeBounds + + " bottom " + mBottomEdgeBounds; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof TaskEdges other)) return false; + + return 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( + mTopEdgeBounds, + mLeftEdgeBounds, + mRightEdgeBounds, + mBottomEdgeBounds); + } + } +} 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..22f0adc42f5d 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 @@ -16,6 +16,9 @@ package com.android.wm.shell.windowdecor +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context @@ -30,12 +33,21 @@ import android.view.SurfaceControlViewHost import android.view.View.OnClickListener import android.view.View.OnGenericMotionListener import android.view.View.OnTouchListener +import android.view.View.SCALE_Y +import android.view.View.TRANSLATION_Y +import android.view.View.TRANSLATION_Z import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.Button +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView 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.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.windowdecor.WindowDecoration.AdditionalWindow @@ -61,14 +73,19 @@ class MaximizeMenu( private var maximizeMenu: AdditionalWindow? = null private lateinit var viewHost: SurfaceControlViewHost private lateinit var leash: SurfaceControl - private val shadowRadius = loadDimensionPixelSize( - R.dimen.desktop_mode_maximize_menu_shadow_radius - ).toFloat() + private val openMenuAnimatorSet = AnimatorSet() private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width) private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) + private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) + + 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) { @@ -81,10 +98,12 @@ class MaximizeMenu( if (maximizeMenu != null) return createMaximizeMenu() setupMaximizeMenu() + animateOpenMenu() } /** Closes the maximize window and releases its view. */ fun close() { + openMenuAnimatorSet.cancel() maximizeMenu?.releaseView() maximizeMenu = null } @@ -124,8 +143,6 @@ class MaximizeMenu( // Bring menu to front when open t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU) .setPosition(leash, menuPosition.x, menuPosition.y) - .setWindowCrop(leash, menuWidth, menuHeight) - .setShadowRadius(leash, shadowRadius) .setCornerRadius(leash, cornerRadius) .show(leash) maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier) @@ -136,6 +153,77 @@ class MaximizeMenu( } } + private fun animateOpenMenu() { + val viewHost = maximizeMenu?.mWindowViewHost + val maximizeMenuView = viewHost?.view ?: return + val maximizeWindowText = maximizeMenuView.requireViewById<TextView>( + R.id.maximize_menu_maximize_window_text) + val snapWindowText = maximizeMenuView.requireViewById<TextView>( + R.id.maximize_menu_snap_window_text) + + openMenuAnimatorSet.playTogether( + ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) + .apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + }, + ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f) + .apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + addUpdateListener { + // Animate padding so that controls stay pinned to the bottom of + // the menu. + val value = animatedValue as Float + val topPadding = menuPadding - + ((1 - value) * menuHeight).toInt() + maximizeMenuView.setPadding(menuPadding, topPadding, + menuPadding, menuPadding) + } + }, + ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + addUpdateListener { + // Scale up the children of the maximize menu so that the menu + // scale is cancelled out and only the background is scaled. + val value = animatedValue as Float + maximizeButtonLayout.scaleY = value + snapButtonsLayout.scaleY = value + maximizeWindowText.scaleY = value + snapWindowText.scaleY = value + } + }, + ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y, + (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + }, + ObjectAnimator.ofInt(maximizeMenuView.background, "alpha", + MAX_DRAWABLE_ALPHA_VALUE).apply { + duration = ALPHA_ANIMATION_DURATION_MS + }, + ValueAnimator.ofFloat(0f, 1f) + .apply { + duration = ALPHA_ANIMATION_DURATION_MS + startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + addUpdateListener { + val value = animatedValue as Float + maximizeButtonLayout.alpha = value + snapButtonsLayout.alpha = value + maximizeWindowText.alpha = value + snapWindowText.alpha = value + } + }, + ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION) + .apply { + duration = ELEVATION_ANIMATION_DURATION_MS + startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + } + ) + openMenuAnimatorSet.start() + } + private fun loadDimensionPixelSize(resourceId: Int): Int { return if (resourceId == Resources.ID_NULL) { 0 @@ -150,23 +238,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 +278,85 @@ 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 { + // Open menu animation constants + private const val ALPHA_ANIMATION_DURATION_MS = 50L + private const val MAX_DRAWABLE_ALPHA_VALUE = 255 + private const val STARTING_MENU_HEIGHT_SCALE = 0.8f + private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L + private const val ELEVATION_ANIMATION_DURATION_MS = 50L + private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L + private const val MENU_Z_TRANSLATION = 1f 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..93e2a21c6b02 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,21 @@ 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.Bitmap; +import android.graphics.Color; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.drawable.Drawable; +import android.os.Trace; 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 +42,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 +50,147 @@ 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 Drawable mAppIcon; + private final SurfaceControlBuilderFactory mSurfaceControlBuilderFactory; + private final WindowDecoration.SurfaceControlViewHostFactory mSurfaceControlViewHostFactory; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + private final Bitmap 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, + Bitmap 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, + Bitmap 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; + } + Trace.beginSection("ResizeVeil#setupResizeVeil"); + 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(); - View v = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null); + mIconSurface = mSurfaceControlBuilderFactory + .create("Resize veil icon of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(mVeilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build(); + + 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.setImageBitmap(mAppIcon); - t.setPosition(mVeilSurface, 0, 0) - .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) - .apply(); - Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 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); + Trace.endSection(); + } + + private boolean obtainDisplayOrRegisterListener() { + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + if (mDisplay == null) { + mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); + return false; + } + return true; } /** @@ -114,58 +204,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 +302,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 +314,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 +330,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 +383,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 +396,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/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 01a6012ea314..1563259f4a1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -67,6 +67,14 @@ public interface WindowDecorViewModel { void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo); /** + * Notifies a task has vanished, which can mean that the task changed windowing mode or was + * removed. + * + * @param taskInfo the task info of the task + */ + void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo); + + /** * Notifies a transition is about to start about the given task to give the window decoration a * chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates * a window decoration if one does not exist but is required. 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..541825437c86 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 @@ -18,6 +18,8 @@ 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.view.WindowInsets.Type.captionBar; +import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; import android.annotation.NonNull; @@ -33,6 +35,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.Binder; +import android.os.Trace; import android.view.Display; import android.view.InsetsSource; import android.view.InsetsState; @@ -40,21 +43,22 @@ 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; import android.window.SurfaceSyncGroup; import android.window.TaskConstants; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Supplier; /** @@ -131,8 +135,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> TaskDragResizer mTaskDragResizer; private boolean mIsCaptionVisible; + /** The most recent set of insets applied to this window decoration. */ + private WindowDecorationInsets mWindowDecorationInsets; private final Binder mOwner = new Binder(); - private final Rect mCaptionInsetsRect = new Rect(); private final float[] mTmpColor = new float[3]; WindowDecoration( @@ -203,7 +208,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mLayoutResId = params.mLayoutResId; if (!mTaskInfo.isVisible) { - releaseViews(); + releaseViews(wct); finishT.hide(mTaskSurface); return; } @@ -226,7 +231,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode) { - releaseViews(); + releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { outResult.mRootView = null; @@ -293,60 +298,57 @@ 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. - 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.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. + final Rect captionInsetsRect = new Rect(taskBounds); + captionInsetsRect.bottom = captionInsetsRect.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.hasInputFeatureSpy()) { + outResult.mCustomizableCaptionRegion.set(captionInsetsRect); + } + 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, captionInsetsRect); + // Subtract the regions used by the caption elements, the rest is + // customizable. + if (params.hasInputFeatureSpy()) { + 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()); + } + + final WindowDecorationInsets newInsets = new WindowDecorationInsets( + mTaskInfo.token, mOwner, captionInsetsRect, boundingRects); + if (!newInsets.equals(mWindowDecorationInsets)) { + // Add or update this caption as an insets source. + mWindowDecorationInsets = newInsets; + mWindowDecorationInsets.addOrUpdate(wct); } } else { - startT.hide(mCaptionContainerSurface); + if (mWindowDecorationInsets != null) { + mWindowDecorationInsets.remove(wct); + mWindowDecorationInsets = null; + } } // Task surface itself @@ -383,6 +385,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> startT.unsetColor(mTaskSurface); } + Trace.beginSection("CaptionViewHostLayout"); if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. @@ -399,24 +402,25 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); lp.setTitle("Caption of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); - if (params.mAllowCaptionInputFallthrough) { - lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; - } else { - lp.inputFeatures &= ~WindowManager.LayoutParams.INPUT_FEATURE_SPY; - } + lp.inputFeatures = params.mInputFeatures; if (mViewHost == null) { + Trace.beginSection("CaptionViewHostLayout-new"); mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, mCaptionWindowManager); if (params.mApplyStartTransactionOnDraw) { mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT); } mViewHost.setView(outResult.mRootView, lp); + Trace.endSection(); } else { + Trace.beginSection("CaptionViewHostLayout-relayout"); if (params.mApplyStartTransactionOnDraw) { mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT); } mViewHost.relayout(lp); + Trace.endSection(); } + Trace.endSection(); // CaptionViewHostLayout } private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, @@ -487,7 +491,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return true; } - void releaseViews() { + void releaseViews(WindowContainerTransaction wct) { if (mViewHost != null) { mViewHost.release(); mViewHost = null; @@ -513,19 +517,21 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> t.apply(); } - final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get(); - wct.removeInsetsSource(mTaskInfo.token, - mOwner, 0 /* index */, WindowInsets.Type.captionBar()); - wct.removeInsetsSource(mTaskInfo.token, - mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures()); - mTaskOrganizer.applyTransaction(wct); + if (mWindowDecorationInsets != null) { + mWindowDecorationInsets.remove(wct); + mWindowDecorationInsets = null; + } } @Override public void close() { + Trace.beginSection("WindowDecoration#close"); mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); - releaseViews(); + final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get(); + releaseViews(wct); + mTaskOrganizer.applyTransaction(wct); mTaskSurface.release(); + Trace.endSection(); } static int loadDimensionPixelSize(Resources resources, int resourceId) { @@ -594,15 +600,18 @@ 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; } final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId); final Rect captionInsets = new Rect(0, 0, 0, captionHeight); - wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(), - captionInsets, null /* boundingRects */); + final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token, + mOwner, captionInsets, null /* boundingRets */); + if (!newInsets.equals(mWindowDecorationInsets)) { + mWindowDecorationInsets = newInsets; + mWindowDecorationInsets.addOrUpdate(wct); + } } static class RelayoutParams { @@ -611,7 +620,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionHeightId; int mCaptionWidthId; final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>(); - boolean mAllowCaptionInputFallthrough; + int mInputFeatures; int mShadowRadiusId; int mCornerRadius; @@ -626,7 +635,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionHeightId = Resources.ID_NULL; mCaptionWidthId = Resources.ID_NULL; mOccludingCaptionElements.clear(); - mAllowCaptionInputFallthrough = false; + mInputFeatures = 0; mShadowRadiusId = Resources.ID_NULL; mCornerRadius = 0; @@ -636,6 +645,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mWindowDecorConfig = null; } + boolean hasInputFeatureSpy() { + return (mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_SPY) != 0; + } + /** * Describes elements within the caption bar that could occlude app content, and should be * sent as bounding rectangles to the insets system. @@ -674,6 +687,51 @@ 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); + } + } + + private static class WindowDecorationInsets { + private static final int INDEX = 0; + private final WindowContainerToken mToken; + private final Binder mOwner; + private final Rect mFrame; + private final Rect[] mBoundingRects; + + private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame, + Rect[] boundingRects) { + mToken = token; + mOwner = owner; + mFrame = frame; + mBoundingRects = boundingRects; + } + + void addOrUpdate(WindowContainerTransaction wct) { + wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects); + wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame, + mBoundingRects); + } + + void remove(WindowContainerTransaction wct) { + wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar()); + wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WindowDecoration.WindowDecorationInsets that)) return false; + return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner, + that.mOwner) && Objects.equals(mFrame, that.mFrame) + && Objects.deepEquals(mBoundingRects, that.mBoundingRects); + } + + @Override + public int hashCode() { + return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects)); + } } /** 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..ec204714c341 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,21 @@ package com.android.wm.shell.windowdecor.extension import android.app.TaskInfo +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 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/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index d718e157afdb..b8a19ad35307 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -12,3 +12,6 @@ jorgegil@google.com nmusgrave@google.com pbdr@google.com tkachenkoi@google.com +mpodolian@google.com +jeremysim@google.com +peanutbutter@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index 5b2ffec67e93..f69a90cc793f 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> @@ -89,6 +91,7 @@ value="trace_config.textproto" /> <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> 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/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 9f7d9fcf1326..b76d06565700 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> @@ -89,6 +91,7 @@ value="trace_config.textproto" /> <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt index bc486c277aa5..984abf8cf8b4 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt @@ -32,7 +32,7 @@ import org.junit.runners.Parameterized /** * Test launching a new activity from bubble. * - * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen` + * To run this test: `atest WMShellFlickerTestsBubbles:ChangeActiveActivityFromBubbleTest` * * Actions: * ``` 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..886b70c5e464 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 @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test launching a new activity from bubble. * - * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen` + * To run this test: `atest WMShellFlickerTestsBubbles:DragToDismissBubbleScreenTest` * * Actions: * ``` 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..2ee53f4fce66 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 @@ -38,7 +38,7 @@ import org.junit.runners.Parameterized /** * Test launching a new activity from bubble. * - * To run this test: `atest WMShellFlickerTests:OpenActivityFromBubbleOnLocksreenTest` + * To run this test: `atest WMShellFlickerTestsBubbles:OpenActivityFromBubbleOnLocksreenTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt index ef7fbfb79beb..463fe0e60da3 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt @@ -29,7 +29,7 @@ import org.junit.runners.Parameterized /** * Test launching a new activity from bubble. * - * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen` + * To run this test: `atest WMShellFlickerTestsBubbles:OpenActivityFromBubbleTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt index 87224b151b78..8df50567a29c 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt @@ -29,7 +29,7 @@ import org.junit.runners.Parameterized /** * Test creating a bubble notification * - * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen` + * To run this test: `atest WMShellFlickerTestsBubbles:SendBubbleNotificationTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index 882b200da3a2..041978c371ff 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> @@ -89,6 +91,7 @@ value="trace_config.textproto" /> <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index f5a8655b81f0..bf040d2a95f4 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> 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/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml index 51a55e359acf..a66dfb4566f9 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> @@ -89,6 +91,7 @@ value="trace_config.textproto" /> <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS new file mode 100644 index 000000000000..73a5a23909c5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS @@ -0,0 +1,5 @@ +# Android > Android OS & Apps > Framework (Java + Native) > Window Manager > WM Shell > Freeform +# Bug component: 929241 + +uysalorhan@google.com +pragyabajoria@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt new file mode 100644 index 000000000000..5563bb9fa934 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt @@ -0,0 +1,47 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt new file mode 100644 index 000000000000..3d16d2219c78 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt @@ -0,0 +1,47 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt new file mode 100644 index 000000000000..d485b82f5ddb --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt @@ -0,0 +1,144 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds +import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType + +class DesktopModeFlickerScenarios { + companion object { + val END_DRAG_TO_DESKTOP = + FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP + } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd( + Components.DESKTOP_MODE_APP + ) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { it.type == TransitionType.CLOSE } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions.findLast { it.type == TransitionType.CLOSE } + return if (lastTransition != null) listOf(lastTransition) + else emptyList() + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + LauncherWindowMovesToTop() + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CORNER_RESIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.CHANGE + } + } + } + ), + assertions = + listOf( + AppWindowIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowRemainInsideDisplayBounds(Components.DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt new file mode 100644 index 000000000000..9dfafe958b0b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt @@ -0,0 +1,44 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) { + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() + + companion object { + + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt new file mode 100644 index 000000000000..1c7d6237eb8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt @@ -0,0 +1,43 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) { + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt new file mode 100644 index 000000000000..8d1a53021683 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt @@ -0,0 +1,43 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithCornerResizeLandscape : ResizeAppWithCornerResize(Rotation.ROTATION_90) { + @ExpectedScenarios(["CORNER_RESIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt new file mode 100644 index 000000000000..2d81c8c44799 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt @@ -0,0 +1,43 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithCornerResizePortrait : ResizeAppWithCornerResize(Rotation.ROTATION_0) { + @ExpectedScenarios(["CORNER_RESIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt new file mode 100644 index 000000000000..e77a45729124 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -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. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +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.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Base Test Class") +abstract class CloseAllAppsWithAppHeaderExit +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + + + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + nonResizeableApp.launchViaIntent(wmHelper) + } + + @Test + open fun closeAllAppsInDesktop() { + nonResizeableApp.closeDesktopApp(wmHelper, device) + mailApp.closeDesktopApp(wmHelper, device) + testApp.closeDesktopApp(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt new file mode 100644 index 000000000000..fe139d2d24a0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt @@ -0,0 +1,67 @@ +/* + * 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.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +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.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + + +@Ignore("Base Test Class") +abstract class EnterDesktopWithDrag +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + } + + @Test + open fun enterDesktopWithDrag() { + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt new file mode 100644 index 000000000000..ac9089a5c1bd --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt @@ -0,0 +1,68 @@ +/* + * 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.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +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.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + + +@Ignore("Base Test Class") +abstract class ResizeAppWithCornerResize +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun resizeAppWithCornerResize() { + testApp.cornerResize(wmHelper, device, DesktopModeAppHelper.Corners.RIGHT_TOP, 50, -50) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} 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/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml index 05f937ab6795..85715db3d952 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml @@ -20,6 +20,8 @@ <option name="isolated-storage" value="false"/> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> <!-- prevents the phone from restarting --> @@ -89,6 +91,7 @@ value="trace_config.textproto" /> <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> 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/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 9c1a88e1caa0..82c070cbf1c3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,10 +16,10 @@ package com.android.wm.shell; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; 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; @@ -435,7 +435,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase { public void testOnCameraCompatActivityChanged() { final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; + taskInfo1.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = + CAMERA_COMPAT_CONTROL_HIDDEN; final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, null); @@ -449,7 +450,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.cameraCompatControlState = + taskInfo2.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); @@ -461,7 +462,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.cameraCompatControlState = + taskInfo3.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; taskInfo3.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo3); @@ -474,7 +475,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo4.displayId = taskInfo1.displayId; taskInfo4.appCompatTaskInfo.topActivityInSizeCompat = true; - taskInfo4.appCompatTaskInfo.cameraCompatControlState = + taskInfo4.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; taskInfo4.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo4); @@ -485,7 +486,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo5 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo5.displayId = taskInfo1.displayId; - taskInfo5.appCompatTaskInfo.cameraCompatControlState = + taskInfo5.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED; taskInfo5.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo5); @@ -496,7 +497,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo6 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo6.displayId = taskInfo1.displayId; - taskInfo6.appCompatTaskInfo.cameraCompatControlState = + taskInfo6.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; taskInfo6.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo6); 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/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index 2ac72affbb0c..ea522cdf2509 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -20,6 +20,8 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; + import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -100,6 +102,20 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim } @Test + public void testTransitionTypeDragResize() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TASK_FRAGMENT_DRAG_RESIZE, 0) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .build(); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */), + new ArrayList()); + + // The animation should be empty when it is a jump cut for drag resize. + assertEquals(0, animator.getDuration()); + } + + @Test public void testInvalidCustomAnimation() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) 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..f6f3aa49bc6e 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 DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation; + private CrossTaskBackAnimation mCrossTaskBackAnimation; private ShellBackAnimationRegistry mShellBackAnimationRegistry; @Before @@ -131,12 +135,14 @@ public class BackAnimationControllerTest extends ShellTestCase { ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); + mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext, + mAnimationBackground, mRootTaskDisplayAreaOrganizer); + mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground); mShellBackAnimationRegistry = - new ShellBackAnimationRegistry( - new CrossActivityBackAnimation(mContext, mAnimationBackground), - new CrossTaskBackAnimation(mContext, mAnimationBackground), - /* dialogCloseAnimation= */ null, - new CustomizeActivityAnimation(mContext, mAnimationBackground), + new ShellBackAnimationRegistry(mDefaultCrossActivityBackAnimation, + mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null, + new CustomCrossActivityBackAnimation(mContext, mAnimationBackground, + mRootTaskDisplayAreaOrganizer), /* defaultBackToHomeAnimation= */ null); mController = new BackAnimationController( @@ -178,7 +184,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(), @@ -347,6 +355,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Verify that we prevent any interaction with the animator callback in case a new gesture // starts while the current back animation has not ended, instead the gesture is queued triggerBackGesture(); + verify(mAnimatorCallback).setTriggerBack(eq(true)); verifyNoMoreInteractions(mAnimatorCallback); // Finish previous back navigation. @@ -387,6 +396,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // starts while the current back animation has not ended, instead the gesture is queued triggerBackGesture(); releaseBackGesture(); + verify(mAnimatorCallback).setTriggerBack(eq(true)); verifyNoMoreInteractions(mAnimatorCallback); // Finish previous back navigation. @@ -405,6 +415,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, @@ -499,7 +535,7 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test - public void callbackShouldDeliverProgress() throws RemoteException { + public void appCallback_receivesStartAndInvoke() throws RemoteException { registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); final int type = BackNavigationInfo.TYPE_CALLBACK; @@ -518,8 +554,9 @@ public class BackAnimationControllerTest extends ShellTestCase { assertTrue("TriggerBack should have been true", result.mTriggerBack); verify(mAppCallback, times(1)).onBackStarted(any()); - verify(mAppCallback, times(1)).onBackProgressed(any()); verify(mAppCallback, times(1)).onBackInvoked(); + // Progress events should be generated from the app process. + verify(mAppCallback, never()).onBackProgressed(any()); verify(mAnimatorCallback, never()).onBackStarted(any()); verify(mAnimatorCallback, never()).onBackProgressed(any()); @@ -527,17 +564,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, + mDefaultCrossActivityBackAnimation.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 +600,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(); @@ -590,7 +643,7 @@ public class BackAnimationControllerTest extends ShellTestCase { */ private void doStartEvents(int startX, int moveX) { doMotionEvent(MotionEvent.ACTION_DOWN, startX); - mController.onPilferPointers(); + mController.onThresholdCrossed(); doMotionEvent(MotionEvent.ACTION_MOVE, moveX); } @@ -629,7 +682,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/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt new file mode 100644 index 000000000000..8bf011192347 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt @@ -0,0 +1,264 @@ +/* + * 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.f + */ +package com.android.wm.shell.back + +import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.app.AppCompatTaskInfo +import android.app.WindowConfiguration +import android.graphics.Color +import android.graphics.Point +import android.graphics.Rect +import android.os.RemoteException +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.Choreographer +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.animation.Animation +import android.window.BackEvent +import android.window.BackMotionEvent +import android.window.BackNavigationInfo +import androidx.test.filters.SmallTest +import com.android.internal.policy.TransitionAnimation +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import junit.framework.TestCase.assertEquals +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class CustomCrossActivityBackAnimationTest : ShellTestCase() { + @Mock private lateinit var backAnimationBackground: BackAnimationBackground + @Mock private lateinit var mockCloseAnimation: Animation + @Mock private lateinit var mockOpenAnimation: Animation + @Mock private lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var transitionAnimation: TransitionAnimation + @Mock private lateinit var appCompatTaskInfo: AppCompatTaskInfo + @Mock private lateinit var transaction: Transaction + + private lateinit var customCrossActivityBackAnimation: CustomCrossActivityBackAnimation + private lateinit var customAnimationLoader: CustomAnimationLoader + + @Before + @Throws(Exception::class) + fun setUp() { + customAnimationLoader = CustomAnimationLoader(transitionAnimation) + customCrossActivityBackAnimation = + CustomCrossActivityBackAnimation( + context, + backAnimationBackground, + rootTaskDisplayAreaOrganizer, + transaction, + mock(Choreographer::class.java), + customAnimationLoader + ) + + whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(OPEN_RES_ID))) + .thenReturn(mockOpenAnimation) + whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(CLOSE_RES_ID))) + .thenReturn(mockCloseAnimation) + whenever(transaction.setColor(any(), any())).thenReturn(transaction) + whenever(transaction.setAlpha(any(), anyFloat())).thenReturn(transaction) + whenever(transaction.setCrop(any(), any())).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), anyInt())).thenReturn(transaction) + spy(customCrossActivityBackAnimation) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishAfterInvoke() { + val finishCalled = startCustomAnimation() + try { + customCrossActivityBackAnimation.getRunner().callback.onBackInvoked() + } catch (r: RemoteException) { + Assert.fail("onBackInvoked throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishAfterCancel() { + val finishCalled = startCustomAnimation() + try { + customCrossActivityBackAnimation.getRunner().callback.onBackCancelled() + } catch (r: RemoteException) { + Assert.fail("onBackCancelled throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishWithoutAnimationAfterInvoke() { + val finishCalled = startCustomAnimation(targets = arrayOf()) + try { + customCrossActivityBackAnimation.getRunner().callback.onBackInvoked() + } catch (r: RemoteException) { + Assert.fail("onBackInvoked throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + fun testLoadCustomAnimation() { + testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 0) + } + + @Test + fun testLoadCustomAnimationNoEnter() { + testLoadCustomAnimation(0, CLOSE_RES_ID, 0) + } + + @Test + fun testLoadWindowAnimations() { + testLoadCustomAnimation(0, 0, 30) + } + + @Test + fun testCustomAnimationHigherThanWindowAnimations() { + testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 30) + } + + private fun testLoadCustomAnimation(enterResId: Int, exitResId: Int, windowAnimations: Int) { + val builder = + BackNavigationInfo.Builder() + .setCustomAnimation(PACKAGE_NAME, enterResId, exitResId, Color.GREEN) + .setWindowAnimations(PACKAGE_NAME, windowAnimations) + val info = builder.build().customAnimationInfo!! + whenever( + transitionAnimation.loadAnimationAttr( + eq(PACKAGE_NAME), + eq(windowAnimations), + anyInt(), + anyBoolean() + ) + ) + .thenReturn(mockCloseAnimation) + whenever(transitionAnimation.loadDefaultAnimationAttr(anyInt(), anyBoolean())) + .thenReturn(mockOpenAnimation) + val result = customAnimationLoader.loadAll(info)!! + if (exitResId != 0) { + if (enterResId == 0) { + verify(transitionAnimation, never()) + .loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(enterResId)) + verify(transitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()) + } else { + assertEquals(result.enterAnimation, mockOpenAnimation) + } + assertEquals(result.backgroundColor.toLong(), Color.GREEN.toLong()) + assertEquals(result.closeAnimation, mockCloseAnimation) + verify(transitionAnimation, never()) + .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean()) + } else if (windowAnimations != 0) { + verify(transitionAnimation, times(2)) + .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean()) + Assert.assertEquals(result.closeAnimation, mockCloseAnimation) + } + } + + private fun startCustomAnimation( + targets: Array<RemoteAnimationTarget> = + arrayOf(createAnimationTarget(false), createAnimationTarget(true)) + ): CountDownLatch { + val backNavigationInfo = + BackNavigationInfo.Builder() + .setCustomAnimation(PACKAGE_NAME, OPEN_RES_ID, CLOSE_RES_ID, /*backgroundColor*/ 0) + .build() + customCrossActivityBackAnimation.prepareNextAnimation( + backNavigationInfo.customAnimationInfo, + 0 + ) + val finishCalled = CountDownLatch(1) + val finishCallback = Runnable { finishCalled.countDown() } + customCrossActivityBackAnimation + .getRunner() + .startAnimation(targets, null, null, finishCallback) + customCrossActivityBackAnimation.runner.callback.onBackStarted(backMotionEventFrom(0f, 0f)) + if (targets.isNotEmpty()) { + verify(mockCloseAnimation) + .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE)) + verify(mockOpenAnimation) + .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE)) + } + return finishCalled + } + + private fun backMotionEventFrom(touchX: Float, progress: Float) = + BackMotionEvent( + /* touchX = */ touchX, + /* touchY = */ 0f, + /* progress = */ progress, + /* velocityX = */ 0f, + /* velocityY = */ 0f, + /* triggerBack = */ false, + /* swipeEdge = */ BackEvent.EDGE_LEFT, + /* departingAnimationTarget = */ null + ) + + private fun createAnimationTarget(open: Boolean): RemoteAnimationTarget { + val topWindowLeash = SurfaceControl() + val taskInfo = RunningTaskInfo() + taskInfo.appCompatTaskInfo = appCompatTaskInfo + taskInfo.taskDescription = ActivityManager.TaskDescription() + return RemoteAnimationTarget( + 1, + if (open) RemoteAnimationTarget.MODE_OPENING else RemoteAnimationTarget.MODE_CLOSING, + topWindowLeash, + false, + Rect(), + Rect(), + -1, + Point(0, 0), + Rect(0, 0, BOUND_SIZE, BOUND_SIZE), + Rect(), + WindowConfiguration(), + true, + null, + null, + taskInfo, + false, + -1 + ) + } + + companion object { + private const val BOUND_SIZE = 100 + private const val OPEN_RES_ID = 1000 + private const val CLOSE_RES_ID = 1001 + private const val PACKAGE_NAME = "TestPackage" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java deleted file mode 100644 index cebbbd890f05..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.back; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -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.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.WindowConfiguration; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.RemoteException; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.Choreographer; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.animation.Animation; -import android.window.BackNavigationInfo; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -@SmallTest -@TestableLooper.RunWithLooper -@RunWith(AndroidTestingRunner.class) -public class CustomizeActivityAnimationTest extends ShellTestCase { - private static final int BOUND_SIZE = 100; - @Mock - private BackAnimationBackground mBackAnimationBackground; - @Mock - private Animation mMockCloseAnimation; - @Mock - private Animation mMockOpenAnimation; - - private CustomizeActivityAnimation mCustomizeActivityAnimation; - - @Before - public void setUp() throws Exception { - mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext, - mBackAnimationBackground, mock(SurfaceControl.Transaction.class), - mock(Choreographer.class)); - spyOn(mCustomizeActivityAnimation); - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation); - } - - RemoteAnimationTarget createAnimationTarget(boolean open) { - SurfaceControl topWindowLeash = new SurfaceControl(); - return new RemoteAnimationTarget(1, - open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING, - topWindowLeash, false, new Rect(), new Rect(), -1, - new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(), - new WindowConfiguration(), true, null, null, null, false, -1); - } - - @Test - public void receiveFinishAfterInvoke() throws InterruptedException { - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(false)); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(true)); - - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage")); - final RemoteAnimationTarget close = createAnimationTarget(false); - final RemoteAnimationTarget open = createAnimationTarget(true); - // start animation with remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation( - new RemoteAnimationTarget[] {close, open}, null, null, finishCallback); - verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked(); - } catch (RemoteException r) { - fail("onBackInvoked throw remote exception"); - } - verify(mCustomizeActivityAnimation).onGestureCommitted(); - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void receiveFinishAfterCancel() throws InterruptedException { - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(false)); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(true)); - - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage")); - final RemoteAnimationTarget close = createAnimationTarget(false); - final RemoteAnimationTarget open = createAnimationTarget(true); - // start animation with remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation( - new RemoteAnimationTarget[] {close, open}, null, null, finishCallback); - verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackCancelled(); - } catch (RemoteException r) { - fail("onBackCancelled throw remote exception"); - } - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException { - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage")); - // start animation without any remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation(new RemoteAnimationTarget[] {}, null, null, finishCallback); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked(); - } catch (RemoteException r) { - fail("onBackInvoked throw remote exception"); - } - verify(mCustomizeActivityAnimation).onGestureCommitted(); - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void testLoadCustomAnimation() { - testLoadCustomAnimation(10, 20, 0); - } - - @Test - public void testLoadCustomAnimationNoEnter() { - testLoadCustomAnimation(0, 10, 0); - } - - @Test - public void testLoadWindowAnimations() { - testLoadCustomAnimation(0, 0, 30); - } - - @Test - public void testCustomAnimationHigherThanWindowAnimations() { - testLoadCustomAnimation(10, 20, 30); - } - - private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) { - final String testPackage = "TestPackage"; - BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() - .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN) - .setWindowAnimations(testPackage, windowAnimations); - final BackNavigationInfo.CustomAnimationInfo info = builder.build() - .getCustomAnimationInfo(); - - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAppTransitionAnimation(eq(testPackage), eq(exitResId)); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean()); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()); - - CustomizeActivityAnimation.AnimationLoadResult result = - mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info); - - if (exitResId != 0) { - if (enterResId == 0) { - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, - never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation) - .loadDefaultAnimationAttr(anyInt(), anyBoolean()); - } else { - assertEquals(result.mEnterAnimation, mMockOpenAnimation); - } - assertEquals(result.mBackgroundColor, Color.GREEN); - assertEquals(result.mCloseAnimation, mMockCloseAnimation); - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never()) - .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); - } else if (windowAnimations != 0) { - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, - times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); - assertEquals(result.mCloseAnimation, mMockCloseAnimation); - } - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt deleted file mode 100644 index 6dbb1e2b8d92..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt +++ /dev/null @@ -1,246 +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 android.util.MathUtils -import android.window.BackEvent -import org.junit.Assert.assertEquals -import org.junit.Test - -class TouchTrackerTest { - private fun linearTouchTracker(): TouchTracker = TouchTracker().apply { - setProgressThresholds(MAX_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR) - } - - private fun nonLinearTouchTracker(): TouchTracker = TouchTracker().apply { - setProgressThresholds(LINEAR_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR) - } - - private fun TouchTracker.assertProgress(expected: Float) { - val actualProgress = createProgressEvent().progress - assertEquals(expected, actualProgress, /* delta = */ 0f) - } - - @Test - fun generatesProgress_onStart() { - val linearTracker = linearTouchTracker() - linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) - val event = linearTracker.createStartEvent(null) - assertEquals(0f, event.progress, 0f) - } - - @Test - fun generatesProgress_leftEdge() { - val linearTracker = linearTouchTracker() - linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) - var touchX = 10f - val velocityX = 0f - val velocityY = 0f - - // Pre-commit - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) - - // Post-commit - touchX += 100f - linearTracker.setTriggerBack(true) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) - - // Cancel - touchX -= 10f - linearTracker.setTriggerBack(false) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // Cancel more - touchX -= 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // Restarted, but pre-commit - val restartX = touchX - touchX += 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE) - - // continue restart within pre-commit - touchX += 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE) - - // Restarted, post-commit - touchX += 10f - linearTracker.setTriggerBack(true) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) - } - - @Test - fun generatesProgress_rightEdge() { - val linearTracker = linearTouchTracker() - linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT) - var touchX = INITIAL_X_RIGHT_EDGE - 10 // Fake right edge - val velocityX = 0f - val velocityY = 0f - val target = MAX_DISTANCE - - // Pre-commit - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) - - // Post-commit - touchX -= 100f - linearTracker.setTriggerBack(true) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) - - // Cancel - touchX += 10f - linearTracker.setTriggerBack(false) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // Cancel more - touchX += 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // Restarted, but pre-commit - val restartX = touchX - touchX -= 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((restartX - touchX) / target) - - // continue restart within pre-commit - touchX -= 10f - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((restartX - touchX) / target) - - // Restarted, post-commit - touchX -= 10f - linearTracker.setTriggerBack(true) - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) - } - - @Test - fun generatesNonLinearProgress_leftEdge() { - val nonLinearTracker = nonLinearTouchTracker() - nonLinearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) - var touchX = 10f - val velocityX = 0f - val velocityY = 0f - val linearTarget = LINEAR_DISTANCE + (MAX_DISTANCE - LINEAR_DISTANCE) * NON_LINEAR_FACTOR - - // Pre-commit: linear progress - nonLinearTracker.update(touchX, 0f, velocityX, velocityY) - nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) - - // Post-commit: still linear progress - touchX += 100f - nonLinearTracker.setTriggerBack(true) - nonLinearTracker.update(touchX, 0f, velocityX, velocityY) - nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) - - // still linear progress - touchX = INITIAL_X_LEFT_EDGE + LINEAR_DISTANCE - nonLinearTracker.update(touchX, 0f, velocityX, velocityY) - nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) - - // non linear progress - touchX += 10 - nonLinearTracker.update(touchX, 0f, velocityX, velocityY) - val nonLinearTouch = (touchX - INITIAL_X_LEFT_EDGE) - LINEAR_DISTANCE - val nonLinearProgress = nonLinearTouch / NON_LINEAR_DISTANCE - val nonLinearTarget = MathUtils.lerp(linearTarget, MAX_DISTANCE, nonLinearProgress) - nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget) - } - - @Test - fun restartingGesture_resetsInitialTouchX_leftEdge() { - val linearTracker = linearTouchTracker() - linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) - var touchX = 100f - val velocityX = 0f - val velocityY = 0f - - // assert that progress is increased when increasing touchX - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) - - // assert that progress is reset to 0 when start location is updated - linearTracker.updateStartLocation() - linearTracker.assertProgress(0f) - - // assert that progress remains 0 when touchX is decreased - touchX -= 50 - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // assert that progress uses new minimal touchX for progress calculation - val newInitialTouchX = touchX - touchX += 100 - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE) - - // assert the same for triggerBack==true - linearTracker.triggerBack = true - linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE) - } - - @Test - fun restartingGesture_resetsInitialTouchX_rightEdge() { - val linearTracker = linearTouchTracker() - linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT) - - var touchX = INITIAL_X_RIGHT_EDGE - 100f - val velocityX = 0f - val velocityY = 0f - - // assert that progress is increased when decreasing touchX - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / MAX_DISTANCE) - - // assert that progress is reset to 0 when start location is updated - linearTracker.updateStartLocation() - linearTracker.assertProgress(0f) - - // assert that progress remains 0 when touchX is increased - touchX += 50 - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress(0f) - - // assert that progress uses new maximal touchX for progress calculation - val newInitialTouchX = touchX - touchX -= 100 - linearTracker.update(touchX, 0f, velocityX, velocityY) - linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE) - - // assert the same for triggerBack==true - linearTracker.triggerBack = true - linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE) - } - - companion object { - private const val MAX_DISTANCE = 500f - private const val LINEAR_DISTANCE = 400f - private const val NON_LINEAR_DISTANCE = MAX_DISTANCE - LINEAR_DISTANCE - private const val NON_LINEAR_FACTOR = 0.2f - private const val INITIAL_X_LEFT_EDGE = 5f - private const val INITIAL_X_RIGHT_EDGE = MAX_DISTANCE - INITIAL_X_LEFT_EDGE - } -}
\ No newline at end of file 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..0f433770777e 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; @@ -1191,20 +1193,74 @@ public class BubbleDataTest extends ShellTestCase { } @Test - public void test_removeOverflowBubble() { - sendUpdatedEntryAtTime(mEntryA1, 2000); + 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); + } + + @Test + public void testShowOverflowChanged_hasOverflowBubbles() { + assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); + sendUpdatedEntryAtTime(mEntryA1, 1000); mBubbleData.setListener(mListener); mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); verifyUpdateReceived(); - assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue(); + assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty(); + } - mBubbleData.removeOverflowBubble(mBubbleA1); + @Test + public void testShowOverflowChanged_false_hasOverflowBubbles() { + assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 1000); + mBubbleData.setListener(mListener); + + // First overflowed causes change event + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); verifyUpdateReceived(); + assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue(); + assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty(); - BubbleData.Update update = mUpdateCaptor.getValue(); - assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1); - assertOverflowChangedTo(ImmutableList.of()); + // Second overflow does not + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertThat(mUpdateCaptor.getValue().showOverflowChanged).isFalse(); + } + + @Test + public void testShowOverflowChanged_noOverflowBubbles() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty(); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); + + verifyUpdateReceived(); + assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue(); + assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); } private void verifyUpdateReceived() { 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/bubbles/animation/PhysicsAnimationLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java index 964711ee8dcb..043128583432 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java @@ -69,7 +69,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { // to animate child views out before actually removing them). mTestableController.setAnimatedProperties(Sets.newHashSet( DynamicAnimation.TRANSLATION_X, - DynamicAnimation.TRANSLATION_Y)); + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.TRANSLATION_Z)); mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X)); mTestableController.setOffsetForProperty( DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET); @@ -282,10 +283,13 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { addOneMoreThanBubbleLimitBubbles(); assertFalse(mLayout.arePropertiesAnimating( - DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.TRANSLATION_Z)); mTestableController.animationForChildAtIndex(0) .translationX(100f) + .translationZ(100f) .start(); // Wait for the animations to get underway. @@ -293,11 +297,13 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X)); assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y)); + assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Z)); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Z); assertFalse(mLayout.arePropertiesAnimating( - DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.TRANSLATION_Z)); } @Test @@ -307,7 +313,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { addOneMoreThanBubbleLimitBubbles(); mTestableController.animationForChildAtIndex(0) - .position(1000, 1000) + .position(1000, 1000, 1000) .start(); mLayout.cancelAllAnimations(); @@ -315,6 +321,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { // Animations should be somewhere before their end point. assertTrue(mViews.get(0).getTranslationX() < 1000); assertTrue(mViews.get(0).getTranslationY() < 1000); + assertTrue(mViews.get(0).getZ() < 10000); } /** Standard test of chained translation animations. */ 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/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 19ce2f3899c3..cfe8e07aa6e5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -116,27 +116,27 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testUpdateDivideBounds() { - mSplitLayout.updateDivideBounds(anyInt(), anyBoolean()); + mSplitLayout.updateDividerBounds(anyInt(), anyBoolean()); verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(), anyInt(), anyBoolean()); } @Test public void testSetDividePosition() { - mSplitLayout.setDividePosition(100, false /* applyLayoutChange */); - assertThat(mSplitLayout.getDividePosition()).isEqualTo(100); + mSplitLayout.setDividerPosition(100, false /* applyLayoutChange */); + assertThat(mSplitLayout.getDividerPosition()).isEqualTo(100); verify(mSplitLayoutHandler, never()).onLayoutSizeChanged(any(SplitLayout.class)); - mSplitLayout.setDividePosition(200, true /* applyLayoutChange */); - assertThat(mSplitLayout.getDividePosition()).isEqualTo(200); + mSplitLayout.setDividerPosition(200, true /* applyLayoutChange */); + assertThat(mSplitLayout.getDividerPosition()).isEqualTo(200); verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); } @Test public void testSetDivideRatio() { - mSplitLayout.setDividePosition(200, false /* applyLayoutChange */); + mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */); mSplitLayout.setDivideRatio(SNAP_TO_50_50); - assertThat(mSplitLayout.getDividePosition()).isEqualTo( + assertThat(mSplitLayout.getDividerPosition()).isEqualTo( mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position); } @@ -153,7 +153,7 @@ public class SplitLayoutTests extends ShellTestCase { DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, SNAP_TO_START_AND_DISMISS); - mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget); + mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget); waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt()); } @@ -165,7 +165,7 @@ public class SplitLayoutTests extends ShellTestCase { DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, SNAP_TO_END_AND_DISMISS); - mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget); + mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget); waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt()); } @@ -189,7 +189,7 @@ public class SplitLayoutTests extends ShellTestCase { } private void waitDividerFlingFinished() { - verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(), + verify(mSplitLayout).flingDividerPosition(anyInt(), anyInt(), anyInt(), mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); } 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/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 2c85495ce5db..9c008647104a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -16,8 +16,8 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -34,7 +34,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; import android.content.res.Configuration; @@ -701,7 +701,8 @@ public class CompatUIControllerTest extends ShellTestCase { taskInfo.taskId = taskId; taskInfo.displayId = displayId; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = + cameraCompatControlState; taskInfo.isVisible = isVisible; taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index dd358e757fde..cd3e8cb0e8e1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -16,10 +16,10 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -28,7 +28,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.graphics.Rect; import android.testing.AndroidTestingRunner; @@ -222,7 +222,8 @@ public class CompatUILayoutTest extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = + cameraCompatControlState; taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index d92e5aa0890a..41a81c1a9921 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -16,10 +16,10 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; @@ -38,7 +38,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.AppCompatTaskInfo; +import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; @@ -544,11 +544,12 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } private static TaskInfo createTaskInfo(boolean hasSizeCompat, - @AppCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) { + @CameraCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = + cameraCompatControlState; taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; // Letterboxed activity that takes half the screen should show size compat restart button taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java index 38d6ea1839c4..02316125bcc3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -16,7 +16,7 @@ package com.android.wm.shell.compatui; -import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -25,7 +25,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.AppCompatTaskInfo.CameraCompatControlState; +import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.ComponentName; import android.testing.AndroidTestingRunner; @@ -148,7 +148,8 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = + cameraCompatControlState; taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); return taskInfo; } 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..2a2483df0792 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -0,0 +1,368 @@ +/* + * 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.content.Context +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.dx.mockito.inline.extended.ExtendedMockito.doReturn +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.shared.DesktopModeStatus +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 + +/** + * Test class for {@link DesktopModeLoggerTransitionObserver} + * + * Usage: atest WMShellUnitTests:DesktopModeLoggerTransitionObserverTest + */ +@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 + @Mock + private lateinit var context: Context + + private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver + private lateinit var shellInit: ShellInit + private lateinit var desktopModeEventLogger: DesktopModeEventLogger + + @Before + fun setup() { + doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } + shellInit = Mockito.spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) + + transitionObserver = DesktopModeLoggerTransitionObserver( + context, 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..8f59f30da697 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,27 +119,66 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun addListener_notifiesVisibleFreeformTask() { - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - executor.flushAll() + fun isOnlyActiveTask_noActiveTasks() { + // Not an active task + assertThat(repo.isOnlyActiveTask(1)).isFalse() + } - assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + @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 addListener_notifiesStashed() { - repo.setStashed(DEFAULT_DISPLAY, true) + 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() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) } @Test @@ -237,6 +278,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 @@ -326,64 +388,126 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun setStashed_stateIsUpdatedForTheDisplay() { - repo.setStashed(DEFAULT_DISPLAY, true) - assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue() - assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse() + fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { + val taskId = 1 + repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) + repo.removeFreeformTask(taskId) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() + } - repo.setStashed(DEFAULT_DISPLAY, false) - assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse() + @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 setStashed_notifyListener() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + 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() + } - repo.setStashed(DEFAULT_DISPLAY, false) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isFalse() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2) + @Test + fun minimizeTaskNotCalled_noTasksMinimized() { + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() } @Test - fun setStashed_secondCallDoesNotNotify() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - repo.setStashed(DEFAULT_DISPLAY, true) - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + fun minimizeTask_onlyThatTaskIsMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isTrue() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() } @Test - fun setStashed_tracksPerDisplay() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) + fun unminimizeTask_taskNoLongerMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + repo.unminimizeTask(displayId = 0, taskId = 0) - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedOnSecondaryDisplay).isFalse() + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } - repo.setStashed(SECOND_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedOnSecondaryDisplay).isTrue() + @Test + fun unminimizeTask_nonExistentTask_doesntCrash() { + repo.unminimizeTask(displayId = 0, taskId = 0) - repo.setStashed(DEFAULT_DISPLAY, false) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isFalse() - assertThat(listener.stashedOnSecondaryDisplay).isTrue() + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + + @Test + fun updateVisibleFreeformTasks_toVisible_taskIsUnminimized() { + repo.minimizeTask(displayId = 10, taskId = 2) + + repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true) + + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun isDesktopModeShowing_noActiveTasks_returnsFalse() { + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() } + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 2, 3)) + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + repo.minimizeTask(displayId = 0, taskId = 2) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 3)) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 @@ -403,12 +527,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 - var stashedOnDefaultDisplay = false - var stashedOnSecondaryDisplay = false - - var stashedChangesOnDefaultDisplay = 0 - var stashedChangesOnSecondaryDisplay = 0 - override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { DEFAULT_DISPLAY -> { @@ -422,20 +540,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { else -> fail("Visible task listener received unexpected display id: $displayId") } } - - override fun onStashedChanged(displayId: Int, stashed: Boolean) { - when (displayId) { - DEFAULT_DISPLAY -> { - stashedOnDefaultDisplay = stashed - stashedChangesOnDefaultDisplay++ - } - SECOND_DISPLAY -> { - stashedOnSecondaryDisplay = stashed - stashedChangesOnDefaultDisplay++ - } - else -> fail("Visible task listener received unexpected display id: $displayId") - } - } } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt new file mode 100644 index 000000000000..285e5b6a04a5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt @@ -0,0 +1,111 @@ +/* + * 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.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.Companion.DesktopUiEventEnum.DESKTOP_WINDOW_EDGE_DRAG_RESIZE +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopModeUiEventLogger] + * + * Usage: atest WMShellUnitTests:DesktopModeUiEventLoggerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeUiEventLoggerTest : ShellTestCase() { + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var logger: DesktopModeUiEventLogger + private val instanceIdSequence = InstanceIdSequence(10) + + + @Before + fun setUp() { + uiEventLoggerFake = UiEventLoggerFake() + logger = DesktopModeUiEventLogger(uiEventLoggerFake, instanceIdSequence) + } + + @Test + fun log_invalidUid_eventNotLogged() { + logger.log(-1, PACKAGE_NAME, DESKTOP_WINDOW_EDGE_DRAG_RESIZE) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0) + } + + @Test + fun log_emptyPackageName_eventNotLogged() { + logger.log(UID, "", DESKTOP_WINDOW_EDGE_DRAG_RESIZE) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0) + } + + @Test + fun log_eventLogged() { + val event = + DESKTOP_WINDOW_EDGE_DRAG_RESIZE + logger.log(UID, PACKAGE_NAME, event) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id) + assertThat(uiEventLoggerFake[0].instanceId).isNull() + assertThat(uiEventLoggerFake[0].uid).isEqualTo(UID) + assertThat(uiEventLoggerFake[0].packageName).isEqualTo(PACKAGE_NAME) + } + + @Test + fun getNewInstanceId() { + val first = logger.getNewInstanceId() + assertThat(first).isNotEqualTo(logger.getNewInstanceId()) + } + + @Test + fun logWithInstanceId_invalidUid_eventNotLogged() { + logger.logWithInstanceId(INSTANCE_ID, -1, PACKAGE_NAME, DESKTOP_WINDOW_EDGE_DRAG_RESIZE) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0) + } + + @Test + fun logWithInstanceId_emptyPackageName_eventNotLogged() { + logger.logWithInstanceId(INSTANCE_ID, UID, "", DESKTOP_WINDOW_EDGE_DRAG_RESIZE) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0) + } + + @Test + fun logWithInstanceId_eventLogged() { + val event = + DESKTOP_WINDOW_EDGE_DRAG_RESIZE + logger.logWithInstanceId(INSTANCE_ID, UID, PACKAGE_NAME, event) + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id) + assertThat(uiEventLoggerFake[0].instanceId).isEqualTo(INSTANCE_ID) + assertThat(uiEventLoggerFake[0].uid).isEqualTo(UID) + assertThat(uiEventLoggerFake[0].packageName).isEqualTo(PACKAGE_NAME) + } + + + companion object { + private val INSTANCE_ID = InstanceId.fakeInstanceId(0) + private const val UID = 10 + private const val PACKAGE_NAME = "com.foo" + } +} 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..f67da5573b7d 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,55 @@ 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.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +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 @@ -58,21 +79,23 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener +import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.splitscreen.SplitScreenController 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,53 +107,95 @@ 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.spy import org.mockito.Mockito.verify +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.capture +import org.mockito.quality.Strictness +import java.util.Optional 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 + @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private lateinit var desktopTasksLimiter: DesktopTasksLimiter 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>() + private val DISPLAY_DIMENSION_SHORT = 1600 + private val DISPLAY_DIMENSION_LONG = 2560 + private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400) + private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240) + private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880) + private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400) + private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861) + private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400) + @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) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - shellInit = Mockito.spy(ShellInit(testExecutor)) + shellInit = spy(ShellInit(testExecutor)) desktopModeTaskRepository = DesktopModeTaskRepository() + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(enterDesktopTransitionHandler.moveToDesktop(any())).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,13 +221,15 @@ class DesktopTasksControllerTest : ShellTestCase() { transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, - mToggleResizeDesktopTaskTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, - shellExecutor + shellExecutor, + Optional.of(desktopTasksLimiter), ) } @@ -189,7 +256,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 +276,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 +315,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 +354,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 +386,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 +416,44 @@ 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 showDesktopApps_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct( + type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Reorder home and freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 0, homeTask, toTop = true) + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -306,9 +483,139 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() { + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true) + setUpLandscapeDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(isResizable = false, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + setUpLandscapeDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val task = setUpFullscreenTask(isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task) + val wct = getLatestMoveToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + 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 +623,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 +640,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 +710,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 +758,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 +772,36 @@ 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 moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val homeTask = setUpHomeTask() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + + controller.moveToDesktop(newTask) + + val wct = getLatestMoveToDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + for (i in 1..<taskLimit) { // Skipping freeformTasks[0] + wct.assertReorderAt(index = i, task = freeformTasks[i]) + } + wct.assertReorderAt(taskLimit, newTask) + } + + @Test + 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 +809,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) @@ -447,6 +851,20 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + setUpHomeTask() + val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } + + controller.moveTaskToFront(freeformTasks[0]) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = true) + wct.assertReorderAt(1, freeformTasks[1], toTop = false) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -510,6 +928,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) @@ -523,6 +983,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + wct!!.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -553,36 +1045,30 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { + fun handleRequest_freeformTask_freeformVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) - markTaskHidden(stashedFreeformTask) - - val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY) - controller.stashDesktopApps(DEFAULT_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(result).isNotNull() - result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask) - assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) - // Stashed state should be cleared - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + val freeformTask2 = createFreeformTask() + assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull() } @Test - fun handleRequest_freeformTask_freeformVisible_returnNull() { + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() - val freeformTask2 = createFreeformTask() - assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull() + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom } @Test @@ -599,7 +1085,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 +1095,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,27 +1107,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - @Test - fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) - markTaskHidden(stashedFreeformTask) - - val freeformTask = createFreeformTask(DEFAULT_DISPLAY) - - controller.stashDesktopApps(DEFAULT_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(freeformTask)) - assertThat(result).isNotNull() - result?.assertReorderSequence(stashedFreeformTask, freeformTask) - - // Stashed state should be cleared - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -698,26 +1164,65 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun stashDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) + fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = setUpFreeformTask().apply { + isTopActivityTransparent = true + numActivities = 1 + } - controller.stashDesktopApps(DEFAULT_DISPLAY) + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue() - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse() + @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 - fun hideStashedDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() - desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true) - desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true) - controller.hideStashedDesktopApps(DEFAULT_DISPLAY) + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - // Check that second display is not affected - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue() + @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 @@ -741,7 +1246,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 +1255,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 +1263,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun enterDesktop_splitScreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -771,12 +1276,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 +1302,294 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = getLatestExitDesktopWct() assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } - private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createFreeformTask(displayId) + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(isResizable = false, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @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(findBoundsChange(wct, task)).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) + } + + @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(findBoundsChange(wct, task)).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) @@ -814,15 +1604,68 @@ class DesktopTasksControllerTest : ShellTestCase() { return task } - private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + private fun setUpFullscreenTask( + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false + ): RunningTaskInfo { val task = createFullscreenTask(displayId) + val activityInfo = ActivityInfo() + activityInfo.screenOrientation = screenOrientation + with(task) { + topActivityInfo = activityInfo + isResizeable = isResizable + configuration.orientation = deviceOrientation + configuration.windowConfiguration.windowingMode = windowingMode + + if (shouldLetterbox) { + if (deviceOrientation == ORIENTATION_LANDSCAPE && + screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { + // Letterbox to portrait size + appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.topActivityLetterboxWidth = 1200 + appCompatTaskInfo.topActivityLetterboxHeight = 1600 + } else if (deviceOrientation == ORIENTATION_PORTRAIT && + screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { + // Letterbox to landscape size + appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.topActivityLetterboxWidth = 1600 + appCompatTaskInfo.topActivityLetterboxHeight = 1200 + } + } else { + appCompatTaskInfo.topActivityBoundsLetterboxed = false + } + + if (deviceOrientation == ORIENTATION_LANDSCAPE) { + configuration.windowConfiguration.appBounds = Rect(0, 0, + DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) + } else { + configuration.windowConfiguration.appBounds = Rect(0, 0, + DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) + } + } + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) return task } + private fun setUpLandscapeDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) + } + + private fun setUpPortraitDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) + } + private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createSplitScreenTask(displayId) + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) @@ -862,6 +1705,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) { @@ -872,6 +1727,17 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } + private fun getLatestDragToDesktopWct(): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) + } else { + verify(shellTaskOrganizer).applyTransaction(capture(arg)) + } + return arg.value + } + private fun getLatestExitDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) if (ENABLE_SHELL_TRANSITIONS) { @@ -883,6 +1749,10 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } + private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = + wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds + + private fun verifyWCTNotExecuted() { if (ENABLE_SHELL_TRANSITIONS) { verify(transitions, never()).startTransition(anyInt(), any(), isNull()) @@ -900,16 +1770,26 @@ 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, + toTop: Boolean? = null +) { + assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) + toTop?.let { assertThat(op.toTop).isEqualTo(it) } } private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { @@ -917,3 +1797,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/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt new file mode 100644 index 000000000000..3c488cac6edd --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -0,0 +1,320 @@ +/* + * 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.os.Binder +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.StubTransaction +import com.google.common.truth.Truth.assertThat +import org.junit.After +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.any +import org.mockito.Mockito.`when` +import org.mockito.quality.Strictness + + +/** + * Test class for {@link DesktopTasksLimiter} + * + * Usage: atest WMShellUnitTests:DesktopTasksLimiterTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTasksLimiterTest : ShellTestCase() { + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var transitions: Transitions + + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var desktopTasksLimiter: DesktopTasksLimiter + private lateinit var desktopTaskRepo: DesktopModeTaskRepository + + @Before + fun setUp() { + mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java).startMocking() + doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } + + desktopTaskRepo = DesktopModeTaskRepository() + + desktopTasksLimiter = DesktopTasksLimiter( + transitions, desktopTaskRepo, shellTaskOrganizer) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + // Currently, the task limit can be overridden through an adb flag. This test ensures the limit + // hasn't been overridden. + @Test + fun getMaxTaskLimit_isSameAsConstant() { + assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo( + DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT) + } + + @Test + fun addPendingMinimizeTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.addPendingMinimizeChange(Binder(), displayId = 1, taskId = task.taskId) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_noPendingTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + Binder() /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() { + val pendingTransition = Binder() + val taskTransition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + taskTransition /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskVisible(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { + val mergedTransition = Binder() + val newTransition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + desktopTasksLimiter.getTransitionObserver().onTransitionMerged( + mergedTransition, newTransition) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + newTransition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + (1..<taskLimit).forEach { _ -> setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + // The following list will be ordered bottom -> top, as the last task is moved to top last. + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isEqualTo(tasks.first()) + assertThat(wct.hierarchyOps.size).isEqualTo(1) + assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(wct.hierarchyOps[0].toTop).isFalse() // Reorder to bottom + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = 0, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + assertThat(minimizedTask).isNull() + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit + 1).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + @Test + fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }, + newTaskIdInFront = setUpFreeformTask().taskId) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + ): RunningTaskInfo { + val task = createFreeformTask(displayId) + `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopTaskRepo.addActiveTask(displayId, task.taskId) + desktopTaskRepo.addOrMoveFreeformTaskToTop(task.taskId) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = true + ) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = false + ) + } +} 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/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 71eea4bb59b1..cd68c6996578 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -19,11 +19,12 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager; @@ -34,8 +35,8 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; -import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -72,8 +73,10 @@ public final class FreeformTaskListenerTests extends ShellTestCase { public void setup() { mMockitoSession = mockitoSession().initMocks(this) .strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(true); + doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mFreeformTaskListener = new FreeformTaskListener( + mContext, mShellInit, mTaskOrganizer, Optional.of(mDesktopModeTaskRepository), 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/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java new file mode 100644 index 000000000000..bd8ac379b86f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java @@ -0,0 +1,110 @@ +/* + * 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.pip2; + +import android.os.Bundle; +import android.os.Parcelable; +import android.testing.AndroidTestingRunner; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.pip2.phone.PipTransitionState; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit test against {@link PhoneSizeSpecSource}. + * + * This test mocks the PiP2 flag to be true. + */ +@RunWith(AndroidTestingRunner.class) +public class PipTransitionStateTest extends ShellTestCase { + private static final String EXTRA_ENTRY_KEY = "extra_entry_key"; + private PipTransitionState mPipTransitionState; + private PipTransitionState.PipTransitionStateChangedListener mStateChangedListener; + private Parcelable mEmptyParcelable; + + @Before + public void setUp() { + mPipTransitionState = new PipTransitionState(); + mPipTransitionState.setState(PipTransitionState.UNDEFINED); + mEmptyParcelable = new Bundle(); + } + + @Test + public void testEnteredState_withoutExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNull(extra); + }; + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test + public void testEnteredState_withExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP, extra); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test(expected = IllegalArgumentException.class) + public void testEnteringState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP); + } + + @Test(expected = IllegalArgumentException.class) + public void testSwipingToPipState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP); + } + + @Test + public void testCustomState_withExtra_thenEntered_withoutExtra() { + final int customState = mPipTransitionState.getCustomState(); + mStateChangedListener = (oldState, newState, extra) -> { + if (newState == customState) { + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + return; + } else if (newState == PipTransitionState.ENTERED_PIP) { + Assert.assertNull(extra); + return; + } + Assert.fail("Neither custom not ENTERED_PIP state is received."); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(customState, extra); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } +} 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..884cb6ec9f74 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,22 +46,29 @@ 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.ExtendedMockito; 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; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -68,10 +76,13 @@ import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.quality.Strictness; import java.util.ArrayList; import java.util.Arrays; @@ -80,7 +91,9 @@ import java.util.Optional; import java.util.function.Consumer; /** - * Tests for {@link RecentTasksController}. + * Tests for {@link RecentTasksController} + * + * Usage: atest WMShellUnitTests:RecentTasksControllerTest */ @RunWith(AndroidJUnit4.class) @SmallTest @@ -96,6 +109,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; @@ -103,14 +123,20 @@ public class RecentTasksControllerTest extends ShellTestCase { private ShellInit mShellInit; private ShellController mShellController; private TestShellExecutor mMainExecutor; + private static StaticMockitoSession sMockitoSession; @Before public void setUp() { + sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus.class).startMocking(); + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mMainExecutor = new TestShellExecutor(); 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); @@ -121,6 +147,11 @@ public class RecentTasksControllerTest extends ShellTestCase { mShellInit.init(); } + @After + public void tearDown() { + sMockitoSession.finishMocking(); + } + @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class)); @@ -260,10 +291,6 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() { - 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); @@ -294,15 +321,54 @@ public class RecentTasksControllerTest extends ShellTestCase { // Check single entries assertEquals(t2, singleGroup1.getTaskInfo1()); assertEquals(t4, singleGroup2.getTaskInfo1()); + } + + @Test + public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() { + 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); - mockitoSession.finishMocking(); + // 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()); } @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() { - StaticMockitoSession mockitoSession = mockitoSession().mockStatic( - DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(false); + ExtendedMockito.doReturn(false) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); @@ -327,8 +393,45 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(t2, recentTasks.get(1).getTaskInfo1()); assertEquals(t3, recentTasks.get(2).getTaskInfo1()); assertEquals(t4, recentTasks.get(3).getTaskInfo1()); + } - mockitoSession.finishMocking(); + @Test + public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() { + 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); + + when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true); + when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true); + when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true); + when(mDesktopModeTaskRepository.isMinimizedTask(3)).thenReturn(true); + + ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( + MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); + + // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents + // entries + assertEquals(3, recentTasks.size()); + GroupedRecentTaskInfo freeformGroup = recentTasks.get(0); + GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1); + GroupedRecentTaskInfo singleGroup2 = recentTasks.get(2); + + // Check that groups have expected types + assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup1.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType()); + + // Check freeform group entries + assertEquals(2, freeformGroup.getTaskInfoList().size()); + assertEquals(t1, freeformGroup.getTaskInfoList().get(0)); + assertEquals(t5, freeformGroup.getTaskInfoList().get(1)); + + // Check single entries + assertEquals(t2, singleGroup1.getTaskInfo1()); + assertEquals(t4, singleGroup2.getTaskInfo1()); } @Test @@ -375,6 +478,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 +602,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/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index befc702b01aa..34b2eebb15a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -39,10 +39,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.annotation.NonNull; import android.app.ActivityManager; @@ -63,6 +66,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; 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.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -105,6 +109,8 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private ShellExecutor mMainExecutor; @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private DefaultMixedHandler mMixedHandler; + @Mock private SplitScreen.SplitInvocationListener mInvocationListener; + private final TestShellExecutor mTestShellExecutor = new TestShellExecutor(); private SplitLayout mSplitLayout; private MainStage mMainStage; private SideStage mSideStage; @@ -147,6 +153,7 @@ public class SplitTransitionTests extends ShellTestCase { .setParentTaskId(mSideStage.mRootTaskInfo.taskId).build(); doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager(); doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager(); + mStageCoordinator.registerSplitAnimationListener(mInvocationListener, mTestShellExecutor); } @Test @@ -452,6 +459,15 @@ public class SplitTransitionTests extends ShellTestCase { mMainStage.activate(new WindowContainerTransaction(), true /* includingTopTask */); } + @Test + @UiThreadTest + public void testSplitInvocationCallback() { + enterSplit(); + mTestShellExecutor.flushAll(); + verify(mInvocationListener, times(1)) + .onSplitAnimationInvoked(eq(true)); + } + private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) { for (int i = 0; i < wct.getHierarchyOps().size(); ++i) { WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i); 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/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index d7c46104b6b1..0434742c571b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -44,7 +44,6 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; -import android.os.Handler; import android.os.Looper; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -498,6 +497,31 @@ public class TaskViewTest extends ShellTestCase { } @Test + public void testStartRootTask_setsBoundsAndVisibility() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + TaskViewBase taskViewBase = mock(TaskViewBase.class); + Rect bounds = new Rect(0, 0, 100, 100); + when(taskViewBase.getCurrentBoundsOnScreen()).thenReturn(bounds); + mTaskViewTaskController.setTaskViewBase(taskViewBase); + + // Surface created, but task not available so bounds / visibility isn't set + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + verify(mTaskViewTransitions, never()).updateVisibilityState( + eq(mTaskViewTaskController), eq(true)); + + // Make the task available + WindowContainerTransaction wct = mock(WindowContainerTransaction.class); + mTaskViewTaskController.startRootTask(mTaskInfo, mLeash, wct); + + // Bounds got set + verify(wct).setBounds(any(WindowContainerToken.class), eq(bounds)); + // Visibility & bounds state got set + verify(mTaskViewTransitions).updateVisibilityState(eq(mTaskViewTaskController), eq(true)); + verify(mTaskViewTransitions).updateBoundsState(eq(mTaskViewTaskController), eq(bounds)); + } + + @Test public void testTaskViewPrepareOpenAnimationSetsBoundsAndVisibility() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index fbc0db9c2850..d3e40f21db23 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -18,6 +18,7 @@ package com.android.wm.shell.taskview; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.google.common.truth.Truth.assertThat; @@ -208,6 +209,48 @@ public class TaskViewTransitionsTest extends ShellTestCase { } @Test + public void testReorderTask_movedToFrontTransaction() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, true); + // Consume the pending transaction from order change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + + // Verify it was consumed + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending2).isNull(); + } + + @Test + public void testReorderTask_movedToBackTransaction() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, false); + // Consume the pending transaction from order change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + + // Verify it was consumed + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK); + assertThat(pending2).isNull(); + } + + @Test public void test_startAnimation_setsTaskNotFound() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); 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..964d86e8bd35 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 @@ -73,6 +73,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import android.util.Pair; import android.view.IRecentsAnimationRunner; @@ -83,9 +84,11 @@ 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; +import android.window.WindowAnimationState; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -97,6 +100,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; +import com.android.systemui.shared.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -113,6 +117,7 @@ import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.StubTransaction; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -140,6 +145,9 @@ public class ShellTransitionTests extends ShellTestCase { private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + @Rule + public final SetFlagsRule setFlagsRule = new SetFlagsRule(); + @Before public void setUp() { doAnswer(invocation -> new Binder()) @@ -280,7 +288,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 +296,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 +448,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,15 +456,73 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } + }; + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test")); + mMainExecutor.flushAll(); + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); + assertEquals(0, mDefaultHandler.activeCount()); + assertTrue(remoteCalled[0]); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); + } + + @Test + public void testRegisteredRemoteTransitionTakeover() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + final Transitions.TransitionHandler takeoverHandler = + transitions.getHandlerForTakeover(token, info); + + if (takeoverHandler == null) { + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); + return; + } + + takeoverHandler.takeOverAnimation(token, info, new SurfaceControl.Transaction(), + wct -> { + try { + finishCallback.onTransitionFinished(wct, null /* sct */); + } catch (RemoteException e) { + // Fail + } + }, new WindowAnimationState[info.getChanges().size()]); } + }; + final boolean[] takeoverRemoteCalled = new boolean[]{false}; + IRemoteTransition testTakeoverRemote = new RemoteTransitionStub() { + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) {} @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { + public void takeOverAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states) + throws RemoteException { + takeoverRemoteCalled[0] = true; + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } }; @@ -476,21 +532,38 @@ public class ShellTransitionTests extends ShellTestCase { filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test")); + transitions.registerRemoteForTakeover( + filter, new RemoteTransition(testTakeoverRemote, "Test")); mMainExecutor.flushAll(); + // Takeover shouldn't happen when the flag is disabled. + setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, new StubTransaction(), new StubTransaction()); assertEquals(0, mDefaultHandler.activeCount()); - assertTrue(remoteCalled[0]); + assertFalse(takeoverRemoteCalled[0]); mDefaultHandler.finishAll(); mMainExecutor.flushAll(); verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); + + // Takeover should happen when the flag is enabled. + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); + assertEquals(0, mDefaultHandler.activeCount()); + assertTrue(takeoverRemoteCalled[0]); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(2)).finishTransition(eq(transitToken), any()); } @Test @@ -499,8 +572,9 @@ public class ShellTransitionTests extends ShellTestCase { transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; + final boolean[] takeoverRemoteCalled = 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, @@ -510,13 +584,12 @@ public class ShellTransitionTests extends ShellTestCase { } @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 { + public void takeOverAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states) + throws RemoteException { + takeoverRemoteCalled[0] = true; + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } }; @@ -524,6 +597,7 @@ public class ShellTransitionTests extends ShellTestCase { OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, new RemoteTransition(testRemote, "Test")); + // Verify that it responds to the remote but not other things. IBinder transitToken = new Binder(); assertNotNull(oneShot.handleRequest(transitToken, @@ -534,6 +608,7 @@ public class ShellTransitionTests extends ShellTestCase { Transitions.TransitionFinishCallback testFinish = mock(Transitions.TransitionFinishCallback.class); + // Verify that it responds to animation properly oneShot.setTransition(transitToken); IBinder anotherToken = new Binder(); @@ -543,6 +618,16 @@ public class ShellTransitionTests extends ShellTestCase { assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0), new StubTransaction(), new StubTransaction(), testFinish)); + assertTrue(remoteCalled[0]); + + // Verify that it handles takeovers properly + IBinder newToken = new Binder(); + oneShot.setTransition(newToken); + assertFalse(oneShot.takeOverAnimation(transitToken, new TransitionInfo(transitType, 0), + new StubTransaction(), testFinish, new WindowAnimationState[0])); + assertTrue(oneShot.takeOverAnimation(newToken, new TransitionInfo(transitType, 0), + new StubTransaction(), testFinish, new WindowAnimationState[0])); + assertTrue(takeoverRemoteCalled[0]); } @Test 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..aa2cee79fcfc 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,14 @@ 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.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +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 +47,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 @@ -51,16 +61,21 @@ 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.DesktopTasksController +import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.sysui.KeyguardChangeListener 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.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 +84,26 @@ 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() + + @JvmField + @Rule + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @Mock private lateinit var mockDesktopModeWindowDecorFactory: DesktopModeWindowDecoration.Factory @Mock private lateinit var mockMainHandler: Handler @@ -102,6 +127,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 +136,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Before fun setUp() { shellInit = ShellInit(mockShellExecutor) + windowDecorByTaskIdSpy.clear() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( mContext, mockShellExecutor, @@ -128,7 +155,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + windowDecorByTaskIdSpy ) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -272,6 +300,20 @@ 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 + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -292,6 +334,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutDoesNotRunWhenNonStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -312,6 +355,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutDoesNotRunWhenNonStatusBarsInsetSourceVisibilityDoesNotChange() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -332,6 +376,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 +495,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 +515,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..608f74b95280 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 @@ -18,6 +18,7 @@ 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.WINDOWING_MODE_MULTI_WINDOW; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; import static com.google.common.truth.Truth.assertThat; @@ -44,6 +45,7 @@ import android.view.Choreographer; import android.view.Display; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.WindowManager; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -173,10 +175,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(); @@ -187,14 +189,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* applyStartTransactionOnDraw= */ true, /* shouldSetTaskPositionAndCrop */ false); - assertThat(relayoutParams.mAllowCaptionInputFallthrough).isTrue(); + assertThat(relayoutParams.hasInputFeatureSpy()).isTrue(); } @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( @@ -204,7 +206,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* applyStartTransactionOnDraw= */ true, /* shouldSetTaskPositionAndCrop */ false); - assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse(); + assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test @@ -220,7 +222,55 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* applyStartTransactionOnDraw= */ true, /* shouldSetTaskPositionAndCrop */ false); - assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse(); + assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); + } + + @Test + public void updateRelayoutParams_freeform_inputChannelNeeded() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse(); + } + + @Test + public void updateRelayoutParams_fullscreen_inputChannelNotNeeded() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); + } + + @Test + public void updateRelayoutParams_multiwindow_inputChannelNotNeeded() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } private void fillRoundedCornersResources(int fillValue) { @@ -268,4 +318,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return taskInfo; } + + private static boolean hasNoInputChannelFeature(RelayoutParams params) { + return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) + != 0; + } } 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..54645083eca8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -0,0 +1,348 @@ +/* + * 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 EDGE_RESIZE_DEBUG_THICKNESS = EDGE_RESIZE_THICKNESS + + (DragResizeWindowGeometry.DEBUG ? DragResizeWindowGeometry.EDGE_DEBUG_BUFFER : 0); + 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, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5)) + .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE)) + .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_DEBUG_THICKNESS, point.y)).isTrue(); + assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue(); + // Vertically along the edge is not contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isFalse(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_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_DEBUG_THICKNESS, point.y)).isFalse(); + assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse(); + // Vertically along the edge is contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isTrue(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_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); + // Make sure we're choosing a point outside of any debug region buffer. + final int cornerRadius = DragResizeWindowGeometry.DEBUG + ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS) + : 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 = DragResizeWindowGeometry.DEBUG + ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS) + : LARGE_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..87425915fbf7 --- /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.Bitmap +import android.graphics.Rect +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: Bitmap + @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..48ac1e5717aa 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 ) } @@ -195,7 +197,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { rectAfterEnd.top += 20 rectAfterEnd.bottom += 20 - verify(mockDesktopWindowDecoration, never()).createResizeVeil() + verify(mockDesktopWindowDecoration, never()).showResizeVeil(any()) verify(mockDesktopWindowDecoration, never()).hideResizeVeil() verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -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..8b8cd119effd 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 @@ -42,6 +42,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.same; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.quality.Strictness.LENIENT; @@ -61,10 +62,10 @@ 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; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -74,7 +75,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.tests.R; import org.junit.Before; @@ -252,16 +253,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); @@ -613,26 +612,86 @@ public class WindowDecorationTests extends ShellTestCase { mockitoSession.finishMocking(); } + @Test + public void testRelayout_captionHidden_insetsRemoved() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true) + .setBounds(new Rect(0, 0, 1000, 1000)) + .build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + + // Run it once so that insets are added. + mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); + windowDecor.relayout(taskInfo); + + // Run it again so that insets are removed. + mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); + windowDecor.relayout(taskInfo); + + verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(captionBar())); + verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(mandatorySystemGestures())); + } @Test - public void testInsetsRemovedWhenCaptionIsHidden() { + public void testRelayout_captionHidden_neverWasVisible_insetsNotRemoved() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) .getDisplay(Display.DEFAULT_DISPLAY); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true) + .setBounds(new Rect(0, 0, 1000, 1000)) + .build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + + // Hidden from the beginning, so no insets were ever added. mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); + windowDecor.relayout(taskInfo); + + // Never added. + verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(captionBar()), any(), any()); + verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + // No need to remove them if they were never added. + verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), + any(), eq(0) /* index */, eq(captionBar())); + verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), + any(), eq(0) /* index */, eq(mandatorySystemGestures())); + } + + @Test + public void testClose_withExistingInsets_insetsRemoved() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); - final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = - new ActivityManager.TaskDescription.Builder(); final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() .setDisplayId(Display.DEFAULT_DISPLAY) - .setTaskDescriptionBuilder(taskDescriptionBuilder) .setVisible(true) + .setBounds(new Rect(0, 0, 1000, 1000)) .build(); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + // Relayout will add insets. + mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); windowDecor.relayout(taskInfo); + verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(captionBar()), any(), any()); + verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + + windowDecor.close(); + // Insets should be removed. verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), eq(0) /* index */, eq(captionBar())); verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), @@ -640,6 +699,82 @@ public class WindowDecorationTests extends ShellTestCase { } @Test + public void testClose_withoutExistingInsets_insetsNotRemoved() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true) + .setBounds(new Rect(0, 0, 1000, 1000)) + .build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + + windowDecor.close(); + + // No need to remove insets. + verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), + any(), eq(0) /* index */, eq(captionBar())); + verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), + any(), eq(0) /* index */, eq(mandatorySystemGestures())); + } + + @Test + public void testRelayout_captionFrameChanged_insetsReapplied() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + // Relayout twice with different bounds. + final ActivityManager.RunningTaskInfo firstTaskInfo = + builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo); + windowDecor.relayout(firstTaskInfo); + final ActivityManager.RunningTaskInfo secondTaskInfo = + builder.setToken(token).setBounds(new Rect(50, 50, 1000, 1000)).build(); + windowDecor.relayout(secondTaskInfo); + + // Insets should be applied twice. + verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(captionBar()), any(), any()); + verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + } + + @Test + public void testRelayout_captionFrameUnchanged_insetsNotApplied() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + // Relayout twice with the same bounds. + final ActivityManager.RunningTaskInfo firstTaskInfo = + builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo); + windowDecor.relayout(firstTaskInfo); + final ActivityManager.RunningTaskInfo secondTaskInfo = + builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build(); + windowDecor.relayout(secondTaskInfo); + + // Insets should only need to be applied once. + verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(captionBar()), any(), any()); + verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + } + + @Test public void testTaskPositionAndCropNotSetWhenFalse() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) @@ -766,6 +901,7 @@ public class WindowDecorationTests extends ShellTestCase { void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw) { + mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); 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/resourcefile_fuzzer/Android.bp b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp index b511244c4a30..619658923865 100644 --- a/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp +++ b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp @@ -19,6 +19,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_libs_androidfw_license"], + default_team: "trendy_team_android_resources", } cc_fuzz { @@ -31,7 +32,7 @@ cc_fuzz { static_libs: ["libgmock"], target: { android: { - shared_libs:[ + shared_libs: [ "libandroidfw", "libbase", "libcutils", @@ -52,4 +53,15 @@ cc_fuzz { ], }, }, + fuzz_config: { + cc: [ + "android-resources@google.com", + ], + componentid: 568761, + description: "The fuzzer targets the APIs of libandroidfw", + vector: "local_no_privileges_required", + service_privilege: "privileged", + users: "multi_user", + fuzzed_code_usage: "shipped", + }, } 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/ADisplay.cpp b/libs/hostgraphics/ADisplay.cpp index 9cc1f40184e3..58fa08281a61 100644 --- a/libs/hostgraphics/ADisplay.cpp +++ b/libs/hostgraphics/ADisplay.cpp @@ -94,14 +94,14 @@ namespace android { int ADisplay_acquirePhysicalDisplays(ADisplay*** outDisplays) { // This is running on host, so there are no physical displays available. // Create 1 fake display instead. - DisplayImpl** const impls = reinterpret_cast<DisplayImpl**>( - malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl))); + DisplayImpl** const impls = + reinterpret_cast<DisplayImpl**>(malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl))); DisplayImpl* const displayData = reinterpret_cast<DisplayImpl*>(impls + 1); - displayData[0] = DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL, - ADataSpace::ADATASPACE_UNKNOWN, - AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, - DisplayConfigImpl()}; + displayData[0] = + DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL, ADataSpace::ADATASPACE_UNKNOWN, + AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, + DisplayConfigImpl()}; impls[0] = displayData; *outDisplays = reinterpret_cast<ADisplay**>(impls); return 1; 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/Fence.cpp b/libs/hostgraphics/Fence.cpp index 9e54816651c4..4383bf02a00e 100644 --- a/libs/hostgraphics/Fence.cpp +++ b/libs/hostgraphics/Fence.cpp @@ -20,4 +20,4 @@ namespace android { const sp<Fence> Fence::NO_FENCE = sp<Fence>(new Fence); -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp index ec304378c6c4..7e14b88a47fa 100644 --- a/libs/hostgraphics/HostBufferQueue.cpp +++ b/libs/hostgraphics/HostBufferQueue.cpp @@ -15,18 +15,26 @@ */ #include <gui/BufferQueue.h> +#include <system/window.h> namespace android { class HostBufferQueue : public IGraphicBufferProducer, public IGraphicBufferConsumer { public: - HostBufferQueue() : mWidth(0), mHeight(0) { } + HostBufferQueue() : mWidth(0), mHeight(0) {} - virtual status_t setConsumerIsProtected(bool isProtected) { return OK; } + // Consumer + virtual status_t setConsumerIsProtected(bool isProtected) { + return OK; + } - virtual status_t detachBuffer(int slot) { return OK; } + virtual status_t detachBuffer(int slot) { + return OK; + } - virtual status_t getReleasedBuffers(uint64_t* slotMask) { return OK; } + virtual status_t getReleasedBuffers(uint64_t* slotMask) { + return OK; + } virtual status_t setDefaultBufferSize(uint32_t w, uint32_t h) { mWidth = w; @@ -35,22 +43,54 @@ public: return OK; } - virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) { return OK; } + virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) { + return OK; + } - virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) { return OK; } + virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) { + return OK; + } - virtual status_t discardFreeBuffers() { return OK; } + virtual status_t discardFreeBuffers() { + return OK; + } virtual status_t acquireBuffer(BufferItem* buffer, nsecs_t presentWhen, - uint64_t maxFrameNumber = 0) { + uint64_t maxFrameNumber = 0) { buffer->mGraphicBuffer = mBuffer; buffer->mSlot = 0; return OK; } - virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { return OK; } + virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { + return OK; + } + + virtual status_t setConsumerUsageBits(uint64_t usage) { + return OK; + } + + // Producer + virtual int query(int what, int* value) { + switch (what) { + case NATIVE_WINDOW_WIDTH: + *value = mWidth; + break; + case NATIVE_WINDOW_HEIGHT: + *value = mHeight; + break; + default: + *value = 0; + break; + } + return OK; + } + + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) { + *buf = mBuffer; + return OK; + } - virtual status_t setConsumerUsageBits(uint64_t usage) { return OK; } private: sp<GraphicBuffer> mBuffer; uint32_t mWidth; @@ -58,8 +98,7 @@ private: }; void BufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer, - sp<IGraphicBufferConsumer>* outConsumer) { - + sp<IGraphicBufferConsumer>* outConsumer) { sp<HostBufferQueue> obj(new HostBufferQueue()); *outProducer = obj; diff --git a/libs/hostgraphics/PublicFormat.cpp b/libs/hostgraphics/PublicFormat.cpp index af6d2738c801..2a2eec63467c 100644 --- a/libs/hostgraphics/PublicFormat.cpp +++ b/libs/hostgraphics/PublicFormat.cpp @@ -30,4 +30,4 @@ PublicFormat mapHalFormatDataspaceToPublicFormat(int format, android_dataspace d return static_cast<PublicFormat>(format); } -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/gui/BufferItem.h b/libs/hostgraphics/gui/BufferItem.h index 01409e19c715..e95a9231dfaf 100644 --- a/libs/hostgraphics/gui/BufferItem.h +++ b/libs/hostgraphics/gui/BufferItem.h @@ -17,16 +17,15 @@ #ifndef ANDROID_GUI_BUFFERITEM_H #define ANDROID_GUI_BUFFERITEM_H +#include <system/graphics.h> #include <ui/Fence.h> #include <ui/Rect.h> - -#include <system/graphics.h> - #include <utils/StrongPointer.h> namespace android { class Fence; + class GraphicBuffer; // The only thing we need here for layoutlib is mGraphicBuffer. The rest of the fields are added @@ -37,6 +36,7 @@ public: enum { INVALID_BUFFER_SLOT = -1 }; BufferItem() : mGraphicBuffer(nullptr), mFence(Fence::NO_FENCE) {} + ~BufferItem() {} sp<GraphicBuffer> mGraphicBuffer; @@ -60,6 +60,6 @@ public: bool mTransformToDisplayInverse; }; -} +} // namespace android #endif // ANDROID_GUI_BUFFERITEM_H diff --git a/libs/hostgraphics/gui/BufferItemConsumer.h b/libs/hostgraphics/gui/BufferItemConsumer.h index 707b313eb102..c25941151800 100644 --- a/libs/hostgraphics/gui/BufferItemConsumer.h +++ b/libs/hostgraphics/gui/BufferItemConsumer.h @@ -17,32 +17,30 @@ #ifndef ANDROID_GUI_BUFFERITEMCONSUMER_H #define ANDROID_GUI_BUFFERITEMCONSUMER_H -#include <utils/RefBase.h> - #include <gui/ConsumerBase.h> #include <gui/IGraphicBufferConsumer.h> +#include <utils/RefBase.h> namespace android { class BufferItemConsumer : public ConsumerBase { public: - BufferItemConsumer( - const sp<IGraphicBufferConsumer>& consumer, - uint64_t consumerUsage, - int bufferCount, - bool controlledByApp) : mConsumer(consumer) { - } + BufferItemConsumer(const sp<IGraphicBufferConsumer>& consumer, uint64_t consumerUsage, + int bufferCount, bool controlledByApp) + : mConsumer(consumer) {} - status_t acquireBuffer(BufferItem *item, nsecs_t presentWhen, bool waitForFence = true) { + status_t acquireBuffer(BufferItem* item, nsecs_t presentWhen, bool waitForFence = true) { return mConsumer->acquireBuffer(item, presentWhen, 0); } - status_t releaseBuffer( - const BufferItem &item, const sp<Fence>& releaseFence = Fence::NO_FENCE) { return OK; } + status_t releaseBuffer(const BufferItem& item, + const sp<Fence>& releaseFence = Fence::NO_FENCE) { + return OK; + } - void setName(const String8& name) { } + void setName(const String8& name) {} - void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) { } + void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) {} status_t setDefaultBufferSize(uint32_t width, uint32_t height) { return mConsumer->setDefaultBufferSize(width, height); @@ -56,16 +54,23 @@ public: return mConsumer->setDefaultBufferDataSpace(defaultDataSpace); } - void abandon() { } + void abandon() {} - status_t detachBuffer(int slot) { return OK; } + status_t detachBuffer(int slot) { + return OK; + } + + status_t discardFreeBuffers() { + return OK; + } - status_t discardFreeBuffers() { return OK; } + void freeBufferLocked(int slotIndex) {} - void freeBufferLocked(int slotIndex) { } + status_t addReleaseFenceLocked(int slot, const sp<GraphicBuffer> graphicBuffer, + const sp<Fence>& fence) { + return OK; + } - status_t addReleaseFenceLocked( - int slot, const sp<GraphicBuffer> graphicBuffer, const sp<Fence>& fence) { return OK; } private: sp<IGraphicBufferConsumer> mConsumer; }; diff --git a/libs/hostgraphics/gui/BufferQueue.h b/libs/hostgraphics/gui/BufferQueue.h index aa3e7268e11c..67a8c00fd267 100644 --- a/libs/hostgraphics/gui/BufferQueue.h +++ b/libs/hostgraphics/gui/BufferQueue.h @@ -29,7 +29,7 @@ public: enum { NO_BUFFER_AVAILABLE = IGraphicBufferConsumer::NO_BUFFER_AVAILABLE }; static void createBufferQueue(sp<IGraphicBufferProducer>* outProducer, - sp<IGraphicBufferConsumer>* outConsumer); + sp<IGraphicBufferConsumer>* outConsumer); }; } // namespace android diff --git a/libs/hostgraphics/gui/ConsumerBase.h b/libs/hostgraphics/gui/ConsumerBase.h index 9002953c0848..7f7309e8a3a8 100644 --- a/libs/hostgraphics/gui/ConsumerBase.h +++ b/libs/hostgraphics/gui/ConsumerBase.h @@ -18,7 +18,6 @@ #define ANDROID_GUI_CONSUMERBASE_H #include <gui/BufferItem.h> - #include <utils/RefBase.h> namespace android { @@ -28,10 +27,11 @@ public: struct FrameAvailableListener : public virtual RefBase { // See IConsumerListener::onFrame{Available,Replaced} virtual void onFrameAvailable(const BufferItem& item) = 0; + virtual void onFrameReplaced(const BufferItem& /* item */) {} }; }; } // namespace android -#endif // ANDROID_GUI_CONSUMERBASE_H
\ No newline at end of file +#endif // ANDROID_GUI_CONSUMERBASE_H diff --git a/libs/hostgraphics/gui/IGraphicBufferConsumer.h b/libs/hostgraphics/gui/IGraphicBufferConsumer.h index 9eb67b218800..14ac4fe71cc8 100644 --- a/libs/hostgraphics/gui/IGraphicBufferConsumer.h +++ b/libs/hostgraphics/gui/IGraphicBufferConsumer.h @@ -16,16 +16,16 @@ #pragma once -#include <utils/RefBase.h> - #include <ui/PixelFormat.h> - #include <utils/Errors.h> +#include <utils/RefBase.h> namespace android { class BufferItem; + class Fence; + class GraphicBuffer; class IGraphicBufferConsumer : virtual public RefBase { @@ -62,4 +62,4 @@ public: virtual status_t discardFreeBuffers() = 0; }; -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/gui/IGraphicBufferProducer.h index a1efd0bcfa4c..8fd8590d10d7 100644 --- a/libs/hostgraphics/gui/IGraphicBufferProducer.h +++ b/libs/hostgraphics/gui/IGraphicBufferProducer.h @@ -17,9 +17,8 @@ #ifndef ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H #define ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H -#include <utils/RefBase.h> - #include <ui/GraphicBuffer.h> +#include <utils/RefBase.h> namespace android { @@ -31,6 +30,10 @@ public: // Disconnect any API originally connected from the process calling disconnect. AllLocal }; + + virtual int query(int what, int* value) = 0; + + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) = 0; }; } // namespace android diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h index 2573931c8543..2774f89cb54c 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/gui/Surface.h @@ -17,25 +17,36 @@ #ifndef ANDROID_GUI_SURFACE_H #define ANDROID_GUI_SURFACE_H -#include <gui/IGraphicBufferProducer.h> +#include <system/window.h> #include <ui/ANativeObjectBase.h> #include <utils/RefBase.h> -#include <system/window.h> + +#include "gui/IGraphicBufferProducer.h" namespace android { class Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase> { public: - explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, - bool controlledByApp = false) { + explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp = false) + : mBufferProducer(bufferProducer) { ANativeWindow::perform = hook_perform; + ANativeWindow::dequeueBuffer = hook_dequeueBuffer; + ANativeWindow::query = hook_query; } - static bool isValid(const sp<Surface>& surface) { return surface != nullptr; } + + static bool isValid(const sp<Surface>& surface) { + return surface != nullptr; + } + void allocateBuffers() {} - uint64_t getNextFrameNumber() const { return 0; } + uint64_t getNextFrameNumber() const { + return 0; + } - int setScalingMode(int mode) { return 0; } + int setScalingMode(int mode) { + return 0; + } virtual int disconnect(int api, IGraphicBufferProducer::DisconnectMode mode = @@ -47,22 +58,88 @@ public: // TODO: implement this return 0; } - virtual int unlockAndPost() { return 0; } - virtual int query(int what, int* value) const { return 0; } + + virtual int unlockAndPost() { + return 0; + } + + virtual int query(int what, int* value) const { + return mBufferProducer->query(what, value); + } + + status_t setDequeueTimeout(nsecs_t timeout) { + return OK; + } + + nsecs_t getLastDequeueStartTime() const { + return 0; + } virtual void destroy() {} + int getBuffersDataSpace() { + return 0; + } + protected: virtual ~Surface() {} - static int hook_perform(ANativeWindow* window, int operation, ...) { return 0; } + static int hook_perform(ANativeWindow* window, int operation, ...) { + va_list args; + va_start(args, operation); + Surface* c = getSelf(window); + int result = c->perform(operation, args); + va_end(args); + return result; + } + + static int hook_query(const ANativeWindow* window, int what, int* value) { + const Surface* c = getSelf(window); + return c->query(what, value); + } + + static int hook_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, + int* fenceFd) { + Surface* c = getSelf(window); + return c->dequeueBuffer(buffer, fenceFd); + } + + virtual int dequeueBuffer(ANativeWindowBuffer** buffer, int* fenceFd) { + mBufferProducer->requestBuffer(0, &mBuffer); + *buffer = mBuffer.get(); + return OK; + } + + virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd) { + return 0; + } + + virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd) { + return 0; + } + + virtual int perform(int operation, va_list args) { + return 0; + } + + virtual int setSwapInterval(int interval) { + return 0; + } + + virtual int setBufferCount(int bufferCount) { + return 0; + } private: // can't be copied Surface& operator=(const Surface& rhs); + Surface(const Surface& rhs); + + const sp<IGraphicBufferProducer> mBufferProducer; + sp<GraphicBuffer> mBuffer; }; } // namespace android -#endif // ANDROID_GUI_SURFACE_H +#endif // ANDROID_GUI_SURFACE_H diff --git a/libs/hostgraphics/ui/Fence.h b/libs/hostgraphics/ui/Fence.h index 04d535c3a211..187c3116f61c 100644 --- a/libs/hostgraphics/ui/Fence.h +++ b/libs/hostgraphics/ui/Fence.h @@ -17,8 +17,8 @@ #ifndef ANDROID_FENCE_H #define ANDROID_FENCE_H -#include <utils/String8.h> #include <utils/RefBase.h> +#include <utils/String8.h> typedef int64_t nsecs_t; @@ -26,11 +26,14 @@ namespace android { class Fence : public LightRefBase<Fence> { public: - Fence() { } - Fence(int) { } + Fence() {} + + Fence(int) {} + static const sp<Fence> NO_FENCE; static constexpr nsecs_t SIGNAL_TIME_PENDING = INT64_MAX; static constexpr nsecs_t SIGNAL_TIME_INVALID = -1; + static sp<Fence> merge(const char* name, const sp<Fence>& f1, const sp<Fence>& f2) { return NO_FENCE; } @@ -40,16 +43,22 @@ public: } enum class Status { - Invalid, // Fence is invalid - Unsignaled, // Fence is valid but has not yet signaled - Signaled, // Fence is valid and has signaled + Invalid, // Fence is invalid + Unsignaled, // Fence is valid but has not yet signaled + Signaled, // Fence is valid and has signaled }; - status_t wait(int timeout) { return OK; } + status_t wait(int timeout) { + return OK; + } - status_t waitForever(const char* logname) { return OK; } + status_t waitForever(const char* logname) { + return OK; + } - int dup() const { return 0; } + int dup() const { + return 0; + } inline Status getStatus() { // The sync_wait call underlying wait() has been measured to be diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/ui/GraphicBuffer.h index ac88e44dbc65..cda45e4660ca 100644 --- a/libs/hostgraphics/ui/GraphicBuffer.h +++ b/libs/hostgraphics/ui/GraphicBuffer.h @@ -19,31 +19,51 @@ #include <stdint.h> #include <sys/types.h> - -#include <vector> - +#include <ui/ANativeObjectBase.h> #include <ui/PixelFormat.h> #include <ui/Rect.h> - #include <utils/RefBase.h> +#include <vector> + namespace android { -class GraphicBuffer : virtual public RefBase { +class GraphicBuffer : public ANativeObjectBase<ANativeWindowBuffer, GraphicBuffer, RefBase> { public: - GraphicBuffer(uint32_t w, uint32_t h):width(w),height(h) { - data.resize(w*h); + GraphicBuffer(uint32_t w, uint32_t h) { + data.resize(w * h); + reserved[0] = data.data(); + width = w; + height = h; + } + + uint32_t getWidth() const { + return static_cast<uint32_t>(width); + } + + uint32_t getHeight() const { + return static_cast<uint32_t>(height); + } + + uint32_t getStride() const { + return static_cast<uint32_t>(width); + } + + uint64_t getUsage() const { + return 0; } - uint32_t getWidth() const { return static_cast<uint32_t>(width); } - uint32_t getHeight() const { return static_cast<uint32_t>(height); } - uint32_t getStride() const { return static_cast<uint32_t>(width); } - uint64_t getUsage() const { return 0; } - PixelFormat getPixelFormat() const { return PIXEL_FORMAT_RGBA_8888; } - //uint32_t getLayerCount() const { return static_cast<uint32_t>(layerCount); } - Rect getBounds() const { return Rect(width, height); } - status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, - android_ycbcr *ycbcr, int fenceFd) { return OK; } + PixelFormat getPixelFormat() const { + return PIXEL_FORMAT_RGBA_8888; + } + + Rect getBounds() const { + return Rect(width, height); + } + + status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, android_ycbcr* ycbcr, int fenceFd) { + return OK; + } status_t lockAsync(uint32_t inUsage, const Rect& rect, void** vaddr, int fenceFd, int32_t* outBytesPerPixel = nullptr, int32_t* outBytesPerStride = nullptr) { @@ -51,11 +71,11 @@ public: return OK; } - status_t unlockAsync(int *fenceFd) { return OK; } + status_t unlockAsync(int* fenceFd) { + return OK; + } private: - uint32_t width; - uint32_t height; std::vector<uint32_t> data; }; diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 54f94f5c4b14..7c1c5b4e7e5f 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", ], }, @@ -333,9 +336,12 @@ cc_defaults { "jni/android_graphics_animation_NativeInterpolatorFactory.cpp", "jni/android_graphics_animation_RenderNodeAnimator.cpp", "jni/android_graphics_Canvas.cpp", + "jni/android_graphics_Color.cpp", "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 +351,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 +425,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 +450,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 +539,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 +565,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 +582,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 +603,7 @@ cc_defaults { "SkiaCanvas.cpp", "SkiaInterpolator.cpp", "Tonemapper.cpp", + "TreeInfo.cpp", "VectorDrawable.cpp", ], @@ -598,43 +620,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 +663,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/ColorFilter.h b/libs/hwui/ColorFilter.h index 1a5b938d6eed..31c9db7ca4fb 100644 --- a/libs/hwui/ColorFilter.h +++ b/libs/hwui/ColorFilter.h @@ -23,17 +23,42 @@ #include "GraphicsJNI.h" #include "SkColorFilter.h" -#include "SkiaWrapper.h" namespace android { namespace uirenderer { -class ColorFilter : public SkiaWrapper<SkColorFilter> { +class ColorFilter : public VirtualLightRefBase { public: static ColorFilter* fromJava(jlong handle) { return reinterpret_cast<ColorFilter*>(handle); } + sk_sp<SkColorFilter> getInstance() { + if (mInstance != nullptr && shouldDiscardInstance()) { + mInstance = nullptr; + } + + if (mInstance == nullptr) { + mInstance = createInstance(); + if (mInstance) { + mInstance = mInstance->makeWithWorkingColorSpace(SkColorSpace::MakeSRGB()); + } + mGenerationId++; + } + return mInstance; + } + + virtual bool shouldDiscardInstance() const { return false; } + + void discardInstance() { mInstance = nullptr; } + + [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; } + protected: ColorFilter() = default; + virtual sk_sp<SkColorFilter> createInstance() = 0; + +private: + sk_sp<SkColorFilter> mInstance = nullptr; + int32_t mGenerationId = 0; }; class BlendModeColorFilter : public ColorFilter { 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/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp index c67b135855f7..5a45ad9085e7 100644 --- a/libs/hwui/SkiaInterpolator.cpp +++ b/libs/hwui/SkiaInterpolator.cpp @@ -20,6 +20,7 @@ #include "include/core/SkTypes.h" #include <cstdlib> +#include <cstring> #include <log/log.h> typedef int Dot14; diff --git a/libs/hwui/SkiaWrapper.h b/libs/hwui/SkiaWrapper.h deleted file mode 100644 index bd0e35aadbb4..000000000000 --- a/libs/hwui/SkiaWrapper.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef SKIA_WRAPPER_H_ -#define SKIA_WRAPPER_H_ - -#include <SkRefCnt.h> -#include <utils/RefBase.h> - -namespace android::uirenderer { - -template <typename T> -class SkiaWrapper : public VirtualLightRefBase { -public: - sk_sp<T> getInstance() { - if (mInstance != nullptr && shouldDiscardInstance()) { - mInstance = nullptr; - } - - if (mInstance == nullptr) { - mInstance = createInstance(); - mGenerationId++; - } - return mInstance; - } - - virtual bool shouldDiscardInstance() const { return false; } - - void discardInstance() { mInstance = nullptr; } - - [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; } - -protected: - virtual sk_sp<T> createInstance() = 0; - -private: - sk_sp<T> mInstance = nullptr; - int32_t mGenerationId = 0; -}; - -} // namespace android::uirenderer - -#endif // SKIA_WRAPPER_H_ 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..70a9ef04d6f3 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -46,6 +46,7 @@ namespace android { extern int register_android_graphics_Canvas(JNIEnv* env); extern int register_android_graphics_CanvasProperty(JNIEnv* env); +extern int register_android_graphics_Color(JNIEnv* env); extern int register_android_graphics_ColorFilter(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); @@ -87,6 +88,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)}, {"android.graphics.Canvas", REG_JNI(register_android_graphics_Canvas)}, {"android.graphics.CanvasProperty", REG_JNI(register_android_graphics_CanvasProperty)}, + {"android.graphics.Color", REG_JNI(register_android_graphics_Color)}, {"android.graphics.ColorFilter", REG_JNI(register_android_graphics_ColorFilter)}, {"android.graphics.ColorSpace", REG_JNI(register_android_graphics_ColorSpace)}, {"android.graphics.CreateJavaOutputStreamAdaptor", @@ -164,8 +166,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..6ace3967ecf3 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -49,6 +49,7 @@ namespace android { extern int register_android_graphics_Canvas(JNIEnv* env); extern int register_android_graphics_CanvasProperty(JNIEnv* env); extern int register_android_graphics_ColorFilter(JNIEnv* env); +extern int register_android_graphics_Color(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); extern int register_android_graphics_FontFamily(JNIEnv* env); @@ -70,7 +71,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); @@ -99,6 +99,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_Canvas), + REG_JNI(register_android_graphics_Color), // This needs to be before register_android_graphics_Graphics, or the latter // will not be able to find the jmethodID for ColorSpace.get(). REG_JNI(register_android_graphics_ColorSpace), @@ -142,7 +143,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/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp index 3ebf7d19202d..0a30c6c14c4c 100644 --- a/libs/hwui/effects/GainmapRenderer.cpp +++ b/libs/hwui/effects/GainmapRenderer.cpp @@ -32,6 +32,8 @@ #include "src/core/SkColorFilterPriv.h" #include "src/core/SkImageInfoPriv.h" #include "src/core/SkRuntimeEffectPriv.h" + +#include <cmath> #endif namespace android::uirenderer { @@ -206,12 +208,12 @@ private: void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) { - const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR), - sk_float_log(gainmapInfo.fGainmapRatioMin.fG), - sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f}); - const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR), - sk_float_log(gainmapInfo.fGainmapRatioMax.fG), - sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f}); + const SkColor4f logRatioMin({std::log(gainmapInfo.fGainmapRatioMin.fR), + std::log(gainmapInfo.fGainmapRatioMin.fG), + std::log(gainmapInfo.fGainmapRatioMin.fB), 1.f}); + const SkColor4f logRatioMax({std::log(gainmapInfo.fGainmapRatioMax.fR), + std::log(gainmapInfo.fGainmapRatioMax.fG), + std::log(gainmapInfo.fGainmapRatioMax.fB), 1.f}); const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f && gainmapInfo.fGainmapGamma.fG == 1.f && gainmapInfo.fGainmapGamma.fB == 1.f; @@ -248,10 +250,10 @@ private: float W = 0.f; if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) { if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) { - W = (sk_float_log(targetHdrSdrRatio) - - sk_float_log(mGainmapInfo.fDisplayRatioSdr)) / - (sk_float_log(mGainmapInfo.fDisplayRatioHdr) - - sk_float_log(mGainmapInfo.fDisplayRatioSdr)); + W = (std::log(targetHdrSdrRatio) - + std::log(mGainmapInfo.fDisplayRatioSdr)) / + (std::log(mGainmapInfo.fDisplayRatioHdr) - + std::log(mGainmapInfo.fDisplayRatioSdr)); } else { W = 1.f; } 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/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 1fcb6920db14..cfca48084d97 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,7 +34,9 @@ namespace flags = com::android::graphics::hwui::flags; namespace android { -inline constexpr int kHighContrastTextBorderWidth = 4; +// These should match the constants in framework/base/core/java/android/text/Layout.java +inline constexpr float kHighContrastTextBorderWidth = 4.0f; +inline constexpr float kHighContrastTextBorderWidthFactor = 0.2f; static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness, const Paint& paint, Canvas* canvas) { @@ -48,7 +50,16 @@ static void simplifyPaint(int color, Paint* paint) { paint->setShader(nullptr); paint->setColorFilter(nullptr); paint->setLooper(nullptr); - paint->setStrokeWidth(kHighContrastTextBorderWidth + 0.04 * paint->getSkFont().getSize()); + + if (flags::high_contrast_text_small_text_rect()) { + paint->setStrokeWidth( + std::max(kHighContrastTextBorderWidth, + kHighContrastTextBorderWidthFactor * paint->getSkFont().getSize())); + } else { + auto borderWidthFactor = 0.04f; + paint->setStrokeWidth(kHighContrastTextBorderWidth + + borderWidthFactor * paint->getSkFont().getSize()); + } paint->setStrokeJoin(SkPaint::kRound_Join); paint->setLooper(nullptr); } @@ -106,36 +117,7 @@ public: Paint outlinePaint(paint); simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint); outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style); - if (flags::high_contrast_text_small_text_rect()) { - const SkFont& font = paint.getSkFont(); - auto padding = kHighContrastTextBorderWidth + 0.1f * font.getSize(); - - // Draw the background only behind each glyph's bounds. We do this instead of using - // the bounds of the entire layout, because the layout includes alignment whitespace - // etc which can obscure other text from separate passes (e.g. emojis). - // Merge all the glyph bounds into one rect for this line, since drawing a rect for - // each glyph is expensive. - SkRect glyphBounds; - SkRect bgBounds; - for (size_t i = start; i < end; i++) { - auto glyph = layout.getGlyphId(i); - - font.getBounds(reinterpret_cast<const SkGlyphID*>(&glyph), 1, &glyphBounds, - &paint); - glyphBounds.offset(layout.getX(i), layout.getY(i)); - - bgBounds.join(glyphBounds); - } - - if (!bgBounds.isEmpty()) { - bgBounds.offset(x, y); - bgBounds.outset(padding, padding); - canvas->drawRect(bgBounds.fLeft, bgBounds.fTop, bgBounds.fRight, - bgBounds.fBottom, outlinePaint); - } - } else { - canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance); - } + canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance); // inner gDrawTextBlobMode = DrawTextBlobMode::HctInner; 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/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 8315c4c0dd4d..07e97f85d588 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -211,11 +211,7 @@ static jclass gRegion_class; static jfieldID gRegion_nativeInstanceID; static jmethodID gRegion_constructorMethodID; -static jclass gByte_class; -static jobject gVMRuntime; -static jclass gVMRuntime_class; -static jmethodID gVMRuntime_newNonMovableArray; -static jmethodID gVMRuntime_addressOf; +static jclass gByte_class; static jclass gColorSpace_class; static jmethodID gColorSpace_getMethodID; @@ -789,13 +785,6 @@ int register_android_graphics_Graphics(JNIEnv* env) gByte_class = (jclass) env->NewGlobalRef( env->GetStaticObjectField(c, env->GetStaticFieldID(c, "TYPE", "Ljava/lang/Class;"))); - gVMRuntime_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "dalvik/system/VMRuntime")); - m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;"); - gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m)); - gVMRuntime_newNonMovableArray = GetMethodIDOrDie(env, gVMRuntime_class, "newNonMovableArray", - "(Ljava/lang/Class;I)Ljava/lang/Object;"); - gVMRuntime_addressOf = GetMethodIDOrDie(env, gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J"); - gColorSpace_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ColorSpace")); gColorSpace_getMethodID = GetStaticMethodIDOrDie(env, gColorSpace_class, "get", "(Landroid/graphics/ColorSpace$Named;)Landroid/graphics/ColorSpace;"); 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/Shader.cpp b/libs/hwui/jni/Shader.cpp index a952be020855..2a057e7a4cdc 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -36,25 +36,6 @@ static const uint32_t sGradientShaderFlags = SkGradientShader::kInterpolateColor return 0; \ } -static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue, jfloatArray hsvArray) -{ - SkScalar hsv[3]; - SkRGBToHSV(red, green, blue, hsv); - - AutoJavaFloatArray autoHSV(env, hsvArray, 3); - float* values = autoHSV.ptr(); - for (int i = 0; i < 3; i++) { - values[i] = SkScalarToFloat(hsv[i]); - } -} - -static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray) -{ - AutoJavaFloatArray autoHSV(env, hsvArray, 3); - SkScalar* hsv = autoHSV.ptr(); - return static_cast<jint>(SkHSVToColor(alpha, hsv)); -} - /////////////////////////////////////////////////////////////////////////////////////////////// static void Shader_safeUnref(SkShader* shader) { @@ -409,11 +390,6 @@ static void RuntimeShader_updateShader(JNIEnv* env, jobject, jlong shaderBuilder /////////////////////////////////////////////////////////////////////////////////////////////// -static const JNINativeMethod gColorMethods[] = { - { "nativeRGBToHSV", "(III[F)V", (void*)Color_RGBToHSV }, - { "nativeHSVToColor", "(I[F)I", (void*)Color_HSVToColor } -}; - static const JNINativeMethod gShaderMethods[] = { { "nativeGetFinalizer", "()J", (void*)Shader_getNativeFinalizer }, }; @@ -456,8 +432,6 @@ static const JNINativeMethod gRuntimeShaderMethods[] = { int register_android_graphics_Shader(JNIEnv* env) { - android::RegisterMethodsOrDie(env, "android/graphics/Color", gColorMethods, - NELEM(gColorMethods)); android::RegisterMethodsOrDie(env, "android/graphics/Shader", gShaderMethods, NELEM(gShaderMethods)); android::RegisterMethodsOrDie(env, "android/graphics/BitmapShader", gBitmapShaderMethods, diff --git a/libs/hwui/jni/android_graphics_Color.cpp b/libs/hwui/jni/android_graphics_Color.cpp new file mode 100644 index 000000000000..c22b8b926373 --- /dev/null +++ b/libs/hwui/jni/android_graphics_Color.cpp @@ -0,0 +1,55 @@ +/* + * 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 "GraphicsJNI.h" + +#include "SkColor.h" + +using namespace android; + +static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue, + jfloatArray hsvArray) +{ + SkScalar hsv[3]; + SkRGBToHSV(red, green, blue, hsv); + + AutoJavaFloatArray autoHSV(env, hsvArray, 3); + float* values = autoHSV.ptr(); + for (int i = 0; i < 3; i++) { + values[i] = SkScalarToFloat(hsv[i]); + } +} + +static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray) +{ + AutoJavaFloatArray autoHSV(env, hsvArray, 3); + SkScalar* hsv = autoHSV.ptr(); + return static_cast<jint>(SkHSVToColor(alpha, hsv)); +} + +static const JNINativeMethod gColorMethods[] = { + { "nativeRGBToHSV", "(III[F)V", (void*)Color_RGBToHSV }, + { "nativeHSVToColor", "(I[F)I", (void*)Color_HSVToColor } +}; + +namespace android { + +int register_android_graphics_Color(JNIEnv* env) { + return android::RegisterMethodsOrDie(env, "android/graphics/Color", gColorMethods, + NELEM(gColorMethods)); +} + +}; // namespace android diff --git a/libs/hwui/jni/android_graphics_ColorSpace.cpp b/libs/hwui/jni/android_graphics_ColorSpace.cpp index 63d3f83febd6..d06206be90d7 100644 --- a/libs/hwui/jni/android_graphics_ColorSpace.cpp +++ b/libs/hwui/jni/android_graphics_ColorSpace.cpp @@ -148,7 +148,7 @@ static const JNINativeMethod gColorSpaceRgbMethods[] = { namespace android { int register_android_graphics_ColorSpace(JNIEnv* env) { - return android::RegisterMethodsOrDie(env, "android/graphics/ColorSpace$Rgb", + return android::RegisterMethodsOrDie(env, "android/graphics/ColorSpace$Rgb$Native", gColorSpaceRgbMethods, NELEM(gColorSpaceRgbMethods)); } 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_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp index ca667b0d09bc..eedc069ed01b 100644 --- a/libs/hwui/jni/android_graphics_Matrix.cpp +++ b/libs/hwui/jni/android_graphics_Matrix.cpp @@ -326,9 +326,6 @@ public: }; static const JNINativeMethod methods[] = { - {"nGetNativeFinalizer", "()J", (void*) SkMatrixGlue::getNativeFinalizer}, - {"nCreate","(J)J", (void*) SkMatrixGlue::create}, - // ------- @FastNative below here --------------- {"nMapPoints","(J[FI[FIIZ)V", (void*) SkMatrixGlue::mapPoints}, {"nMapRect","(JLandroid/graphics/RectF;Landroid/graphics/RectF;)Z", @@ -376,11 +373,21 @@ static const JNINativeMethod methods[] = { {"nEquals", "(JJ)Z", (void*) SkMatrixGlue::equals} }; +static const JNINativeMethod extra_methods[] = { + {"nGetNativeFinalizer", "()J", (void*)SkMatrixGlue::getNativeFinalizer}, + {"nCreate", "(J)J", (void*)SkMatrixGlue::create}, +}; + static jclass sClazz; static jfieldID sNativeInstanceField; static jmethodID sCtor; int register_android_graphics_Matrix(JNIEnv* env) { + // Methods only used on Ravenwood (for now). See the javadoc on Matrix$ExtraNativesx + // for why we need it. + RegisterMethodsOrDie(env, "android/graphics/Matrix$ExtraNatives", extra_methods, + NELEM(extra_methods)); + int result = RegisterMethodsOrDie(env, "android/graphics/Matrix", methods, NELEM(methods)); jclass clazz = FindClassOrDie(env, "android/graphics/Matrix"); 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.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index b87002371775..8e07a2f31de1 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -15,14 +15,18 @@ */ #include "ShaderCache.h" + #include <GrDirectContext.h> #include <SkData.h> #include <gui/TraceUtils.h> #include <log/log.h> #include <openssl/sha.h> + #include <algorithm> #include <array> +#include <mutex> #include <thread> + #include "FileBlobCache.h" #include "Properties.h" 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..8bb11badb607 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; @@ -994,7 +1010,15 @@ void CanvasContext::destroyHardwareResources() { } void CanvasContext::onContextDestroyed() { - destroyHardwareResources(); + // We don't want to destroyHardwareResources as that will invalidate display lists which + // the client may not be expecting. Instead just purge all scratch resources + if (mRenderPipeline->isContextReady()) { + freePrefetchedLayers(); + for (const sp<RenderNode>& node : mRenderNodes) { + node->destroyLayers(); + } + mRenderPipeline->onDestroyHardwareResources(); + } } DeferredLayerUpdater* CanvasContext::createTextureLayer() { 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..7a155c583fd4 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> @@ -44,11 +45,12 @@ void HintSessionWrapper::HintSessionBinding::init() { LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!"); BIND_APH_METHOD(getManager); - BIND_APH_METHOD(createSession); + BIND_APH_METHOD(createSessionInternal); BIND_APH_METHOD(closeSession); 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,17 +112,18 @@ 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)] { - return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos); + mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] { + return mBinding->createSessionInternal(manager, tids.data(), tids.size(), + targetDurationNanos, SessionTag::HWUI); }); return false; } @@ -143,6 +150,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..859cc57dea9f 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -17,9 +17,11 @@ #pragma once #include <android/performance_hint.h> +#include <private/performance_hint_private.h> #include <future> #include <optional> +#include <vector> #include "utils/TimeUtils.h" @@ -47,11 +49,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 +65,8 @@ private: pid_t mUiThreadId; pid_t mRenderThreadId; + std::vector<pid_t> mPermanentSessionTids; + std::vector<pid_t> mActiveFunctorTids; bool mSessionValid = true; @@ -73,15 +81,18 @@ private: virtual ~HintSessionBinding() = default; virtual void init(); APerformanceHintManager* (*getManager)(); - APerformanceHintSession* (*createSession)(APerformanceHintManager* manager, - const int32_t* tids, size_t tidCount, - int64_t defaultTarget) = nullptr; + APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager, + const int32_t* tids, size_t tidCount, + int64_t defaultTarget, + SessionTag tag) = nullptr; void (*closeSession)(APerformanceHintSession* session) = nullptr; void (*updateTargetWorkDuration)(APerformanceHintSession* session, int64_t targetDuration) = nullptr; 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/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index a8e85475aff0..0f29613cad33 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -322,11 +322,16 @@ bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& window return false; } - err = native_window_set_buffer_count(window, windowInfo.bufferCount); - if (err != 0) { - ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffer_count(%zu) failed: %s (%d)", - windowInfo.bufferCount, strerror(-err), err); - return false; + // If bufferCount == 1 then we're in shared buffer mode and we cannot actually call + // set_buffer_count, it'll just fail. + if (windowInfo.bufferCount > 1) { + err = native_window_set_buffer_count(window, windowInfo.bufferCount); + if (err != 0) { + ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffer_count(%zu) failed: %s " + "(%d)", + windowInfo.bufferCount, strerror(-err), err); + return false; + } } err = native_window_set_usage(window, windowInfo.windowUsageFlags); diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp index 10a740a1f803..a8db0f4aa4f0 100644 --- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp +++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp @@ -52,12 +52,13 @@ protected: void init() override; MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ()); - MOCK_METHOD(APerformanceHintSession*, fakeCreateSession, - (APerformanceHintManager*, const int32_t*, size_t, int64_t)); + MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal, + (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag)); MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*)); 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; }; @@ -71,22 +72,28 @@ protected: // Must be static so we can point to them as normal fn pointers with HintSessionBinding static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); }; - static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, size_t idsSize, + int64_t initialTarget, + SessionTag tag) { + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubManagedCreateSessionInternal( + APerformanceHintManager* manager, const int32_t* ids, size_t idsSize, + int64_t initialTarget, SessionTag tag) { sMockBinding->allowCreationToFinish.get_future().wait(); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, + size_t idsSize, + int64_t initialTarget, + SessionTag tag) { std::this_thread::sleep_for(50ms); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } static void stubCloseSession(APerformanceHintSession* session) { sMockBinding->fakeCloseSession(session); @@ -102,11 +109,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 @@ -129,18 +145,20 @@ void HintSessionWrapperTests::SetUp() { mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId); mWrapper->mBinding = sMockBinding; EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr)); - ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr)); + ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr)); + ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0)); } void HintSessionWrapperTests::MockHintSessionBinding::init() { sMockBinding->getManager = &stubGetManager; - if (sMockBinding->createSession == nullptr) { - sMockBinding->createSession = &stubCreateSession; + if (sMockBinding->createSessionInternal == nullptr) { + sMockBinding->createSessionInternal = &stubCreateSessionInternal; } sMockBinding->closeSession = &stubCloseSession; sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration; sMockBinding->reportActualWorkDuration = &stubReportActualWorkDuration; sMockBinding->sendHint = &stubSendHint; + sMockBinding->setThreads = &stubSetThreads; } void HintSessionWrapperTests::TearDown() { @@ -151,14 +169,14 @@ void HintSessionWrapperTests::TearDown() { TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) { EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = stubSlowCreateSession; + sMockBinding->createSessionInternal = stubSlowCreateSessionInternal; mWrapper->init(); mWrapper = nullptr; Mock::VerifyAndClearExpectations(sMockBinding.get()); } TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) { - EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1); mWrapper->init(); waitForWrapperReady(); } @@ -207,7 +225,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesBeforeAsyncCreationFinish // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens after EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -234,7 +252,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens before EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -339,4 +357,44 @@ TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct EXPECT_EQ(mWrapper->alive(), false); } +TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) { + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(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..6a560b365247 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}; @@ -408,7 +403,7 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) { } static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) { - float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g); + float pow_gain_ginv = std::pow(gain, 1 / trfn.g); skcms_TransferFunction result; result.g = trfn.g; result.a = trfn.a * pow_gain_ginv; 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/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index 6a465442c2b4..f1ee3256dbee 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -117,7 +117,7 @@ FloatPoint MouseCursorController::getPosition() const { return {mLocked.pointerX, mLocked.pointerY}; } -int32_t MouseCursorController::getDisplayId() const { +ui::LogicalDisplayId MouseCursorController::getDisplayId() const { std::scoped_lock lock(mLock); return mLocked.viewport.displayId; } @@ -467,10 +467,10 @@ void MouseCursorController::startAnimationLocked() REQUIRES(mLock) { std::function<bool(nsecs_t)> func = std::bind(&MouseCursorController::doAnimations, this, _1); /* - * Using -1 for displayId here to avoid removing the callback + * Using ui::LogicalDisplayId::INVALID for displayId here to avoid removing the callback * if a TouchSpotController with the same display is removed. */ - mContext.addAnimationCallback(-1, func); + mContext.addAnimationCallback(ui::LogicalDisplayId::INVALID, func); } } // namespace android diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index 00dc0854440e..dc7e8ca16c8a 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -47,7 +47,7 @@ public: void move(float deltaX, float deltaY); void setPosition(float x, float y); FloatPoint getPosition() const; - int32_t getDisplayId() const; + ui::LogicalDisplayId getDisplayId() const; void fade(PointerControllerInterface::Transition transition); void unfade(PointerControllerInterface::Transition transition); void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index f9dc5fac7e21..cca1b07c3118 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -24,7 +24,6 @@ #include <SkColor.h> #include <android-base/stringprintf.h> #include <android-base/thread_annotations.h> -#include <com_android_input_flags.h> #include <ftl/enum.h> #include <mutex> @@ -35,14 +34,10 @@ #define INDENT2 " " #define INDENT3 " " -namespace input_flags = com::android::input::flags; - namespace android { namespace { -static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer(); - const ui::Transform kIdentityTransform; } // namespace @@ -68,27 +63,24 @@ void PointerController::DisplayInfoListener::onPointerControllerDestroyed() { std::shared_ptr<PointerController> PointerController::create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, ControllerType type) { + SpriteController& spriteController, ControllerType type) { // using 'new' to access non-public constructor std::shared_ptr<PointerController> controller; switch (type) { case ControllerType::MOUSE: controller = std::shared_ptr<PointerController>( - new MousePointerController(policy, looper, spriteController, enabled)); + new MousePointerController(policy, looper, spriteController)); break; case ControllerType::TOUCH: controller = std::shared_ptr<PointerController>( - new TouchPointerController(policy, looper, spriteController, enabled)); + new TouchPointerController(policy, looper, spriteController)); break; case ControllerType::STYLUS: controller = std::shared_ptr<PointerController>( - new StylusPointerController(policy, looper, spriteController, enabled)); + new StylusPointerController(policy, looper, spriteController)); break; - case ControllerType::LEGACY: default: - controller = std::shared_ptr<PointerController>( - new PointerController(policy, looper, spriteController, enabled)); - break; + LOG_ALWAYS_FATAL("Invalid ControllerType: %d", static_cast<int>(type)); } /* @@ -108,10 +100,9 @@ std::shared_ptr<PointerController> PointerController::create( } PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled) + const sp<Looper>& looper, SpriteController& spriteController) : PointerController( - policy, looper, spriteController, enabled, + policy, looper, spriteController, [](const sp<android::gui::WindowInfosListener>& listener) { auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{}, std::vector<android::gui::DisplayInfo>{}); @@ -125,11 +116,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, - bool enabled, const WindowListenerRegisterConsumer& registerListener, WindowListenerUnregisterConsumer unregisterListener) - : mEnabled(enabled), - mContext(policy, looper, spriteController, *this), + : mContext(policy, looper, spriteController, *this), mCursorController(mContext), mDisplayInfoListener(sp<DisplayInfoListener>::make(this)), mUnregisterWindowInfosListener(std::move(unregisterListener)) { @@ -142,7 +131,6 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::~PointerController() { mDisplayInfoListener->onPointerControllerDestroyed(); mUnregisterWindowInfosListener(mDisplayInfoListener); - mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0}); } std::mutex& PointerController::getLock() const { @@ -150,15 +138,11 @@ std::mutex& PointerController::getLock() const { } std::optional<FloatRect> PointerController::getBounds() const { - if (!mEnabled) return {}; - return mCursorController.getBounds(); } void PointerController::move(float deltaX, float deltaY) { - if (!mEnabled) return; - - const int32_t displayId = mCursorController.getDisplayId(); + const ui::LogicalDisplayId displayId = mCursorController.getDisplayId(); vec2 transformed; { std::scoped_lock lock(getLock()); @@ -169,9 +153,7 @@ void PointerController::move(float deltaX, float deltaY) { } void PointerController::setPosition(float x, float y) { - if (!mEnabled) return; - - const int32_t displayId = mCursorController.getDisplayId(); + const ui::LogicalDisplayId displayId = mCursorController.getDisplayId(); vec2 transformed; { std::scoped_lock lock(getLock()); @@ -182,11 +164,7 @@ void PointerController::setPosition(float x, float y) { } FloatPoint PointerController::getPosition() const { - if (!mEnabled) { - return FloatPoint{0, 0}; - } - - const int32_t displayId = mCursorController.getDisplayId(); + const ui::LogicalDisplayId displayId = mCursorController.getDisplayId(); const auto p = mCursorController.getPosition(); { std::scoped_lock lock(getLock()); @@ -195,29 +173,21 @@ FloatPoint PointerController::getPosition() const { } } -int32_t PointerController::getDisplayId() const { - if (!mEnabled) return ADISPLAY_ID_NONE; - +ui::LogicalDisplayId PointerController::getDisplayId() const { return mCursorController.getDisplayId(); } void PointerController::fade(Transition transition) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.fade(transition); } void PointerController::unfade(Transition transition) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.unfade(transition); } void PointerController::setPresentation(Presentation presentation) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); if (mLocked.presentation == presentation) { @@ -226,33 +196,13 @@ void PointerController::setPresentation(Presentation presentation) { mLocked.presentation = presentation; - if (ENABLE_POINTER_CHOREOGRAPHER) { - // When pointer choreographer is enabled, the presentation mode is only set once when the - // PointerController is constructed, before the display viewport is provided. - // TODO(b/293587049): Clean up the PointerController interface after pointer choreographer - // is permanently enabled. The presentation can be set in the constructor. - mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); - return; - } - - if (!mCursorController.isViewportValid()) { - return; - } - - if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) { - // For now, we support stylus hover using the mouse cursor implementation. - // TODO: Add proper support for stylus hover icons. - mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); - - mCursorController.getAdditionalMouseResources(); - clearSpotsLocked(); - } + // The presentation mode is only set once when the PointerController is constructed, + // before the display viewport is provided. + mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); } void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, - BitSet32 spotIdBits, int32_t displayId) { - if (!mEnabled) return; - + BitSet32 spotIdBits, ui::LogicalDisplayId displayId) { std::scoped_lock lock(getLock()); std::array<PointerCoords, MAX_POINTERS> outSpotCoords{}; const ui::Transform& transform = getTransformForDisplayLocked(displayId); @@ -272,12 +222,13 @@ void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t if (it == mLocked.spotControllers.end()) { mLocked.spotControllers.try_emplace(displayId, displayId, mContext); } - mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits); + bool skipScreenshot = mLocked.displaysToSkipScreenshot.find(displayId) != + mLocked.displaysToSkipScreenshot.end(); + mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits, + skipScreenshot); } void PointerController::clearSpots() { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); clearSpotsLocked(); } @@ -310,12 +261,6 @@ void PointerController::reloadPointerResources() { } void PointerController::setDisplayViewport(const DisplayViewport& viewport) { - struct PointerDisplayChangeArgs { - int32_t displayId; - FloatPoint cursorPosition; - }; - std::optional<PointerDisplayChangeArgs> pointerDisplayChanged; - { // acquire lock std::scoped_lock lock(getLock()); @@ -327,44 +272,42 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) { mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); if (viewport.displayId != mLocked.pointerDisplayId) { mLocked.pointerDisplayId = viewport.displayId; - pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()}; } } // release lock - - if (pointerDisplayChanged) { - // Notify the policy without holding the pointer controller lock. - mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId, - pointerDisplayChanged->cursorPosition); - } } void PointerController::updatePointerIcon(PointerIconStyle iconId) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.updatePointerIcon(iconId); } void PointerController::setCustomPointerIcon(const SpriteIcon& icon) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.setCustomPointerIcon(icon); } +void PointerController::setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) { + std::scoped_lock lock(getLock()); + if (skip) { + mLocked.displaysToSkipScreenshot.insert(displayId); + } else { + mLocked.displaysToSkipScreenshot.erase(displayId); + } +} + void PointerController::doInactivityTimeout() { fade(Transition::GRADUAL); } void PointerController::onDisplayViewportsUpdated(const std::vector<DisplayViewport>& viewports) { - std::unordered_set<int32_t> displayIdSet; + std::unordered_set<ui::LogicalDisplayId> displayIdSet; for (const DisplayViewport& viewport : viewports) { displayIdSet.insert(viewport.displayId); } std::scoped_lock lock(getLock()); for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) { - int32_t displayId = it->first; + ui::LogicalDisplayId displayId = it->first; if (!displayIdSet.count(displayId)) { /* * Ensures that an in-progress animation won't dereference @@ -383,7 +326,8 @@ void PointerController::onDisplayInfosChangedLocked( mLocked.mDisplayInfos = displayInfo; } -const ui::Transform& PointerController::getTransformForDisplayLocked(int displayId) const { +const ui::Transform& PointerController::getTransformForDisplayLocked( + ui::LogicalDisplayId displayId) const { const auto& di = mLocked.mDisplayInfos; auto it = std::find_if(di.begin(), di.end(), [displayId](const gui::DisplayInfo& info) { return info.displayId == displayId; @@ -392,15 +336,12 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display } std::string PointerController::dump() { - if (!mEnabled) { - return INDENT "PointerController: DISABLED due to ongoing PointerChoreographer refactor\n"; - } - std::string dump = INDENT "PointerController:\n"; std::scoped_lock lock(getLock()); dump += StringPrintf(INDENT2 "Presentation: %s\n", ftl::enum_string(mLocked.presentation).c_str()); - dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId); + dump += StringPrintf(INDENT2 "Pointer Display ID: %s\n", + mLocked.pointerDisplayId.toString().c_str()); dump += StringPrintf(INDENT2 "Viewports:\n"); for (const auto& info : mLocked.mDisplayInfos) { info.dump(dump, INDENT3); @@ -416,8 +357,8 @@ std::string PointerController::dump() { MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::POINTER); } @@ -429,8 +370,8 @@ MousePointerController::~MousePointerController() { TouchPointerController::TouchPointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::SPOT); } @@ -442,8 +383,8 @@ TouchPointerController::~TouchPointerController() { StylusPointerController::StylusPointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::STYLUS_HOVER); } diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index 6ee5707622ca..c6430f7f36ff 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -47,8 +47,7 @@ class PointerController : public PointerControllerInterface { public: static std::shared_ptr<PointerController> create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, - ControllerType type = ControllerType::LEGACY); + SpriteController& spriteController, ControllerType type); ~PointerController() override; @@ -56,17 +55,18 @@ public: void move(float deltaX, float deltaY) override; void setPosition(float x, float y) override; FloatPoint getPosition() const override; - int32_t getDisplayId() const override; + ui::LogicalDisplayId getDisplayId() const override; void fade(Transition transition) override; void unfade(Transition transition) override; void setDisplayViewport(const DisplayViewport& viewport) override; void setPresentation(Presentation presentation) override; void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, - BitSet32 spotIdBits, int32_t displayId) override; + BitSet32 spotIdBits, ui::LogicalDisplayId displayId) override; void clearSpots() override; void updatePointerIcon(PointerIconStyle iconId) override; void setCustomPointerIcon(const SpriteIcon& icon) override; + void setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) override; virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); @@ -86,12 +86,12 @@ protected: // Constructor used to test WindowInfosListener registration. PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, + SpriteController& spriteController, const WindowListenerRegisterConsumer& registerListener, WindowListenerUnregisterConsumer unregisterListener); PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled); + SpriteController& spriteController); private: friend PointerControllerContext::LooperCallback; @@ -103,18 +103,17 @@ private: // we use the DisplayInfoListener's lock in PointerController. std::mutex& getLock() const; - const bool mEnabled; - PointerControllerContext mContext; MouseCursorController mCursorController; struct Locked { Presentation presentation; - int32_t pointerDisplayId = ADISPLAY_ID_NONE; + ui::LogicalDisplayId pointerDisplayId = ui::LogicalDisplayId::INVALID; std::vector<gui::DisplayInfo> mDisplayInfos; - std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers; + std::unordered_map<ui::LogicalDisplayId, TouchSpotController> spotControllers; + std::unordered_set<ui::LogicalDisplayId> displaysToSkipScreenshot; } mLocked GUARDED_BY(getLock()); class DisplayInfoListener : public gui::WindowInfosListener { @@ -133,7 +132,8 @@ private: sp<DisplayInfoListener> mDisplayInfoListener; const WindowListenerUnregisterConsumer mUnregisterWindowInfosListener; - const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock()); + const ui::Transform& getTransformForDisplayLocked(ui::LogicalDisplayId displayId) const + REQUIRES(getLock()); void clearSpotsLocked() REQUIRES(getLock()); }; @@ -142,15 +142,14 @@ class MousePointerController : public PointerController { public: /** A version of PointerController that controls one mouse pointer. */ MousePointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~MousePointerController() override; void setPresentation(Presentation) override { LOG_ALWAYS_FATAL("Should not be called"); } - void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override { + void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override { LOG_ALWAYS_FATAL("Should not be called"); } void clearSpots() override { @@ -162,8 +161,7 @@ class TouchPointerController : public PointerController { public: /** A version of PointerController that controls touch spots. */ TouchPointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~TouchPointerController() override; @@ -179,7 +177,7 @@ public: FloatPoint getPosition() const override { LOG_ALWAYS_FATAL("Should not be called"); } - int32_t getDisplayId() const override { + ui::LogicalDisplayId getDisplayId() const override { LOG_ALWAYS_FATAL("Should not be called"); } void fade(Transition) override { @@ -208,15 +206,14 @@ class StylusPointerController : public PointerController { public: /** A version of PointerController that controls one stylus pointer. */ StylusPointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~StylusPointerController() override; void setPresentation(Presentation) override { LOG_ALWAYS_FATAL("Should not be called"); } - void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override { + void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override { LOG_ALWAYS_FATAL("Should not be called"); } void clearSpots() override { diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp index 15c35176afce..747eb8e5ad1b 100644 --- a/libs/input/PointerControllerContext.cpp +++ b/libs/input/PointerControllerContext.cpp @@ -138,12 +138,12 @@ int PointerControllerContext::LooperCallback::handleEvent(int /* fd */, int even return 1; // keep the callback } -void PointerControllerContext::addAnimationCallback(int32_t displayId, +void PointerControllerContext::addAnimationCallback(ui::LogicalDisplayId displayId, std::function<bool(nsecs_t)> callback) { mAnimator.addCallback(displayId, callback); } -void PointerControllerContext::removeAnimationCallback(int32_t displayId) { +void PointerControllerContext::removeAnimationCallback(ui::LogicalDisplayId displayId) { mAnimator.removeCallback(displayId); } @@ -161,14 +161,14 @@ void PointerControllerContext::PointerAnimator::initializeDisplayEventReceiver() } } -void PointerControllerContext::PointerAnimator::addCallback(int32_t displayId, +void PointerControllerContext::PointerAnimator::addCallback(ui::LogicalDisplayId displayId, std::function<bool(nsecs_t)> callback) { std::scoped_lock lock(mLock); mLocked.callbacks[displayId] = callback; startAnimationLocked(); } -void PointerControllerContext::PointerAnimator::removeCallback(int32_t displayId) { +void PointerControllerContext::PointerAnimator::removeCallback(ui::LogicalDisplayId displayId) { std::scoped_lock lock(mLock); auto it = mLocked.callbacks.find(displayId); if (it == mLocked.callbacks.end()) { diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index 98c3988e7df4..d42214883d3a 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -72,16 +72,16 @@ protected: virtual ~PointerControllerPolicyInterface() {} public: - virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) = 0; - virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) = 0; + virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) = 0; + virtual void loadPointerResources(PointerResources* outResources, + ui::LogicalDisplayId displayId) = 0; virtual void loadAdditionalMouseResources( std::map<PointerIconStyle, SpriteIcon>* outResources, std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, - int32_t displayId) = 0; + ui::LogicalDisplayId displayId) = 0; virtual PointerIconStyle getDefaultPointerIconId() = 0; virtual PointerIconStyle getDefaultStylusIconId() = 0; virtual PointerIconStyle getCustomPointerIconId() = 0; - virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0; }; /* @@ -103,7 +103,7 @@ public: nsecs_t getAnimationTime(); - void clearSpotsByDisplay(int32_t displayId); + void clearSpotsByDisplay(ui::LogicalDisplayId displayId); void setHandlerController(std::shared_ptr<PointerController> controller); void setCallbackController(std::shared_ptr<PointerController> controller); @@ -113,8 +113,9 @@ public: void handleDisplayEvents(); - void addAnimationCallback(int32_t displayId, std::function<bool(nsecs_t)> callback); - void removeAnimationCallback(int32_t displayId); + void addAnimationCallback(ui::LogicalDisplayId displayId, + std::function<bool(nsecs_t)> callback); + void removeAnimationCallback(ui::LogicalDisplayId displayId); class MessageHandler : public virtual android::MessageHandler { public: @@ -137,8 +138,8 @@ private: public: PointerAnimator(PointerControllerContext& context); - void addCallback(int32_t displayId, std::function<bool(nsecs_t)> callback); - void removeCallback(int32_t displayId); + void addCallback(ui::LogicalDisplayId displayId, std::function<bool(nsecs_t)> callback); + void removeCallback(ui::LogicalDisplayId displayId); void handleVsyncEvents(); nsecs_t getAnimationTimeLocked(); @@ -149,7 +150,7 @@ private: bool animationPending{false}; nsecs_t animationTime{systemTime(SYSTEM_TIME_MONOTONIC)}; - std::unordered_map<int32_t, std::function<bool(nsecs_t)>> callbacks; + std::unordered_map<ui::LogicalDisplayId, std::function<bool(nsecs_t)>> callbacks; } mLocked GUARDED_BY(mLock); DisplayEventReceiver mDisplayEventReceiver; diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index 6dc45a6aebec..af499390d390 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -19,9 +19,9 @@ #include "SpriteController.h" -#include <log/log.h> -#include <utils/String8.h> +#include <android-base/logging.h> #include <gui/Surface.h> +#include <utils/String8.h> namespace android { @@ -129,7 +129,7 @@ void SpriteController::doUpdateSprites() { update.state.surfaceVisible = false; update.state.surfaceControl = obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight, - update.state.displayId); + update.state.displayId, update.state.skipScreenshot); if (update.state.surfaceControl != NULL) { update.surfaceChanged = surfaceChanged = true; } @@ -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 | DIRTY_SKIP_SCREENSHOT))))) { 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 @@ -255,6 +260,14 @@ void SpriteController::doUpdateSprites() { t.setLayer(update.state.surfaceControl, surfaceLayer); } + if (wantSurfaceVisibleAndDrawn && + (becomingVisible || (update.state.dirty & DIRTY_SKIP_SCREENSHOT))) { + int32_t flags = + update.state.skipScreenshot ? ISurfaceComposerClient::eSkipScreenshot : 0; + t.setFlags(update.state.surfaceControl, flags, + ISurfaceComposerClient::eSkipScreenshot); + } + if (becomingVisible) { t.show(update.state.surfaceControl); @@ -328,19 +341,22 @@ void SpriteController::ensureSurfaceComposerClient() { } sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, - int32_t displayId) { + ui::LogicalDisplayId displayId, + bool hideOnMirrored) { ensureSurfaceComposerClient(); const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId); if (parent == nullptr) { - ALOGE("Failed to get the parent surface for pointers on display %d", displayId); + LOG(ERROR) << "Failed to get the parent surface for pointers on display " << displayId; } + int32_t createFlags = ISurfaceComposerClient::eHidden | ISurfaceComposerClient::eCursorWindow; + if (hideOnMirrored) { + createFlags |= ISurfaceComposerClient::eSkipScreenshot; + } const sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface(String8("Sprite"), width, height, - PIXEL_FORMAT_RGBA_8888, - ISurfaceComposerClient::eHidden | - ISurfaceComposerClient::eCursorWindow, + PIXEL_FORMAT_RGBA_8888, createFlags, parent ? parent->getHandle() : nullptr); if (surfaceControl == nullptr || !surfaceControl->isValid()) { ALOGE("Error creating sprite surface."); @@ -388,12 +404,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 +421,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 } @@ -459,7 +476,7 @@ void SpriteController::SpriteImpl::setTransformationMatrix( } } -void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) { +void SpriteController::SpriteImpl::setDisplayId(ui::LogicalDisplayId displayId) { AutoMutex _l(mController.mLock); if (mLocked.state.displayId != displayId) { @@ -468,6 +485,15 @@ void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) { } } +void SpriteController::SpriteImpl::setSkipScreenshot(bool skip) { + AutoMutex _l(mController.mLock); + + if (mLocked.state.skipScreenshot != skip) { + mLocked.state.skipScreenshot = skip; + invalidateLocked(DIRTY_SKIP_SCREENSHOT); + } +} + void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) { bool wasDirty = mLocked.state.dirty; mLocked.state.dirty |= dirty; diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 04ecb3895aa2..e147c567ae2d 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -95,7 +95,11 @@ public: virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0; /* Sets the id of the display where the sprite should be shown. */ - virtual void setDisplayId(int32_t displayId) = 0; + virtual void setDisplayId(ui::LogicalDisplayId displayId) = 0; + + /* Sets the flag to hide sprite on mirrored displays. + * This will add ISurfaceComposerClient::eSkipScreenshot flag to the sprite. */ + virtual void setSkipScreenshot(bool skip) = 0; }; /* @@ -111,7 +115,7 @@ public: */ class SpriteController { public: - using ParentSurfaceProvider = std::function<sp<SurfaceControl>(int /*displayId*/)>; + using ParentSurfaceProvider = std::function<sp<SurfaceControl>(ui::LogicalDisplayId)>; SpriteController(const sp<Looper>& looper, int32_t overlayLayer, ParentSurfaceProvider parent); SpriteController(const SpriteController&) = delete; SpriteController& operator=(const SpriteController&) = delete; @@ -151,6 +155,8 @@ private: DIRTY_HOTSPOT = 1 << 6, DIRTY_DISPLAY_ID = 1 << 7, DIRTY_ICON_STYLE = 1 << 8, + DIRTY_DRAW_DROP_SHADOW = 1 << 9, + DIRTY_SKIP_SCREENSHOT = 1 << 10, }; /* Describes the state of a sprite. @@ -159,28 +165,23 @@ private: * on the sprites for a long time. * Note that the SpriteIcon holds a reference to a shared (and immutable) bitmap. */ struct SpriteState { - inline SpriteState() : - dirty(0), visible(false), - positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT), - surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) { - } - - uint32_t dirty; + uint32_t dirty{0}; SpriteIcon icon; - bool visible; - float positionX; - float positionY; - int32_t layer; - float alpha; + bool visible{false}; + float positionX{0}; + float positionY{0}; + int32_t layer{0}; + float alpha{1.0f}; SpriteTransformationMatrix transformationMatrix; - int32_t displayId; + ui::LogicalDisplayId displayId{ui::LogicalDisplayId::DEFAULT}; sp<SurfaceControl> surfaceControl; - int32_t surfaceWidth; - int32_t surfaceHeight; - bool surfaceDrawn; - bool surfaceVisible; + int32_t surfaceWidth{0}; + int32_t surfaceHeight{0}; + bool surfaceDrawn{false}; + bool surfaceVisible{false}; + bool skipScreenshot{false}; inline bool wantSurfaceVisible() const { return visible && alpha > 0.0f && icon.isValid(); @@ -207,7 +208,8 @@ private: virtual void setLayer(int32_t layer); virtual void setAlpha(float alpha); virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix); - virtual void setDisplayId(int32_t displayId); + virtual void setDisplayId(ui::LogicalDisplayId displayId); + virtual void setSkipScreenshot(bool skip); inline const SpriteState& getStateLocked() const { return mLocked.state; @@ -271,7 +273,8 @@ private: void doDisposeSurfaces(); void ensureSurfaceComposerClient(); - sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId); + sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, ui::LogicalDisplayId displayId, + bool hideOnMirrored); }; } // namespace android 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(); } diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp index 99952aa14904..7462481f8779 100644 --- a/libs/input/TouchSpotController.cpp +++ b/libs/input/TouchSpotController.cpp @@ -40,12 +40,13 @@ namespace android { // --- Spot --- void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float newX, float newY, - int32_t displayId) { + ui::LogicalDisplayId displayId, bool skipScreenshot) { sprite->setLayer(Sprite::BASE_LAYER_SPOT + id); sprite->setAlpha(alpha); sprite->setTransformationMatrix(SpriteTransformationMatrix(scale, 0.0f, 0.0f, scale)); sprite->setPosition(newX, newY); sprite->setDisplayId(displayId); + sprite->setSkipScreenshot(skipScreenshot); x = newX; y = newY; @@ -68,7 +69,8 @@ void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const // --- TouchSpotController --- -TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context) +TouchSpotController::TouchSpotController(ui::LogicalDisplayId displayId, + PointerControllerContext& context) : mDisplayId(displayId), mContext(context) { mContext.getPolicy()->loadPointerResources(&mResources, mDisplayId); } @@ -84,7 +86,7 @@ TouchSpotController::~TouchSpotController() { } void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, - BitSet32 spotIdBits) { + BitSet32 spotIdBits, bool skipScreenshot) { #if DEBUG_SPOT_UPDATES ALOGD("setSpots: idBits=%08x", spotIdBits.value); for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) { @@ -93,7 +95,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32 const PointerCoords& c = spotCoords[spotIdToIndex[id]]; ALOGD(" spot %d: position=(%0.3f, %0.3f), pressure=%0.3f, displayId=%" PRId32 ".", id, c.getAxisValue(AMOTION_EVENT_AXIS_X), c.getAxisValue(AMOTION_EVENT_AXIS_Y), - c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId); + c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId.id); } #endif @@ -116,7 +118,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32 spot = createAndAddSpotLocked(id, mLocked.displaySpots); } - spot->updateSprite(&icon, x, y, mDisplayId); + spot->updateSprite(&icon, x, y, mDisplayId, skipScreenshot); } for (Spot* spot : mLocked.displaySpots) { @@ -273,7 +275,7 @@ void TouchSpotController::dump(std::string& out, const char* prefix) const { out += prefix; out += "SpotController:\n"; out += prefix; - StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId); + StringAppendF(&out, INDENT "DisplayId: %s\n", mDisplayId.toString().c_str()); std::scoped_lock lock(mLock); out += prefix; StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating)); diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h index 5bbc75d9570b..ac37fa430249 100644 --- a/libs/input/TouchSpotController.h +++ b/libs/input/TouchSpotController.h @@ -29,10 +29,10 @@ namespace android { */ class TouchSpotController { public: - TouchSpotController(int32_t displayId, PointerControllerContext& context); + TouchSpotController(ui::LogicalDisplayId displayId, PointerControllerContext& context); ~TouchSpotController(); void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, - BitSet32 spotIdBits); + BitSet32 spotIdBits, bool skipScreenshot); void clearSpots(); void reloadSpotResources(); @@ -59,14 +59,15 @@ private: y(0.0f), mLastIcon(nullptr) {} - void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId); + void updateSprite(const SpriteIcon* icon, float x, float y, ui::LogicalDisplayId displayId, + bool skipScreenshot); void dump(std::string& out, const char* prefix = "") const; private: const SpriteIcon* mLastIcon; }; - int32_t mDisplayId; + ui::LogicalDisplayId mDisplayId; mutable std::mutex mLock; diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index a1bb5b3f1cc4..2dcb1f1d1650 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include <com_android_input_flags.h> #include <flag_macros.h> #include <gmock/gmock.h> #include <gtest/gtest.h> @@ -30,8 +29,6 @@ namespace android { -namespace input_flags = com::android::input::flags; - enum TestCursorType { CURSOR_TYPE_DEFAULT = 0, CURSOR_TYPE_HOVER, @@ -55,20 +52,19 @@ std::pair<float, float> getHotSpotCoordinatesForType(int32_t type) { class MockPointerControllerPolicyInterface : public PointerControllerPolicyInterface { public: - virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) override; - virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) override; + virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) override; + virtual void loadPointerResources(PointerResources* outResources, + ui::LogicalDisplayId displayId) override; virtual void loadAdditionalMouseResources( std::map<PointerIconStyle, SpriteIcon>* outResources, std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, - int32_t displayId) override; + ui::LogicalDisplayId displayId) override; virtual PointerIconStyle getDefaultPointerIconId() override; virtual PointerIconStyle getDefaultStylusIconId() override; virtual PointerIconStyle getCustomPointerIconId() override; - virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); - std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; } private: void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType); @@ -76,16 +72,15 @@ private: bool pointerIconLoaded{false}; bool pointerResourcesLoaded{false}; bool additionalMouseResourcesLoaded{false}; - std::optional<int32_t /*displayId*/> latestPointerDisplayId; }; -void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) { +void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId) { loadPointerIconForType(icon, CURSOR_TYPE_DEFAULT); pointerIconLoaded = true; } void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources* outResources, - int32_t) { + ui::LogicalDisplayId) { loadPointerIconForType(&outResources->spotHover, CURSOR_TYPE_HOVER); loadPointerIconForType(&outResources->spotTouch, CURSOR_TYPE_TOUCH); loadPointerIconForType(&outResources->spotAnchor, CURSOR_TYPE_ANCHOR); @@ -94,7 +89,7 @@ void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources void MockPointerControllerPolicyInterface::loadAdditionalMouseResources( std::map<PointerIconStyle, SpriteIcon>* outResources, - std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t) { + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, ui::LogicalDisplayId) { SpriteIcon icon; PointerAnimation anim; @@ -146,12 +141,6 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic icon->hotSpotY = hotSpot.second; } -void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, - const FloatPoint& /*position*/ -) { - latestPointerDisplayId = displayId; -} - class TestPointerController : public PointerController { public: TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener, @@ -159,7 +148,6 @@ public: SpriteController& spriteController) : PointerController( policy, looper, spriteController, - /*enabled=*/true, [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) -> std::vector<gui::DisplayInfo> { // Register listener @@ -178,7 +166,7 @@ protected: PointerControllerTest(); ~PointerControllerTest(); - void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT); + void ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId = ui::LogicalDisplayId::DEFAULT); sp<MockSprite> mPointerSprite; sp<MockPointerControllerPolicyInterface> mPolicy; @@ -217,7 +205,7 @@ PointerControllerTest::~PointerControllerTest() { mThread.join(); } -void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) { +void PointerControllerTest::ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId) { DisplayViewport viewport; viewport.displayId = displayId; viewport.logicalRight = 1600; @@ -267,8 +255,7 @@ TEST_F(PointerControllerTest, useStylusTypeForStylusHover) { mPointerController->reloadPointerResources(); } -TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) { +TEST_F(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources) { // Setting the presentation mode before a display viewport is set will not load any resources. mPointerController->setPresentation(PointerController::Presentation::POINTER); ASSERT_TRUE(mPolicy->noResourcesAreLoaded()); @@ -278,26 +265,7 @@ TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoe ASSERT_TRUE(mPolicy->allResourcesAreLoaded()); } -TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIcon, - REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(input_flags, - enable_pointer_choreographer))) { - ensureDisplayViewportIsSet(); - mPointerController->setPresentation(PointerController::Presentation::POINTER); - mPointerController->unfade(PointerController::Transition::IMMEDIATE); - - int32_t type = CURSOR_TYPE_ADDITIONAL; - std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type); - EXPECT_CALL(*mPointerSprite, setVisible(true)); - EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); - EXPECT_CALL(*mPointerSprite, - setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)), - Field(&SpriteIcon::hotSpotX, hotspot.first), - Field(&SpriteIcon::hotSpotY, hotspot.second)))); - mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type)); -} - -TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIconWithChoreographer, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) { +TEST_F(PointerControllerTest, updatePointerIconWithChoreographer) { // When PointerChoreographer is enabled, the presentation mode is set before the viewport. mPointerController->setPresentation(PointerController::Presentation::POINTER); ensureDisplayViewportIsSet(); @@ -348,28 +316,43 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } -TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) { - EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId()) - << "A pointer display change does not occur when PointerController is created."; - - ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT); - - const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId(); - ASSERT_TRUE(lastReportedPointerDisplayId) - << "The policy is notified of a pointer display change when the viewport is first set."; - EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId) - << "Incorrect pointer display notified."; - - ensureDisplayViewportIsSet(42); - - EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId()) - << "The policy is notified when the pointer display changes."; - - // Release the PointerController. - mPointerController = nullptr; +TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) { + ensureDisplayViewportIsSet(); - EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId()) - << "The pointer display changes to invalid when PointerController is destroyed."; + PointerCoords testSpotCoords; + testSpotCoords.clear(); + testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_X, 1); + testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, 1); + BitSet32 testIdBits; + testIdBits.markBit(0); + std::array<uint32_t, MAX_POINTER_ID + 1> testIdToIndex; + + sp<MockSprite> testSpotSprite(new NiceMock<MockSprite>); + + // By default sprite is not marked secure + EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testSpotSprite)); + EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false)); + + // Update spots to sync state with sprite + mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits, + ui::LogicalDisplayId::DEFAULT); + testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); + + // Marking the display to skip screenshot should update sprite as well + mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, true); + EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true)); + + // Update spots to sync state with sprite + mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits, + ui::LogicalDisplayId::DEFAULT); + testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); + + // Reset flag and verify again + mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, false); + EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false)); + mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits, + ui::LogicalDisplayId::DEFAULT); + testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); } class PointerControllerWindowInfoListenerTest : public Test {}; diff --git a/libs/input/tests/mocks/MockSprite.h b/libs/input/tests/mocks/MockSprite.h index 013b79c3a3bf..21628fb9f72c 100644 --- a/libs/input/tests/mocks/MockSprite.h +++ b/libs/input/tests/mocks/MockSprite.h @@ -33,7 +33,8 @@ public: MOCK_METHOD(void, setLayer, (int32_t), (override)); MOCK_METHOD(void, setAlpha, (float), (override)); MOCK_METHOD(void, setTransformationMatrix, (const SpriteTransformationMatrix&), (override)); - MOCK_METHOD(void, setDisplayId, (int32_t), (override)); + MOCK_METHOD(void, setDisplayId, (ui::LogicalDisplayId), (override)); + MOCK_METHOD(void, setSkipScreenshot, (bool), (override)); }; } // namespace android diff --git a/libs/input/tests/mocks/MockSpriteController.h b/libs/input/tests/mocks/MockSpriteController.h index 62f1d65e77a5..9ef6b7c3b480 100644 --- a/libs/input/tests/mocks/MockSpriteController.h +++ b/libs/input/tests/mocks/MockSpriteController.h @@ -27,7 +27,7 @@ class MockSpriteController : public SpriteController { public: MockSpriteController(sp<Looper> looper) - : SpriteController(looper, 0, [](int) { return nullptr; }) {} + : SpriteController(looper, 0, [](ui::LogicalDisplayId) { return nullptr; }) {} ~MockSpriteController() {} MOCK_METHOD(sp<Sprite>, createSprite, (), (override)); |