diff options
Diffstat (limited to 'libs')
667 files changed, 34894 insertions, 9642 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..ecf47209a802 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -16,15 +16,20 @@ 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.os.SystemProperties; 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 +43,59 @@ 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 value of the system property that indicates no override is set. + */ + private static final int NO_LEVEL_OVERRIDE = -1; + + /** + * 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, generateLogMessage()); + } + + private String generateLogMessage() { + final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, " + + "vendor API level=" + mVersion); + final int levelOverride = getLevelOverride(); + if (levelOverride != NO_LEVEL_OVERRIDE) { + logBuilder.append(", override to ").append(levelOverride); + } + logBuilder.append(", activity embedding enabled=").append(mIsActivityEmbeddingEnabled); + return logBuilder.toString(); } // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 5; + final int levelOverride = getLevelOverride(); + return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion; + } + + private int getLevelOverride() { + return SystemProperties.getInt("persist.wm.debug.ext_version_override", NO_LEVEL_OVERRIDE); } @NonNull @@ -74,8 +113,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 +130,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 +141,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 +183,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..290fefa5abfa --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -0,0 +1,1413 @@ +/* + * 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.content.pm.ActivityInfo.CONFIG_DENSITY; +import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; +import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION; +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.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. + final TaskFragmentContainer primaryContainer = + topSplitContainer.getPrimaryContainer(); + final TaskFragmentContainer secondaryContainer = + topSplitContainer.getSecondaryContainer(); + + // 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, primaryContainer); + return; + } + + // Update the decor surface owner if needed. + boolean isDraggableExpandType = + SplitAttributesHelper.isDraggableExpandType(splitAttributes); + final TaskFragmentContainer decorSurfaceOwnerContainer = + isDraggableExpandType ? secondaryContainer : primaryContainer; + + if (!Objects.equals( + mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) { + createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer); + } + + final Configuration parentConfiguration = parentInfo.getConfiguration(); + final Rect taskBounds = parentConfiguration.windowConfiguration.getBounds(); + final boolean isVerticalSplit = isVerticalSplit(splitAttributes); + final boolean isReversedLayout = isReversedLayout(splitAttributes, parentConfiguration); + final int dividerWidthPx = getDividerWidthPx(dividerAttributes); + + updateProperties( + new Properties( + parentConfiguration, + dividerAttributes, + decorSurface, + getInitialDividerPosition( + primaryContainer, secondaryContainer, taskBounds, + dividerWidthPx, isDraggableExpandType, isVerticalSplit, + isReversedLayout), + isVerticalSplit, + isReversedLayout, + parentInfo.getDisplayId(), + isDraggableExpandType, + primaryContainer, + secondaryContainer) + ); + } + } + + @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 TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull Rect taskBounds, + int dividerWidthPx, + boolean isDraggableExpandType, + boolean isVerticalSplit, + boolean isReversedLayout) { + if (isDraggableExpandType) { + // If the secondary container is fully expanded by dragging the divider, we display the + // divider on the edge. + final int fullyExpandedPosition = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + return isReversedLayout ? fullyExpandedPosition : 0; + } else { + final Rect primaryBounds = primaryContainer.getLastRequestedBounds(); + final Rect secondaryBounds = secondaryContainer.getLastRequestedBounds(); + return isVerticalSplit + ? Math.min(primaryBounds.right, secondaryBounds.right) + : Math.min(primaryBounds.bottom, secondaryBounds.bottom); + } + } + + private static boolean isVerticalSplit(@NonNull SplitAttributes splitAttributes) { + final int layoutDirection = splitAttributes.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, mProperties.mDividerWidthPx, + mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, + calculateMinPosition(), calculateMaxPosition()); + mRenderer.setDividerPosition(mDividerPosition); + + // Convert to use screen-based coordinates to prevent lost track of motion events + // while moving divider bar and calculating dragging velocity. + event.setLocation(event.getRawX(), event.getRawY()); + final int action = event.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + 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.width() - mProperties.mDividerWidthPx + : taskBounds.height() - mProperties.mDividerWidthPx; + + final float displayDensity = getDisplayDensity(); + final boolean isDraggingToFullscreenAllowed = + isDraggingToFullscreenAllowed(mProperties.mDividerAttributes); + return dividerPositionWithPositionOptions( + dividerPosition, + minPosition, + maxPosition, + fullyExpandedPosition, + velocity, + displayDensity, + isDraggingToFullscreenAllowed); + } + + /** + * Returns the divider position given a set of position options. A snap algorithm can 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 and if dragging to fullscreen + * is allowed. + */ + @VisibleForTesting + static int dividerPositionWithPositionOptions(int dividerPosition, int minPosition, + int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, + boolean isDraggingToFullscreenAllowed) { + if (isDraggingToFullscreenAllowed) { + final float minDismissVelocityPxPerSecond = + MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; + if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { + return 0; + } + if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { + return fullyExpandedPosition; + } + } + final float minFlingVelocityPxPerSecond = + MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; + if (Math.abs(velocity) >= minFlingVelocityPxPerSecond) { + return dividerPositionForFling( + dividerPosition, minPosition, maxPosition, velocity); + } + if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { + return dividerPosition; + } + return snap( + dividerPosition, + isDraggingToFullscreenAllowed + ? new int[] {0, minPosition, maxPosition, fullyExpandedPosition} + : new int[] {minPosition, maxPosition}); + } + + /** + * Returns the closest position that is in the fling direction. + */ + private static int dividerPositionForFling(int dividerPosition, int minPosition, + int maxPosition, float velocity) { + final boolean isBackwardDirection = velocity < 0; + if (isBackwardDirection) { + return dividerPosition < maxPosition ? minPosition : maxPosition; + } else { + return dividerPosition > minPosition ? maxPosition : minPosition; + } + } + + /** + * Returns the snapped position from a list of possible positions. Currently, this method + * snaps to the closest position by distance from the divider position. + */ + 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(), + mProperties.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); + } + + @GuardedBy("mLock") + private int calculateMaxPosition() { + return calculateMaxPosition( + mProperties.mConfiguration.windowConfiguration.getBounds(), + mProperties.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() { + synchronized (mLock) { + return calculateNewSplitRatio( + mDividerPosition, + mProperties.mConfiguration.windowConfiguration.getBounds(), + mProperties.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 to v7. + return false; + } + + /** + * Returns the new split ratio of the {@link SplitContainer} based on the current divider + * position. + * + * @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(SplitAttributes)}. + * @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( + 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 int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + + final float newRatio; + if (isVerticalSplit) { + final int newPrimaryWidth = isReversedLayout + ? taskBounds.width() - (dividerPosition + dividerWidthPx) + : dividerPosition; + newRatio = 1.0f * newPrimaryWidth / usableSize; + } else { + final int newPrimaryHeight = isReversedLayout + ? taskBounds.height() - (dividerPosition + dividerWidthPx) + : dividerPosition; + 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 = + CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION; + @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; + @NonNull + private final TaskFragmentContainer mPrimaryContainer; + @NonNull + private final TaskFragmentContainer mSecondaryContainer; + private final int mDividerWidthPx; + + @VisibleForTesting + Properties( + @NonNull Configuration configuration, + @NonNull DividerAttributes dividerAttributes, + @NonNull SurfaceControl decorSurface, + int initialDividerPosition, + boolean isVerticalSplit, + boolean isReversedLayout, + int displayId, + boolean isDraggableExpandType, + @NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer) { + mConfiguration = configuration; + mDividerAttributes = dividerAttributes; + mDecorSurface = decorSurface; + mInitialDividerPosition = initialDividerPosition; + mIsVerticalSplit = isVerticalSplit; + mIsReversedLayout = isReversedLayout; + mDisplayId = displayId; + mIsDraggableExpandType = isDraggableExpandType; + mPrimaryContainer = primaryContainer; + mSecondaryContainer = secondaryContainer; + mDividerWidthPx = getDividerWidthPx(dividerAttributes); + } + + /** + * 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.mPrimaryContainer == b.mPrimaryContainer + && a.mSecondaryContainer == b.mSecondaryContainer; + } + + 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 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() { + 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(mProperties.mDividerWidthPx, mHandleWidthPx); + dividerSurfacePosition = mProperties.mIsReversedLayout + ? mDividerPosition + : mDividerPosition + mProperties.mDividerWidthPx - mDividerSurfaceWidthPx; + dividerSurfacePosition = + Math.clamp(dividerSurfacePosition, 0, + mProperties.mIsVerticalSplit + ? taskBounds.width() - mDividerSurfaceWidthPx + : taskBounds.height() - mDividerSurfaceWidthPx); + } else { + mDividerSurfaceWidthPx = mProperties.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 + final int offset = mDividerPosition - dividerSurfacePosition; + mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); + mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); + + 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); + + // Ensure that the divider layout is always LTR regardless of the locale, because we + // already considered the locale when determining the split layout direction and the + // computed divider line position always starts from the left. This only affects the + // horizontal layout and does not have any effect on the top-to-bottom layout. + mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + 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( + mProperties.mDividerWidthPx, taskBounds.height()) + : new FrameLayout.LayoutParams( + taskBounds.width(), mProperties.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) { + final Color primaryVeilColor = getContainerBackgroundColor( + mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR); + final Color secondaryVeilColor = getContainerBackgroundColor( + mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR); + t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor)) + .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor)) + .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 + mProperties.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 + mProperties.mDividerWidthPx, + taskBounds.width(), taskBounds.height()); + primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; + secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; + } + if (mPrimaryVeil != null) { + t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); + t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); + t.setVisibility(mPrimaryVeil, !primaryBounds.isEmpty()); + } + if (mSecondaryVeil != null) { + t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); + t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); + t.setVisibility(mSecondaryVeil, !secondaryBounds.isEmpty()); + } + } + + 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 038d0081ead8..f78e2b5170fc 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; @@ -48,6 +50,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.getActivityInt import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; +import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams; import android.annotation.CallbackExecutor; import android.app.Activity; @@ -56,6 +59,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 +69,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 +90,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 +106,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") @@ -126,6 +134,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); /** + * Stores the token of the associated Activity that maps to the + * {@link OverlayContainerRestoreParams} of the most recent created overlay container. + */ + @GuardedBy("mLock") + final ArrayMap<IBinder, OverlayContainerRestoreParams> mOverlayRestoreParams = new ArrayMap<>(); + + /** * A developer-defined {@link SplitAttributes} calculator to compute the current * {@link SplitAttributes} with the current device and window states. * It is registered via {@link #setSplitAttributesCalculator(Function)} @@ -161,6 +176,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 +197,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 +358,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 +427,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 +440,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) { @@ -661,11 +694,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen exception); break; case TYPE_ACTIVITY_REPARENTED_TO_TASK: + final IBinder candidateAssociatedActToken, lastOverlayToken; + if (Flags.fixPipRestoreToOverlay()) { + candidateAssociatedActToken = change.getOtherActivityToken(); + lastOverlayToken = change.getTaskFragmentToken(); + } else { + candidateAssociatedActToken = lastOverlayToken = null; + } onActivityReparentedToTask( wct, taskId, change.getActivityIntent(), - change.getActivityToken()); + change.getActivityToken(), + candidateAssociatedActToken, + lastOverlayToken); break; default: throw new IllegalArgumentException( @@ -825,13 +867,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); return; } + + 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. + if (taskContainer.isVisible()) { + taskContainer.setInvisible(); + } + return; + } + // Checks if container should be updated before apply new parentInfo. 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; @@ -877,11 +934,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * different process, the server will generate a temporary token that * the organizer can use to reparent the activity through * {@link WindowContainerTransaction} if needed. + * @param candidateAssociatedActToken The token of the candidate associated-activity. + * @param lastOverlayToken The last parent overlay container token. */ @VisibleForTesting @GuardedBy("mLock") void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct, - int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) { + int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken, + @Nullable IBinder candidateAssociatedActToken, @Nullable IBinder lastOverlayToken) { + // Reparent the activity to an overlay container if needed. + final OverlayContainerRestoreParams params = getOverlayContainerRestoreParams( + candidateAssociatedActToken, lastOverlayToken); + if (params != null) { + final Activity associatedActivity = getActivity(candidateAssociatedActToken); + final TaskFragmentContainer targetContainer = createOrUpdateOverlayTaskFragmentIfNeeded( + wct, params.mOptions, params.mIntent, associatedActivity); + if (targetContainer != null) { + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activityToken); + return; + } + } + // If the activity belongs to the current app process, we treat it as a new activity // launch. final Activity activity = getActivity(activityToken); @@ -926,6 +1000,43 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** + * Returns the {@link OverlayContainerRestoreParams} that stored last time the {@code + * associatedActivityToken} associated with and only if data matches the {@code overlayToken}. + * Otherwise, return {@code null}. + */ + @VisibleForTesting + @GuardedBy("mLock") + @Nullable + OverlayContainerRestoreParams getOverlayContainerRestoreParams( + @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) { + if (!Flags.fixPipRestoreToOverlay()) { + return null; + } + + if (associatedActivityToken == null || overlayToken == null) { + return null; + } + + final TaskFragmentContainer.OverlayContainerRestoreParams params = + mOverlayRestoreParams.get(associatedActivityToken); + if (params == null) { + return null; + } + + if (params.mOverlayToken != overlayToken) { + // Not the same overlay container, no need to restore. + return null; + } + + final Activity associatedActivity = getActivity(associatedActivityToken); + if (associatedActivity == null || associatedActivity.isFinishing()) { + return null; + } + + return params; + } + + /** * Called when the {@link WindowContainerTransaction} created with * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side. * @@ -990,6 +1101,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; } @@ -1036,8 +1148,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; } @@ -1208,10 +1319,12 @@ 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)); + final TaskFragmentContainer newContainer = + new TaskFragmentContainer.Builder(this, getTaskId(activity), activity) + .setPendingAppearedActivity(activity).build(); mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity); } } @@ -1378,9 +1491,29 @@ 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); + } + + mOverlayRestoreParams.remove(activity.getActivityToken()); + updateCallbackIfNecessary(); + } + @VisibleForTesting @GuardedBy("mLock") - void onActivityDestroyed(@NonNull Activity activity) { + void onActivityDestroyed(@NonNull WindowContainerTransaction wct, @NonNull Activity activity) { if (!activity.isFinishing()) { // onDestroyed is triggered without finishing. This happens when the activity is // relaunched. In this case, we don't want to cleanup the record. @@ -1390,8 +1523,10 @@ 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); } + + mOverlayRestoreParams.remove(activity.getActivityToken()); // We didn't trigger the callback if there were any pending appeared activities, so check // again after the pending is removed. updateCallbackIfNecessary(); @@ -1471,12 +1606,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; } } @@ -1570,7 +1708,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable Activity launchingActivity) { return createEmptyContainer(wct, intent, taskId, new ActivityStackAttributes.Builder().build(), launchingActivity, - null /* overlayTag */, null /* launchOptions */); + null /* overlayTag */, null /* launchOptions */, + false /* shouldAssociateWithLaunchingActivity */); } /** @@ -1585,7 +1724,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; @@ -1601,17 +1740,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Can't find any activity in the Task that we can use as the owner activity. return null; } - final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */, - intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag, - launchOptions); + final TaskFragmentContainer container = + new TaskFragmentContainer.Builder(this, taskId, activityInTask) + .setPendingAppearedIntent(intent) + .setOverlayTag(overlayTag) + .setLaunchOptions(launchOptions) + .setAssociatedActivity(associateLaunchingActivity ? activityInTask : null) + .build(); 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(), @@ -1672,82 +1814,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; - } - - - // Check appeared activity if there is no such pending appeared activity. - return getContainer(container -> container.hasAppearedActivity(activityToken)); - } - - @GuardedBy("mLock") - TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) { - return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); - } - - @GuardedBy("mLock") - TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, - @NonNull Activity activityInTask, int taskId) { - return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, - activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, - null /* launchOptions */); - } - - @GuardedBy("mLock") - TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, - @NonNull Activity activityInTask, int taskId) { - return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, - null /* launchOptions */); - } - - @GuardedBy("mLock") - TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, - @NonNull Activity activityInTask, int taskId, - @NonNull TaskFragmentContainer pairedPrimaryContainer) { - return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId, pairedPrimaryContainer, null /* tag */, - null /* launchOptions */); - } - - /** - * Creates and registers a new organized container with an optional activity that will be - * re-parented to it in a WCT. - * - * @param pendingAppearedActivity the activity that will be reparented to the TaskFragment. - * @param pendingAppearedIntent the Intent that will be started in the TaskFragment. - * @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 - * 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. - */ - @GuardedBy("mLock") - TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, - @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, - @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, - @Nullable Bundle launchOptions) { - if (activityInTask == null) { - throw new IllegalArgumentException("activityInTask must not be null,"); - } - if (!mTaskContainers.contains(taskId)) { - mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); + for (int i = mTaskContainers.size() - 1; i >= 0; --i) { + final TaskFragmentContainer container = mTaskContainers.valueAt(i) + .getContainerWithActivity(activityToken); + if (container != null) { + return container; + } } - final TaskContainer taskContainer = mTaskContainers.get(taskId); - final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag, - launchOptions); - return container; + return null; } /** @@ -1912,7 +1986,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (shouldContainerBeExpanded(container)) { if (container.getInfo() != null) { - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } // If the info is not available yet the task fragment will be expanded when it's ready return; @@ -1935,7 +2009,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); - if (dismissOverlayContainerIfNeeded(wct, taskContainer)) { + if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) { return; } @@ -1959,22 +2033,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; } /** @@ -2038,19 +2117,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); } /** @@ -2100,6 +2167,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. @@ -2113,6 +2185,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()); @@ -2135,6 +2211,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) { @@ -2208,6 +2289,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); @@ -2405,13 +2489,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; @@ -2438,6 +2519,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mTaskContainers.get(taskId); } + @GuardedBy("mLock") + void addTaskContainer(int taskId, TaskContainer taskContainer) { + mTaskContainers.put(taskId, taskContainer); + mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor)); + } + Handler getHandler() { return mHandler; } @@ -2456,6 +2543,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @VisibleForTesting + @Nullable + ActivityThread.ActivityClientRecord getActivityClientRecord(@NonNull Activity activity) { + return ActivityThread.currentActivityThread() + .getActivityClient(activity.getActivityToken()); + } + + @VisibleForTesting ActivityStartMonitor getActivityStartMonitor() { return mActivityStartMonitor; } @@ -2468,8 +2562,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @VisibleForTesting @Nullable IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) { - final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() - .getActivityClient(activity.getActivityToken()); + final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity); return record != null ? record.mTaskFragmentToken : null; } @@ -2552,25 +2645,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 @@ -2580,9 +2700,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { + if (isActivityFromSplit(launchActivity)) { + // We restrict to launch the overlay from split. Fallback to treat it as normal + // launch. + return null; + } + final List<TaskFragmentContainer> overlayContainers = - getAllOverlayTaskFragmentContainers(); + getAllNonFinishingOverlayContainers(); final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); + final boolean associateLaunchingActivity = options + .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true); // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions // specified by Intent, expand the overlay container to fill the parent task instead. @@ -2603,35 +2731,100 @@ 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); + } + + @GuardedBy("mLock") + private boolean isActivityFromSplit(@NonNull Activity activity) { + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (container == null) { + return false; + } + return getActiveSplitForContainer(container) != null; } private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @@ -2720,6 +2913,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override + public void onActivityPostPaused(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } + synchronized (mLock) { + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_CLOSE); + SplitController.this.onActivityPaused( + transactionRecord.getTransaction(), activity); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } + + @Override public void onActivityPostDestroyed(@NonNull Activity activity) { if (activity.isChild()) { // Skip Activity that is child of another Activity (ActivityGroup) because it's @@ -2727,7 +2937,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } synchronized (mLock) { - SplitController.this.onActivityDestroyed(activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_CLOSE); + SplitController.this.onActivityDestroyed( + transactionRecord.getTransaction(), activity); + transactionRecord.apply(false /* shouldApplyIndependently */); } } } @@ -2876,17 +3091,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 - private static ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { + @Override + public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { + if (!Flags.activityWindowInfoFlag()) { + return null; + } + synchronized (mLock) { + final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); + return activityWindowInfo != null + ? translateActivityWindowInfo(activity, activityWindowInfo) + : null; + } + } + + @VisibleForTesting + void onActivityWindowInfoChanged(@NonNull IBinder activityToken, + @NonNull ActivityWindowInfo activityWindowInfo) { + synchronized (mLock) { + if (mEmbeddedActivityWindowInfoCallback == null) { + return; + } + final Executor executor = mEmbeddedActivityWindowInfoCallback.first; + final Consumer<EmbeddedActivityWindowInfo> callback = + mEmbeddedActivityWindowInfoCallback.second; + + final Activity activity = getActivity(activityToken); + if (activity == null) { + return; + } + final EmbeddedActivityWindowInfo info = translateActivityWindowInfo( + activity, activityWindowInfo); + + executor.execute(() -> callback.accept(info)); + } + } + + @Nullable + private ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { if (activity.isFinishing()) { return null; } - final ActivityThread.ActivityClientRecord record = - ActivityThread.currentActivityThread() - .getActivityClient(activity.getActivityToken()); + final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity); return record != null ? record.getActivityWindowInfo() : null; } + @NonNull + private static EmbeddedActivityWindowInfo translateActivityWindowInfo( + @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) { + final boolean isEmbedded = activityWindowInfo.isEmbedded(); + final Rect 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 @@ -2957,4 +3255,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..d888fa9d6feb 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, @@ -185,8 +186,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Create new empty task fragment final int taskId = primaryContainer.getTaskId(); - final TaskFragmentContainer secondaryContainer = mController.newContainer( - secondaryIntent, primaryActivity, taskId); + final TaskFragmentContainer secondaryContainer = + new TaskFragmentContainer.Builder(mController, taskId, primaryActivity) + .setPendingAppearedIntent(secondaryIntent).build(); final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes); final int windowingMode = mController.getTaskContainer(taskId) @@ -260,7 +262,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { - container = mController.newContainer(activity, taskId); + container = new TaskFragmentContainer.Builder(mController, taskId, activity) + .setPendingAppearedActivity(activity).build(); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForTaskFragment(relBounds); final IBinder reparentActivityToken = activity.getActivityToken(); @@ -303,15 +306,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); if (primaryContainer == null) { - primaryContainer = mController.newContainer(launchingActivity, - launchingActivity.getTaskId()); + primaryContainer = new TaskFragmentContainer.Builder(mController, + launchingActivity.getTaskId(), launchingActivity) + .setPendingAppearedActivity(launchingActivity).build(); } final int taskId = primaryContainer.getTaskId(); - final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, - launchingActivity, taskId, - // Pass in the primary container to make sure it is added right above the primary. - primaryContainer); + final TaskFragmentContainer secondaryContainer = + new TaskFragmentContainer.Builder(mController, taskId, launchingActivity) + .setPendingAppearedIntent(activityIntent) + // Pass in the primary container to make sure it is added right above the + // primary. + .setPairedPrimaryContainer(primaryContainer) + .build(); final TaskContainer taskContainer = mController.getTaskContainer(taskId); final int windowingMode = taskContainer.getWindowingModeForTaskFragment( primaryRelBounds); @@ -367,6 +374,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 +407,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 +436,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 +503,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 +606,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 +622,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,30 +654,32 @@ 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); } /** - * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully - * covered by the task bounds. Otherwise, returns {@code bounds}. + * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not + * fully covered by the task bounds. Otherwise, returns {@code relBounds}. */ @NonNull - static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, - @NonNull Rect taskBounds) { - if (bounds.isEmpty()) { + static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension, + @NonNull TaskFragmentContainer container) { + if (relBounds.isEmpty()) { // Don't need to check if the bounds follows the task bounds. - return bounds; + return relBounds; } - if (boundsSmallerThanMinDimensions(bounds, minDimension)) { + if (boundsSmallerThanMinDimensions(relBounds, minDimension)) { // Expand the bounds if the bounds are smaller than minimum dimensions. return new Rect(); } - if (!taskBounds.contains(bounds)) { + final TaskContainer taskContainer = container.getTaskContainer(); + final Rect relTaskBounds = new Rect(taskContainer.getBounds()); + relTaskBounds.offsetTo(0, 0); + if (!relTaskBounds.contains(relBounds)) { // Expand the bounds if the bounds exceed the task bounds. return new Rect(); } - return bounds; + return relBounds; } @Override @@ -685,8 +739,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 +749,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 +768,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 +809,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 +1010,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 +1029,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 +1040,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 +1051,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 +1177,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 +1185,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary // computation have the same direction, which is from (top, left) to (bottom, right). final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); - switch (layoutDirection) { - case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: - case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: - return splitType; - case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: - case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: - return reversedSplitType; - case LayoutDirection.LOCALE: { - boolean isLtr = taskConfiguration.getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - return isLtr ? splitType : reversedSplitType; - } - } + return isReversedLayout(splitAttributes, taskConfiguration) + ? reversedSplitType + : splitType; } else if (splitType instanceof HingeSplitType) { final HingeSplitType hinge = (HingeSplitType) splitType; @WindowingMode diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 73109e266905..ee00c4cd67eb 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,32 +137,39 @@ class TaskContainer { } int getDisplayId() { - return mDisplayId; + return mInfo.getDisplayId(); } boolean isVisible() { - return mIsVisible; + return mInfo.isVisible(); + } + + 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; } /** @@ -148,21 +178,23 @@ class TaskContainer { boolean shouldUpdateContainer(@NonNull TaskFragmentParentInfo info) { final Configuration configuration = info.getConfiguration(); - return info.isVisible() - // No need to update presentation in PIP until the Task exit PIP. - && !isInPictureInPicture(configuration) - // If the task properties equals regardless of starting position, don't need to - // update the container. - && (mConfiguration.diffPublicOnly(configuration) != 0 - || mDisplayId != info.getDisplayId()); + if (isInPictureInPicture(configuration)) { + // No need to update presentation in PIP until the Task exit PIP. + return false; + } + + // If the task properties equals regardless of starting position, don't + // need to update the container. + 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) { @@ -181,7 +213,7 @@ class TaskContainer { } boolean isInPictureInPicture() { - return isInPictureInPicture(mConfiguration); + return isInPictureInPicture(mInfo.getConfiguration()); } private static boolean isInPictureInPicture(@NonNull Configuration configuration) { @@ -194,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. */ @@ -202,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); } } @@ -228,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)) { @@ -273,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()) { @@ -304,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); @@ -318,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); } @@ -389,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(); + + // If the primary container is fully expanded, we should finish all the associated + // secondary containers. + if (newRatio == RATIO_EXPANDED_PRIMARY) { + for (final SplitContainer splitContainer : mSplitContainers) { + if (primaryContainer == splitContainer.getPrimaryContainer()) { + outContainersToFinish.add(splitContainer.getSecondaryContainer()); + } + } + + // Temporarily suppress the placeholder rule in the TaskContainer. This will be restored + // if a new split is added into the TaskContainer. + mPlaceholderRuleSuppressed = true; + + mOverrideSplitType = null; + return; + } + + final SplitType newSplitType; + if (newRatio == RATIO_EXPANDED_SECONDARY) { + newSplitType = new ExpandContainersSplitType(); + // We do not want to apply ExpandContainersSplitType to new split containers. + mOverrideSplitType = null; + } else { + // We save the override RatioSplitType and apply to new split containers. + newSplitType = mOverrideSplitType = new RatioSplitType(newRatio); + } + for (final SplitContainer splitContainer : mSplitContainers) { + if (primaryContainer == splitContainer.getPrimaryContainer()) { + updateDefaultSplitAttributes(splitContainer, newSplitType); + } + } + } + + @Nullable + SplitContainer getTopNonFinishingSplitContainer() { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = mSplitContainers.get(i); + if (!splitContainer.getPrimaryContainer().isFinished() + && !splitContainer.getSecondaryContainer().isFinished()) { + return splitContainer; + } + } + return null; + } + private void onTaskFragmentContainerUpdated() { // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce // another special container that should also be on top in the future. updateSplitPinContainerIfNecessary(); // Update overlay container after split pin container since the overlay should be on top of // pin container. - updateOverlayContainerIfNecessary(); + updateAlwaysOnTopOverlayIfNecessary(); + } + + private void updateAlwaysOnTopOverlayIfNecessary() { + final List<TaskFragmentContainer> alwaysOnTopOverlays = mContainers + .stream().filter(TaskFragmentContainer::isAlwaysOnTopOverlay).toList(); + if (alwaysOnTopOverlays.size() > 1) { + throw new IllegalStateException("There must be at most one always-on-top overlay " + + "container per Task"); + } + mAlwaysOnTopOverlayContainer = alwaysOnTopOverlays.isEmpty() + ? null : alwaysOnTopOverlays.getFirst(); + if (mAlwaysOnTopOverlayContainer != null) { + moveContainerToLastIfNecessary(mAlwaysOnTopOverlayContainer); + } } private void updateSplitPinContainerIfNecessary() { @@ -423,18 +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..7173b0c95230 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -36,6 +36,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; @@ -113,6 +114,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,23 +185,14 @@ class TaskFragmentContainer { private boolean mIsIsolatedNavigationEnabled; /** - * Whether to apply dimming on the parent Task that was requested last. + * Whether this TaskFragment is pinned. */ - private boolean mLastDimOnTask; + private boolean mIsPinned; /** - * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController, - * TaskFragmentContainer, String, Bundle) + * Whether to apply dimming on the parent Task that was requested last. */ - TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, - @Nullable Intent pendingAppearedIntent, - @NonNull TaskContainer taskContainer, - @NonNull SplitController controller, - @Nullable TaskFragmentContainer pairedPrimaryContainer) { - this(pendingAppearedActivity, pendingAppearedIntent, taskContainer, - controller, pairedPrimaryContainer, null /* overlayTag */, - null /* launchOptions */); - } + private boolean mLastDimOnTask; /** * Creates a container with an existing activity that will be re-parented to it in a window @@ -197,12 +201,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, + private 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( @@ -212,9 +218,9 @@ class TaskFragmentContainer { mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; mOverlayTag = overlayTag; - if (overlayTag != null) { - Objects.requireNonNull(launchOptions); - } + mAssociatedActivityToken = associatedActivity != null + ? associatedActivity.getActivityToken() : null; + if (launchOptions != null) { mLaunchOptions.putAll(launchOptions); } @@ -249,6 +255,15 @@ class TaskFragmentContainer { addPendingAppearedActivity(pendingAppearedActivity); } mPendingAppearedIntent = pendingAppearedIntent; + + // Save the information necessary for restoring the overlay when needed. + if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null + && associatedActivity != null && !associatedActivity.isFinishing()) { + final IBinder associatedActivityToken = associatedActivity.getActivityToken(); + final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, + launchOptions, pendingAppearedIntent); + mController.mOverlayRestoreParams.put(associatedActivityToken, params); + } } /** @@ -420,14 +435,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 +495,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 +787,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 +888,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 +1028,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 +1073,7 @@ class TaskFragmentContainer { + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished + " overlayTag=" + mOverlayTag + + " associatedActivityToken=" + mAssociatedActivityToken + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" @@ -997,4 +1095,136 @@ class TaskFragmentContainer { } return sb.append("]").toString(); } + + static final class Builder { + @NonNull + private final SplitController mSplitController; + + // The parent Task id of the new TaskFragment. + private final int mTaskId; + + // The activity in the same Task so that we can get the Task bounds if needed. + @NonNull + private final Activity mActivityInTask; + + // The activity that will be reparented to the TaskFragment. + @Nullable + private Activity mPendingAppearedActivity; + + // The Intent that will be started in the TaskFragment. + @Nullable + private Intent mPendingAppearedIntent; + + // The paired primary {@link TaskFragmentContainer}. When it is set, the new container + // will be added right above it. + @Nullable + private TaskFragmentContainer mPairedPrimaryContainer; + + // The launch options bundle to create a container. Must be specified for overlay container. + @Nullable + private Bundle mLaunchOptions; + + // The tag for the new created overlay container. This is required when creating an + // overlay container. + @Nullable + private String mOverlayTag; + + // The associated activity of the overlay container. Must be {@code null} for a + // non-overlay container. + @Nullable + private Activity mAssociatedActivity; + + Builder(@NonNull SplitController splitController, int taskId, + @Nullable Activity activityInTask) { + if (taskId <= 0) { + throw new IllegalArgumentException("taskId is invalid, " + taskId); + } + + mSplitController = splitController; + mTaskId = taskId; + mActivityInTask = activityInTask; + } + + @NonNull + Builder setPendingAppearedActivity(@Nullable Activity pendingAppearedActivity) { + mPendingAppearedActivity = pendingAppearedActivity; + return this; + } + + @NonNull + Builder setPendingAppearedIntent(@Nullable Intent pendingAppearedIntent) { + mPendingAppearedIntent = pendingAppearedIntent; + return this; + } + + @NonNull + Builder setPairedPrimaryContainer(@Nullable TaskFragmentContainer pairedPrimaryContainer) { + mPairedPrimaryContainer = pairedPrimaryContainer; + return this; + } + + @NonNull + Builder setLaunchOptions(@Nullable Bundle launchOptions) { + mLaunchOptions = launchOptions; + return this; + } + + @NonNull + Builder setOverlayTag(@Nullable String overlayTag) { + mOverlayTag = overlayTag; + return this; + } + + @NonNull + Builder setAssociatedActivity(@Nullable Activity associatedActivity) { + mAssociatedActivity = associatedActivity; + return this; + } + + @NonNull + TaskFragmentContainer build() { + if (mOverlayTag != null) { + Objects.requireNonNull(mLaunchOptions); + } else if (mAssociatedActivity != null) { + throw new IllegalArgumentException("Associated activity must be null for " + + "non-overlay activity."); + } + + TaskContainer taskContainer = mSplitController.getTaskContainer(mTaskId); + if (taskContainer == null && mActivityInTask == null) { + throw new IllegalArgumentException("mActivityInTask must be set."); + } + + if (taskContainer == null) { + // Adding a TaskContainer if no existed one. + taskContainer = new TaskContainer(mTaskId, mActivityInTask); + mSplitController.addTaskContainer(mTaskId, taskContainer); + } + + return new TaskFragmentContainer(mPendingAppearedActivity, mPendingAppearedIntent, + taskContainer, mSplitController, mPairedPrimaryContainer, mOverlayTag, + mLaunchOptions, mAssociatedActivity); + } + } + + static class OverlayContainerRestoreParams { + /** The token of the overlay container */ + @NonNull + final IBinder mOverlayToken; + + /** The launch options to create this container. */ + @NonNull + final Bundle mOptions; + + /** The Intent that used to be started in the overlay container. */ + @NonNull + final Intent mIntent; + + OverlayContainerRestoreParams(@NonNull IBinder overlayToken, @NonNull Bundle options, + @NonNull Intent intent) { + mOverlayToken = overlayToken; + mOptions = options; + mIntent = intent; + } + } } 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..4f51815ed05d --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -0,0 +1,941 @@ +/* + * 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 Rect mockTaskBounds = new Rect(0, 0, 2000, 1000); + 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( + mockPrimaryContainer, mockSecondaryContainer, mockTaskBounds, + 50 /* divideWidthPx */, false /* isDraggableExpandType */, + true /* isVerticalSplit */, false /* isReversedLayout */), + true /* isVerticalSplit */, + false /* isReversedLayout */, + Display.DEFAULT_DISPLAY, + false /* isDraggableExpandType */, + mockPrimaryContainer, + mockSecondaryContainer + ); + + 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( + 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( + 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( + 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( + 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( + 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( + 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.dividerPositionWithPositionOptions( + 10 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + -dismissVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is greater than maxPosition and the velocity is enough to be dismissed + assertEquals( + 1200, // Fully expanded position + DividerPresenter.dividerPositionWithPositionOptions( + 1000 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + dismissVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // 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.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and larger + // than maxPosition + assertEquals( + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 950 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and smaller + // than minPosition + assertEquals( + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 20 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to maxPosition bounds and the velocity is enough for + // backward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 2200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to maxPosition bounds and the velocity is enough + // for backward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to minPosition bounds and the velocity is enough for + // forward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to minPosition bounds and the velocity is enough + // for forward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + } + + @Test + public void testDividerPositionWithDraggingToFullscreenNotAllowed() { + final float displayDensity = 600F; + 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 returned when the velocity is not fast enough for fling and is in + // between minPosition and maxPosition + assertEquals( + 500, // dividerPosition is not snapped + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and larger + // than maxPosition + assertEquals( + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 950 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and smaller + // than minPosition + assertEquals( + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 20 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and at the + // closed position + assertEquals( + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 0 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and at the + // fully expanded position + assertEquals( + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to maxPosition bounds and the velocity is enough for + // backward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 2200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to maxPosition bounds and the velocity is enough + // for backward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to minPosition bounds and the velocity is enough for + // forward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to minPosition bounds and the velocity is enough + // for forward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + } + + 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/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index a069ac7256d6..d649c6d57137 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -248,4 +248,17 @@ public class EmbeddingTestUtils { return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate, intentPredicate, windowMetricsPredicate); } + + @NonNull + static TaskFragmentContainer createTfContainer( + @NonNull SplitController splitController, @NonNull Activity activity) { + return createTfContainer(splitController, TASK_ID, activity); + } + + @NonNull + static TaskFragmentContainer createTfContainer( + @NonNull SplitController splitController, int taskId, @NonNull Activity activity) { + return new TaskFragmentContainer.Builder(splitController, taskId, activity) + .setPendingAppearedActivity(activity).build(); + } } 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..7b473b04548c 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 @@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -42,10 +43,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 +64,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 +79,6 @@ public class JetpackTaskFragmentOrganizerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); spyOn(mOrganizer); @@ -101,13 +106,16 @@ public class JetpackTaskFragmentOrganizerTest { @Test public void testExpandTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); + doReturn(taskContainer).when(mSplitController).getTaskContainer(anyInt()); + final TaskFragmentContainer container = new TaskFragmentContainer.Builder(mSplitController, + taskContainer.getTaskId(), null /* activityInTask */) + .setPendingAppearedIntent(new Intent()) + .build(); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); - mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + mOrganizer.expandTaskFragment(mTransaction, container); verify(mTransaction).setWindowingMode(container.getInfo().getToken(), WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 28fbadbebe7f..7a0b9a0ece6b 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,16 @@ 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.EmbeddingTestUtils.createTfContainer; +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 +44,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 +86,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 +105,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 +138,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 +189,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 +282,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 +307,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 +349,39 @@ 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); } + @Test + public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + final TaskFragmentContainer primaryContainer = + createMockTaskFragmentContainer(primaryActivity); + final TaskFragmentContainer secondaryContainer = + createMockTaskFragmentContainer(secondaryActivity); + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( + activityActivityPair -> true /* activityPairPredicate */, + activityIntentPair -> true /* activityIntentPairPredicate */, + parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build(); + mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity, + secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes()); + + assertThat(createOrUpdateOverlayTaskFragmentIfNeeded("test", primaryActivity)).isNull(); + assertThat(createOrUpdateOverlayTaskFragmentIfNeeded("test", secondaryActivity)).isNull(); + } + 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 +390,38 @@ 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"); - SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS); + assertThat(sanitizeBounds(bounds, null, overlayContainer) + .isEmpty()).isTrue(); + } + + @Test + public void testSanitizeBounds_taskInSplitScreen() { + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1"); + TaskContainer taskContainer = overlayContainer.getTaskContainer(); + spyOn(taskContainer); + doReturn(new Rect(TASK_BOUNDS.left + TASK_BOUNDS.width() / 2, TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom)).when(taskContainer).getBounds(); + final Rect taskBounds = taskContainer.getBounds(); + final Rect bounds = new Rect(taskBounds.width() / 2, 0, taskBounds.width(), + taskBounds.height()); + + assertThat(sanitizeBounds(bounds, null, overlayContainer) + .isEmpty()).isFalse(); } @Test @@ -294,9 +431,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 +441,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 +492,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 +512,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); @@ -407,8 +547,8 @@ public class OverlayPresentationTest { @Test public void testUpdateActivityStackAttributes_nullContainer_earlyReturn() { - final TaskFragmentContainer container = mSplitController.newContainer(mActivity, - mActivity.getTaskId()); + final TaskFragmentContainer container = createTfContainer(mSplitController, + mActivity.getTaskId(), mActivity); mSplitController.updateActivityStackAttributes( ActivityStack.Token.createFromBinder(container.getTaskFragmentToken()), new ActivityStackAttributes.Builder().build()); @@ -443,11 +583,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,21 +602,20 @@ 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( new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(), - false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo); @@ -487,7 +625,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 +643,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 +669,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 +715,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 +733,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 +742,215 @@ 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()); + } + + @Test + public void testOnActivityReparentedToTask_overlayRestoration() { + mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY); + + // Prepares and mock the data necessary for the test. + final IBinder activityToken = mActivity.getActivityToken(); + final Intent intent = new Intent(); + final IBinder fillTaskActivityToken = new Binder(); + final IBinder lastOverlayToken = new Binder(); + final TaskFragmentContainer overlayContainer = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(intent).build(); + final TaskFragmentContainer.OverlayContainerRestoreParams params = mock( + TaskFragmentContainer.OverlayContainerRestoreParams.class); + doReturn(params).when(mSplitController).getOverlayContainerRestoreParams(any(), any()); + doReturn(overlayContainer).when(mSplitController).createOrUpdateOverlayTaskFragmentIfNeeded( + any(), any(), any(), any()); + + // Verify the activity should be reparented to the overlay container. + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken, + fillTaskActivityToken, lastOverlayToken); + verify(mTransaction).reparentActivityToTaskFragment( + eq(overlayContainer.getTaskFragmentToken()), eq(activityToken)); + } + /** - * 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) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, - activity.getTaskId()); - setupTaskFragmentInfo(container, 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 = createTfContainer(mSplitController, + activity.getTaskId(), activity); + setupTaskFragmentInfo(container, activity, isVisible); return container; } @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) { - Activity activity = createMockActivity(); - TaskFragmentContainer overlayContainer = mSplitController.newContainer( - null /* pendingAppearedActivity */, mIntent, activity, taskId, - null /* pairedPrimaryContainer */, tag, Bundle.EMPTY); - setupTaskFragmentInfo(overlayContainer, activity); + 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 = + new TaskFragmentContainer.Builder(mSplitController, taskId, activity) + .setPendingAppearedIntent(mIntent) + .setOverlayTag(tag) + .setLaunchOptions(Bundle.EMPTY) + .setAssociatedActivity(associateLaunchingActivity ? activity : null) + .build(); + 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..640b1fced455 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 @@ -41,6 +41,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSpli import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; @@ -59,7 +60,6 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; @@ -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 = @@ -176,7 +198,7 @@ public class SplitControllerTest { @Test public void testOnTaskFragmentVanished() { - final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); // The TaskFragment has been removed in the server, we only need to cleanup the reference. @@ -191,7 +213,7 @@ public class SplitControllerTest { public void testOnTaskFragmentAppearEmptyTimeout() { // Setup to make sure a transaction record is started. mTransactionManager.startNewTransaction(); - final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); @@ -202,19 +224,19 @@ public class SplitControllerTest { @Test public void testOnActivityDestroyed() { doReturn(new Binder()).when(mActivity).getActivityToken(); - final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); assertTrue(tf.hasActivity(mActivity.getActivityToken())); // 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())); } @@ -223,12 +245,9 @@ public class SplitControllerTest { public void testNewContainer() { // Must pass in a valid activity. assertThrows(IllegalArgumentException.class, () -> - mSplitController.newContainer(null /* activity */, TASK_ID)); - assertThrows(IllegalArgumentException.class, () -> - mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID)); + createTfContainer(mSplitController, null /* activity */)); - final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, mActivity, - TASK_ID); + final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); assertNotNull(tf); @@ -241,7 +260,7 @@ public class SplitControllerTest { public void testUpdateContainer() { // Make SplitController#launchPlaceholderIfNecessary(TaskFragmentContainer) return true // and verify if shouldContainerBeExpanded() not called. - final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); spyOn(tf); doReturn(mActivity).when(tf).getTopNonFinishingActivity(); doReturn(true).when(tf).isEmpty(); @@ -275,7 +294,10 @@ public class SplitControllerTest { doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); - doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); + final SplitRule splitRule = createSplitRule(mActivity, mActivity); + doReturn(splitRule).when(splitContainer).getSplitRule(); + doReturn(splitRule.getDefaultSplitAttributes()) + .when(splitContainer).getDefaultSplitAttributes(); taskContainer = mSplitController.getTaskContainer(TASK_ID); taskContainer.addSplitContainer(splitContainer); // Add a mock SplitContainer on top of splitContainer @@ -344,8 +366,12 @@ public class SplitControllerTest { public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - intent, taskContainer, mSplitController, null /* pairedPrimaryContainer */); + final int taskId = taskContainer.getTaskId(); + mSplitController.addTaskContainer(taskId, taskContainer); + final TaskFragmentContainer container = new TaskFragmentContainer.Builder(mSplitController, + taskId, null /* activityInTask */) + .setPendingAppearedIntent(intent) + .build(); final SplitController.ActivityStartMonitor monitor = mSplitController.getActivityStartMonitor(); @@ -372,7 +398,8 @@ public class SplitControllerTest { @Test public void testOnActivityReparentedToTask_sameProcess() { mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(), - mActivity.getActivityToken()); + mActivity.getActivityToken(), null /* fillTaskActivityToken */, + null /* lastOverlayToken */); // Treated as on activity created, but allow to split as primary. verify(mSplitController).resolveActivityToContainer(mTransaction, @@ -384,11 +411,13 @@ public class SplitControllerTest { @Test public void testOnActivityReparentedToTask_diffProcess() { // Create an empty TaskFragment to initialize for the Task. - mSplitController.newContainer(new Intent(), mActivity, TASK_ID); + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(new Intent()).build(); final IBinder activityToken = new Binder(); final Intent intent = new Intent(); - mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken); + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken, + null /* fillTaskActivityToken */, null /* lastOverlayToken */); // Treated as starting new intent verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean()); @@ -568,8 +597,9 @@ public class SplitControllerTest { verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any()); // Place in the top container if there is no other rule matched. - final TaskFragmentContainer topContainer = mSplitController - .newContainer(new Intent(), mActivity, TASK_ID); + final TaskFragmentContainer topContainer = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(new Intent()).build(); mSplitController.placeActivityInTopContainer(mTransaction, mActivity); verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(), @@ -577,7 +607,7 @@ public class SplitControllerTest { // Not reparent if activity is in a TaskFragment. clearInvocations(mTransaction); - mSplitController.newContainer(mActivity, TASK_ID); + createTfContainer(mSplitController, mActivity); mSplitController.placeActivityInTopContainer(mTransaction, mActivity); verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any()); @@ -589,8 +619,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertFalse(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); + verify(mSplitController, never()).addTaskContainer(anyInt(), any()); } @Test @@ -605,7 +634,6 @@ public class SplitControllerTest { assertTrue(result); assertNotNull(container); - verify(mSplitController).newContainer(mActivity, TASK_ID); verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(), mActivity); } @@ -615,12 +643,12 @@ public class SplitControllerTest { setupExpandRule(mActivity); // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. - final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, mActivity); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container); } @Test @@ -665,8 +693,8 @@ public class SplitControllerTest { // Don't launch placeholder if the activity is not in the topmost active TaskFragment. final Activity activity = createMockActivity(); - mSplitController.newContainer(mActivity, TASK_ID); - mSplitController.newContainer(activity, TASK_ID); + createTfContainer(mSplitController, mActivity); + createTfContainer(mSplitController, activity); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -684,7 +712,7 @@ public class SplitControllerTest { (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); // Launch placeholder if the activity is in the topmost expanded TaskFragment. - mSplitController.newContainer(mActivity, TASK_ID); + createTfContainer(mSplitController, mActivity); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -736,10 +764,11 @@ public class SplitControllerTest { final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); // Activity is already in primary split, no need to create new split. - final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, - TASK_ID); - final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( - secondaryIntent, mActivity, TASK_ID); + final TaskFragmentContainer primaryContainer = + createTfContainer(mSplitController, mActivity); + final TaskFragmentContainer secondaryContainer = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(secondaryIntent).build(); mSplitController.registerSplit( mTransaction, primaryContainer, @@ -752,8 +781,6 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -765,10 +792,11 @@ public class SplitControllerTest { // The new launched activity is in primary split, but there is no rule for it to split with // the secondary, so return false. - final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, - TASK_ID); - final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( - secondaryIntent, mActivity, TASK_ID); + final TaskFragmentContainer primaryContainer = + createTfContainer(mSplitController, mActivity); + final TaskFragmentContainer secondaryContainer = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(secondaryIntent).build(); mSplitController.registerSplit( mTransaction, primaryContainer, @@ -795,8 +823,6 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -825,10 +851,10 @@ public class SplitControllerTest { doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent(); // Activity is a placeholder. - final TaskFragmentContainer primaryContainer = mSplitController.newContainer( - primaryActivity, TASK_ID); - final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity, - TASK_ID); + final TaskFragmentContainer primaryContainer = + createTfContainer(mSplitController, primaryActivity); + final TaskFragmentContainer secondaryContainer = + createTfContainer(mSplitController, mActivity); mSplitController.registerSplit( mTransaction, primaryContainer, @@ -847,8 +873,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); - final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, - TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow); container.addPendingAppearedActivity(mActivity); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -863,8 +888,7 @@ public class SplitControllerTest { setupSplitRule(mActivity, activityBelow); // Disallow to split as primary. - final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, - TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow); container.addPendingAppearedActivity(mActivity); boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -934,8 +958,7 @@ public class SplitControllerTest { doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); - final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, - TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow); container.addPendingAppearedActivity(mActivity); // Allow to split as primary. @@ -953,8 +976,7 @@ public class SplitControllerTest { doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); - final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, - TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow); container.addPendingAppearedActivity(mActivity); boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, @@ -1017,8 +1039,8 @@ public class SplitControllerTest { public void testResolveActivityToContainer_skipIfNonTopOrPinned() { final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); final Activity pinnedActivity = createMockActivity(); - final TaskFragmentContainer topContainer = mSplitController.newContainer(pinnedActivity, - TASK_ID); + final TaskFragmentContainer topContainer = + createTfContainer(mSplitController, pinnedActivity); final TaskContainer taskContainer = container.getTaskContainer(); spyOn(taskContainer); doReturn(container).when(taskContainer).getTopNonFinishingTaskFragmentContainer(false); @@ -1185,7 +1207,7 @@ public class SplitControllerTest { mSplitController.onTransactionReady(transaction); verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent), - eq(activityToken)); + eq(activityToken), any(), any()); verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), anyInt(), anyBoolean()); } @@ -1324,7 +1346,7 @@ public class SplitControllerTest { // Launch placeholder for activity in top TaskFragment. setupPlaceholderRule(mActivity); mTransactionManager.startNewTransaction(); - final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mSplitController, mActivity); mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, true /* isOnCreated */); @@ -1338,9 +1360,10 @@ public class SplitControllerTest { // Do not launch placeholder for invisible activity below the top TaskFragment. setupPlaceholderRule(mActivity); mTransactionManager.startNewTransaction(); - final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); - final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity, - TASK_ID); + final TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity); + final TaskFragmentContainer topTf = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(new Intent()).build(); bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity, false /* isVisible */)); topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); @@ -1356,9 +1379,10 @@ public class SplitControllerTest { // Launch placeholder for visible activity below the top TaskFragment. setupPlaceholderRule(mActivity); mTransactionManager.startNewTransaction(); - final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); - final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity, - TASK_ID); + final TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity); + final TaskFragmentContainer topTf = + new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity) + .setPendingAppearedIntent(new Intent()).build(); bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity, true /* isVisible */)); topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); @@ -1385,7 +1409,7 @@ public class SplitControllerTest { @Test public void testFinishActivityStacks_finishSingleActivityStack() { - TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity)); final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID); @@ -1399,8 +1423,8 @@ public class SplitControllerTest { @Test public void testFinishActivityStacks_finishActivityStacksInOrder() { - TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); - TaskFragmentContainer topTf = mSplitController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity); + TaskFragmentContainer topTf = createTfContainer(mSplitController, mActivity); bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity)); topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); @@ -1529,6 +1553,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,20 +1668,24 @@ 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; } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, - activity.getTaskId()); + final TaskFragmentContainer container = createTfContainer(mSplitController, + activity.getTaskId(), activity); setupTaskFragmentInfo(container, activity); return container; } 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..816e2dae1e5b 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; @@ -30,6 +31,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActi import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES; @@ -85,10 +87,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 +110,10 @@ import java.util.ArrayList; public class SplitPresenterTest { private Activity mActivity; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private Resources mActivityResources; @Mock @@ -119,7 +127,6 @@ public class SplitPresenterTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = @@ -133,7 +140,7 @@ public class SplitPresenterTest { @Test public void testCreateTaskFragment() { - final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mController, mActivity); mPresenter.createTaskFragment(mTransaction, container.getTaskFragmentToken(), mActivity.getActivityToken(), TASK_BOUNDS, WINDOWING_MODE_MULTI_WINDOW); @@ -144,7 +151,7 @@ public class SplitPresenterTest { @Test public void testResizeTaskFragment() { - final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mController, mActivity); mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo); mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS); @@ -160,7 +167,7 @@ public class SplitPresenterTest { @Test public void testUpdateWindowingMode() { - final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mController, mActivity); mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo); mPresenter.updateWindowingMode(mTransaction, container.getTaskFragmentToken(), WINDOWING_MODE_MULTI_WINDOW); @@ -178,8 +185,8 @@ public class SplitPresenterTest { @Test public void testSetAdjacentTaskFragments() { - final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); - final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container0 = createTfContainer(mController, mActivity); + final TaskFragmentContainer container1 = createTfContainer(mController, mActivity); mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(), container1.getTaskFragmentToken(), null /* adjacentParams */); @@ -196,8 +203,8 @@ public class SplitPresenterTest { @Test public void testClearAdjacentTaskFragments() { - final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); - final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container0 = createTfContainer(mController, mActivity); + final TaskFragmentContainer container1 = createTfContainer(mController, mActivity); // No request to clear as it is not set by default. mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken()); @@ -218,8 +225,8 @@ public class SplitPresenterTest { @Test public void testSetCompanionTaskFragment() { - final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); - final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container0 = createTfContainer(mController, mActivity); + final TaskFragmentContainer container1 = createTfContainer(mController, mActivity); mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(), container1.getTaskFragmentToken()); @@ -236,7 +243,7 @@ public class SplitPresenterTest { @Test public void testSetTaskFragmentDimOnTask() { - final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mController, mActivity); mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true); verify(mTransaction).addTaskFragmentOperation(eq(container.getTaskFragmentToken()), any()); @@ -249,7 +256,7 @@ public class SplitPresenterTest { @Test public void testUpdateAnimationParams() { - final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container = createTfContainer(mController, mActivity); // Verify the default. assertTrue(container.areLastRequestedAnimationParamsEqual( @@ -280,6 +287,28 @@ public class SplitPresenterTest { } @Test + public void testSetTaskFragmentPinned() { + final TaskFragmentContainer container = createTfContainer(mController, mActivity); + + // 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); @@ -639,8 +668,8 @@ public class SplitPresenterTest { public void testExpandSplitContainerIfNeeded() { Activity secondaryActivity = createMockActivity(); SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); - TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); - TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); + TaskFragmentContainer primaryTf = createTfContainer(mController, mActivity); + TaskFragmentContainer secondaryTf = createTfContainer(mController, secondaryActivity); SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity, secondaryTf, splitRule, SPLIT_ATTRIBUTES); @@ -665,8 +694,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,15 +704,15 @@ 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 public void testCreateNewSplitContainer_secondaryAbovePrimary() { final Activity secondaryActivity = createMockActivity(); - final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID); - final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer bottomTf = createTfContainer(mController, secondaryActivity); + final TaskFragmentContainer primaryTf = createTfContainer(mController, mActivity); final SplitPairRule rule = createSplitPairRuleBuilder(pair -> pair.first == mActivity && pair.second == secondaryActivity, pair -> false, metrics -> true) 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..284723279b80 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 @@ -29,6 +29,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -42,11 +43,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; @@ -56,18 +58,19 @@ import java.util.List; * Build/Install/Run: * atest WMJetpackUnitTests:TaskContainerTest */ + +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @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(); @@ -127,8 +130,11 @@ public class TaskContainerTest { assertTrue(taskContainer.isEmpty()); - final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); + doReturn(taskContainer).when(mController).getTaskContainer(anyInt()); + final TaskFragmentContainer tf = new TaskFragmentContainer.Builder(mController, + taskContainer.getTaskId(), null /* activityInTask */) + .setPendingAppearedIntent(new Intent()) + .build(); assertFalse(taskContainer.isEmpty()); @@ -143,12 +149,17 @@ public class TaskContainerTest { final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopNonFinishingTaskFragmentContainer()); - final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); + doReturn(taskContainer).when(mController).getTaskContainer(anyInt()); + final TaskFragmentContainer tf0 = new TaskFragmentContainer.Builder(mController, + taskContainer.getTaskId(), null /* activityInTask */) + .setPendingAppearedIntent(new Intent()) + .build(); assertEquals(tf0, taskContainer.getTopNonFinishingTaskFragmentContainer()); - final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer.Builder(mController, + taskContainer.getTaskId(), null /* activityInTask */) + .setPendingAppearedIntent(new Intent()) + .build(); assertEquals(tf1, taskContainer.getTopNonFinishingTaskFragmentContainer()); } 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..7fab371cb790 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); @@ -96,24 +100,27 @@ public class TaskFragmentContainerTest { @Test public void testNewContainer() { final TaskContainer taskContainer = createTestTaskContainer(); + mController.addTaskContainer(taskContainer.getTaskId(), taskContainer); // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(null, null, taskContainer, mController, - null /* pairedPrimaryContainer */)); + () -> new TaskFragmentContainer.Builder(mController, taskContainer.getTaskId(), + null /* activityInTask */).build()); // One of the activity and the intent must be null. assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController, - null /* pairedPrimaryContainer */)); + () -> new TaskFragmentContainer.Builder(mController, taskContainer.getTaskId(), + null /* activityInTask */) + .setPendingAppearedActivity(createMockActivity()) + .setPendingAppearedIntent(mIntent) + .build()); } @Test public void testFinish() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer(taskContainer, + mActivity, null /* pendingAppearedIntent */); doReturn(container).when(mController).getContainerWithActivity(mActivity); // Only remove the activity, but not clear the reference until appeared. @@ -144,15 +151,13 @@ public class TaskFragmentContainerTest { @Test public void testFinish_notFinishActivityThatIsReparenting() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer container0 = createTaskFragmentContainer(taskContainer, + mActivity, null /* pendingAppearedIntent */); final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); container0.setInfo(mTransaction, info); // Request to reparent the activity to a new TaskFragment. - final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer container1 = createTaskFragmentContainer(taskContainer, + mActivity, null /* pendingAppearedIntent */); doReturn(container1).when(mController).getContainerWithActivity(mActivity); // The activity is requested to be reparented, so don't finish it. @@ -167,15 +172,13 @@ public class TaskFragmentContainerTest { public void testFinish_alwaysFinishPlaceholder() { // Register container1 as a placeholder final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer container0 = createTaskFragmentContainer(taskContainer, + mActivity, null /* pendingAppearedIntent */); final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity); container0.setInfo(mTransaction, info0); final Activity placeholderActivity = createMockActivity(); - final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer container1 = createTaskFragmentContainer(taskContainer, + placeholderActivity, null /* pendingAppearedIntent */); final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity); container1.setInfo(mTransaction, info1); final SplitAttributes splitAttributes = new SplitAttributes.Builder().build(); @@ -203,9 +206,8 @@ public class TaskFragmentContainerTest { public void testSetInfo() { final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. - final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer pendingActivityContainer = createTaskFragmentContainer( + taskContainer, mActivity, null /* pendingAppearedIntent */); assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains( mActivity.getActivityToken())); @@ -217,9 +219,8 @@ public class TaskFragmentContainerTest { assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); // Pending intent should be cleared when the container becomes non-empty. - final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( - null /* pendingAppearedActivity */, mIntent, taskContainer, mController, - null /* pairedPrimaryContainer */); + final TaskFragmentContainer pendingIntentContainer = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); @@ -233,8 +234,8 @@ public class TaskFragmentContainerTest { @Test public void testIsWaitingActivityAppear() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); assertTrue(container.isWaitingActivityAppear()); @@ -255,8 +256,8 @@ public class TaskFragmentContainerTest { public void testAppearEmptyTimeout() { doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); assertNull(container.mAppearEmptyTimeout); @@ -295,8 +296,8 @@ public class TaskFragmentContainerTest { @Test public void testCollectNonFinishingActivities() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); List<Activity> activities = container.collectNonFinishingActivities(); assertTrue(activities.isEmpty()); @@ -323,8 +324,8 @@ public class TaskFragmentContainerTest { @Test public void testCollectNonFinishingActivities_checkIfStable() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); // In case mInfo is null, collectNonFinishingActivities(true) should return null. List<Activity> activities = @@ -349,8 +350,8 @@ public class TaskFragmentContainerTest { @Test public void testAddPendingActivity() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); container.addPendingAppearedActivity(mActivity); assertEquals(1, container.collectNonFinishingActivities().size()); @@ -363,10 +364,10 @@ public class TaskFragmentContainerTest { @Test public void testIsAbove() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); - final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container0 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); + final TaskFragmentContainer container1 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); assertTrue(container1.isAbove(container0)); assertFalse(container0.isAbove(container1)); @@ -375,8 +376,8 @@ public class TaskFragmentContainerTest { @Test public void testGetBottomMostActivity() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); container.addPendingAppearedActivity(mActivity); assertEquals(mActivity, container.getBottomMostActivity()); @@ -392,8 +393,8 @@ public class TaskFragmentContainerTest { @Test public void testOnActivityDestroyed() { final TaskContainer taskContainer = createTestTaskContainer(mController); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); container.addPendingAppearedActivity(mActivity); final List<IBinder> activities = new ArrayList<>(); activities.add(mActivity.getActivityToken()); @@ -402,7 +403,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())); @@ -412,8 +413,8 @@ public class TaskFragmentContainerTest { public void testIsInIntermediateState() { // True if no info set. final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); spyOn(taskContainer); doReturn(true).when(taskContainer).isVisible(); @@ -475,8 +476,8 @@ public class TaskFragmentContainerTest { @Test public void testHasAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); container.addPendingAppearedActivity(mActivity); assertFalse(container.hasAppearedActivity(mActivity.getActivityToken())); @@ -492,8 +493,8 @@ public class TaskFragmentContainerTest { @Test public void testHasPendingAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); container.addPendingAppearedActivity(mActivity); assertTrue(container.hasPendingAppearedActivity(mActivity.getActivityToken())); @@ -509,10 +510,10 @@ public class TaskFragmentContainerTest { @Test public void testHasActivity() { final TaskContainer taskContainer = createTestTaskContainer(mController); - final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); - final TaskFragmentContainer container2 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container1 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); + final TaskFragmentContainer container2 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, mIntent); // Activity is pending appeared on container2. container2.addPendingAppearedActivity(mActivity); @@ -534,9 +535,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()); @@ -548,17 +547,19 @@ public class TaskFragmentContainerTest { @Test public void testNewContainerWithPairedPrimaryContainer() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer tf0 = new TaskFragmentContainer( - null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, - null /* pairedPrimaryTaskFragment */); - final TaskFragmentContainer tf1 = new TaskFragmentContainer( - null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, - null /* pairedPrimaryTaskFragment */); + mController.addTaskContainer(taskContainer.getTaskId(), taskContainer); + final TaskFragmentContainer tf0 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, new Intent()); + final TaskFragmentContainer tf1 = createTaskFragmentContainer( + taskContainer, null /* pendingAppearedActivity */, new Intent()); // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted // right above tf0. - final TaskFragmentContainer tf2 = new TaskFragmentContainer( - null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, tf0); + final TaskFragmentContainer tf2 = new TaskFragmentContainer.Builder(mController, + taskContainer.getTaskId(), null /* activityInTask */) + .setPendingAppearedIntent(new Intent()) + .setPairedPrimaryContainer(tf0) + .build(); assertEquals(0, taskContainer.indexOf(tf0)); assertEquals(1, taskContainer.indexOf(tf2)); assertEquals(2, taskContainer.indexOf(tf1)); @@ -567,18 +568,15 @@ public class TaskFragmentContainerTest { @Test public void testNewContainerWithPairedPendingAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer tf0 = new TaskFragmentContainer( - createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryTaskFragment */); - final TaskFragmentContainer tf1 = new TaskFragmentContainer( - null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, - null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf0 = createTaskFragmentContainer(taskContainer, + createMockActivity(), null /* pendingAppearedIntent */); + final TaskFragmentContainer tf1 = createTaskFragmentContainer(taskContainer, + null /* pendingAppearedActivity */, new Intent()); // When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any // TaskFragment without any Activity. - final TaskFragmentContainer tf2 = new TaskFragmentContainer( - createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, - null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf2 = createTaskFragmentContainer(taskContainer, + createMockActivity(), null /* pendingAppearedIntent */); assertEquals(0, taskContainer.indexOf(tf0)); assertEquals(1, taskContainer.indexOf(tf2)); assertEquals(2, taskContainer.indexOf(tf1)); @@ -587,9 +585,8 @@ public class TaskFragmentContainerTest { @Test public void testIsVisible() { final TaskContainer taskContainer = createTestTaskContainer(); - final TaskFragmentContainer container = new TaskFragmentContainer( - null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, - null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer container = createTaskFragmentContainer(taskContainer, + null /* pendingAppearedActivity */, new Intent()); // Not visible when there is not appeared. assertFalse(container.isVisible()); @@ -615,4 +612,14 @@ public class TaskFragmentContainerTest { doReturn(activity).when(mController).getActivity(activityToken); return activity; } + + private TaskFragmentContainer createTaskFragmentContainer(TaskContainer taskContainer, + Activity pendingAppearedActivity, Intent pendingAppearedIntent) { + final int taskId = taskContainer.getTaskId(); + mController.addTaskContainer(taskId, taskContainer); + return new TaskFragmentContainer.Builder(mController, taskId, pendingAppearedActivity) + .setPendingAppearedActivity(pendingAppearedActivity) + .setPendingAppearedIntent(pendingAppearedIntent) + .build(); + } } 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..25d3067a34bc 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -45,13 +45,13 @@ 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", "src/com/android/wm/shell/common/split/SplitScreenConstants.java", "src/com/android/wm/shell/common/TransactionPool.java", "src/com/android/wm/shell/common/TriangleShape.java", + "src/com/android/wm/shell/common/desktopmode/*.kt", "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java", "src/com/android/wm/shell/pip/PipContentOverlay.java", "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", @@ -166,10 +166,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 { @@ -188,12 +206,14 @@ android_library { "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.arch.core_core-runtime", + "androidx.compose.material3_material3", "androidx-constraintlayout_constraintlayout", "androidx.dynamicanimation_dynamicanimation", "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..bcb1d292fce2 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -23,4 +23,36 @@ <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" /> + + <activity + android:name=".bubbles.shortcut.CreateBubbleShortcutActivity" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" + android:label="Bubbles" + android:icon="@drawable/ic_bubbles_shortcut_widget"> + <intent-filter> + <action android:name="android.intent.action.CREATE_SHORTCUT" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <activity + android:name=".bubbles.shortcut.ShowBubblesActivity" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" > + <intent-filter> + <action android:name="com.android.wm.shell.bubbles.action.SHOW_BUBBLES"/> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + </application> </manifest> diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp index 3891eebbc01f..7f8f57b172ff 100644 --- a/libs/WindowManager/Shell/aconfig/Android.bp +++ b/libs/WindowManager/Shell/aconfig/Android.bp @@ -10,4 +10,4 @@ aconfig_declarations { java_aconfig_library { name: "com_android_wm_shell_flags_lib", aconfig_declarations: "com_android_wm_shell_flags", -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 7ff204c695f8..112eb617e7a6 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -1,3 +1,5 @@ +# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto + package: "com.android.wm.shell" container: "system" @@ -64,3 +66,58 @@ 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" +} + +flag { + name: "enable_bubble_stashing" + namespace: "multitasking" + description: "Allow the floating bubble stack to stash on the edge of the screen" + bug: "341361249" +} + +flag { + name: "enable_tiny_taskbar" + namespace: "multitasking" + description: "Enables Taskbar on phones" + bug: "341784466" +} + +flag { + name: "enable_bubble_anything" + namespace: "multitasking" + description: "Enable UI affordances to put other content into a bubble" + bug: "342245211" +} + +flag { + name: "only_reuse_bubbled_task_when_launched_from_bubble" + namespace: "multitasking" + description: "Allow reusing bubbled tasks for new activities only when launching from bubbles" + bug: "328229865" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "animate_bubble_size_change" + namespace: "multitasking" + description: "Turns on the animation for bubble bar icons size change" + bug: "335575529" + metadata { + purpose: PURPOSE_BUGFIX + } +} 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..9e1440d5716b 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,56 @@ 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) + ) + + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + 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) + ) + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + 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 +294,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 +322,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 +531,109 @@ 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 windowBounds = Rect(0, 0, 2000, 2600) + val insets = Insets.of(10, 20, 5, 15) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = insets, + windowBounds = windowBounds + ) + positioner.update(deviceConfig) + + val bubbleBarHeight = 100 + positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight + + 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.bubbleBarTopOnScreen - 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 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..327e2059557c 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,40 +18,52 @@ 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.platform.test.flag.junit.SetFlagsRule 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.Flags 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 java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import java.util.function.Consumer +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.function.Consumer /** Unit tests for [BubbleStackView]. */ @SmallTest @RunWith(AndroidJUnit4::class) class BubbleStackViewTest { + @get:Rule val setFlagsRule = SetFlagsRule() + private val context = ApplicationProvider.getApplicationContext<Context>() private lateinit var positioner: BubblePositioner private lateinit var iconFactory: BubbleIconFactory @@ -61,9 +73,12 @@ class BubbleStackViewTest { private lateinit var windowManager: IWindowManager private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory private lateinit var bubbleData: BubbleData + private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager + private var sysuiProxy = mock<SysuiProxy>() @Before fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false windowManager = WindowManagerGlobal.getWindowManagerService()!! @@ -80,7 +95,6 @@ class BubbleStackViewTest { ) ) positioner = BubblePositioner(context, windowManager) - val bubbleStackViewManager = FakeBubbleStackViewManager() bubbleData = BubbleData( context, @@ -89,8 +103,7 @@ class BubbleStackViewTest { BubbleEducationController(context), shellExecutor ) - - val sysuiProxy = mock<SysuiProxy>() + bubbleStackViewManager = FakeBubbleStackViewManager() expandedViewManager = FakeBubbleExpandedViewManager() bubbleTaskViewFactory = FakeBubbleTaskViewFactory() bubbleStackView = @@ -104,34 +117,267 @@ 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() + } + + 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() + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_noOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) - 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(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + // Overflow shouldn't be attached + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_hasOverflowContents_hasOverflow() { + // Add a bubble to the overflow + val bubble1 = createAndInflateChatBubble(key = "bubble1") + bubbleData.notificationEntryUpdated(bubble1, false, false) + bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE) + assertThat(bubbleData.overflowBubbles).isNotEmpty() + + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_hasOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_true() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_false() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // The overflow should've been removed + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_ignored() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // showOverflow should've been ignored, so the overflow would be attached + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + 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 +398,6 @@ class BubbleStackViewTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() assertThat(bubble.isInflated).isTrue() - return bubble } private class FakeBubbleStackViewManager : BubbleStackViewManager { @@ -176,7 +421,7 @@ class BubbleStackViewTest { r.run() } - override fun removeCallbacks(r: Runnable) {} + override fun removeCallbacks(r: Runnable?) {} override fun hasCallback(r: Runnable): Boolean = false } @@ -211,5 +456,7 @@ class BubbleStackViewTest { override fun isStackExpanded(): Boolean = false override fun isShowingAsBubbleBar(): Boolean = false + + override fun hideCurrentInputMethod() {} } } 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..ace2c131050c --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,459 @@ +/* + * 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.getInstrumentation +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.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.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_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 dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + 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.bubbleBarTopOnScreen = + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnRightSide() { + getInstrumentation().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(RIGHT) + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnLeftSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_rightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_leftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_rightToLeftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_leftToRightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_rightToLeft_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + // 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() + } + + /** + * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_leftToRight_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + // 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() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_rightToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_leftToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_rightToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_leftToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_rightToLeftToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_leftToRightToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_rightToLeftRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_leftToRightRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + private fun getExpectedDropTargetBoundsOnLeft(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + true /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun getExpectedDropTargetBoundsOnRight(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + false /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) + } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) + } + } + + private fun View.bounds(): Rect { + return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect -> + rect.offsetTo(x.toInt(), y.toInt()) + } + } + + 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..0d64527b6c13 100644 --- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml +++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml @@ -24,8 +24,8 @@ android:toDegrees="275"> <shape android:shape="ring" - android:thickness="3dp" - android:innerRadius="17dp" + android:thickness="2dp" + 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_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml index 5d9fe67e8bee..9566f2f140c7 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml @@ -15,7 +15,8 @@ ~ limitations under the License. --> <shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@android:color/white" /> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow" /> <corners android:radius="@dimen/desktop_mode_maximize_menu_corner_radius" /> </shape> 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..a30cfb74bf4a 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,10 @@ --> <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"/> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow"/> + <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/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml index 022594982ca3..4e673e65e053 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml @@ -14,9 +14,19 @@ ~ 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="#bf309fb5" /> - <corners android:radius="20dp" /> - <stroke android:width="1dp" color="#A00080FF"/> -</shape> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:id="@+id/indicator_solid"> + <shape android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorPrimaryContainer" /> + <corners android:radius="28dp" /> + </shape> + </item> + <item android:id="@+id/indicator_stroke"> + <shape android:shape="rectangle"> + <corners android:radius="28dp" /> + <stroke android:width="1dp" + android:color="?androidprv:attr/materialColorPrimaryContainer"/> + </shape> + </item> +</layer-list> diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml new file mode 100644 index 000000000000..b208f2fea7b2 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml @@ -0,0 +1,19 @@ +<?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. + --> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_bubbles_shortcut_widget_background" /> + <foreground android:drawable="@drawable/ic_bubbles_shortcut_widget_foreground" /> +</adaptive-icon> diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml new file mode 100644 index 000000000000..510221fb2859 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="M0,0h108v108h-108z" + android:fillColor="#FFC20C"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml new file mode 100644 index 000000000000..a41b6a961bb2 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml @@ -0,0 +1,36 @@ +<?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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@android:color/white"> + <group android:scaleX="0.58" + android:scaleY="0.58" + android:translateX="5.04" + android:translateY="5.04"> + <path + android:fillColor="@android:color/white" + android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> + <path + android:fillColor="@android:color/white" + android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> + <path + android:fillColor="@android:color/white" + android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/> + </group> +</vector>
\ 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_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.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_app_handle.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_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index a5605a7ff50a..7b31c1420a7c 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -27,13 +27,11 @@ <LinearLayout android:id="@+id/open_menu_button" android:layout_width="wrap_content" - android:layout_height="match_parent" - android:tint="?androidprv:attr/materialColorOnSurface" - android:background="?android:selectableItemBackground" + android:layout_height="40dp" android:orientation="horizontal" android:clickable="true" android:focusable="true" - android:paddingStart="12dp"> + android:layout_marginStart="12dp"> <ImageView android:id="@+id/application_icon" android:layout_width="@dimen/desktop_mode_caption_icon_radius" @@ -80,11 +78,12 @@ <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:clickable="true" - android:focusable="true" /> + android:focusable="true"/> <ImageButton android:id="@+id/close_window" @@ -93,8 +92,6 @@ android:paddingHorizontal="10dp" android:paddingVertical="8dp" android:layout_marginEnd="8dp" - android:tint="?androidprv:attr/materialColorOnSurface" - android:background="?android:selectableItemBackgroundBorderless" android:contentDescription="@string/close_button_text" android:src="@drawable/desktop_mode_header_ic_close" android:scaleType="centerCrop" 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..7d5f9cdbebc8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -15,41 +15,87 @@ ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" style="?android:attr/buttonBarStyle" android:layout_width="@dimen/desktop_mode_maximize_menu_width" android:layout_height="@dimen/desktop_mode_maximize_menu_height" android:orientation="horizontal" android:gravity="center" - android: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"/> + <Button + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_maximize_button" + style="?android:attr/buttonBarButtonStyle" + android:stateListAnimator="@null" + android:layout_marginRight="8dp" + android:layout_marginBottom="4dp" + android:alpha="0"/> - <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..cf1b8947467e 100644 --- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml @@ -16,23 +16,29 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <ProgressBar - android:id="@+id/progress_bar" - style="?android:attr/progressBarStyleHorizontal" - android:progressDrawable="@drawable/circular_progress" - android:layout_width="40dp" - android:layout_height="40dp" - android:indeterminate="false" - android:visibility="invisible"/> + + <FrameLayout + android:layout_width="44dp" + android:layout_height="40dp"> + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:progressDrawable="@drawable/circular_progress" + android:layout_width="32dp" + android:layout_height="32dp" + android:indeterminate="false" + android:layout_marginHorizontal="6dp" + android:layout_marginVertical="4dp" + android:visibility="invisible"/> + </FrameLayout> <ImageButton android:id="@+id/maximize_window" - android:layout_width="40dp" + android:layout_width="44dp" android:layout_height="40dp" - android:padding="9dp" + android:paddingHorizontal="10dp" + android:paddingVertical="8dp" android:contentDescription="@string/maximize_button_text" - android:tint="?androidprv:attr/materialColorOnSurface" - android:background="?android:selectableItemBackgroundBorderless" android:src="@drawable/decor_desktop_mode_maximize_button_dark" android:scaleType="fitCenter" /> </merge>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index dd6f8455f82a..1c8f5e60c5c9 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -53,7 +53,7 @@ <string name="accessibility_split_top" msgid="2789329702027147146">"Verdeel bo"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Verdeel onder"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Gebruik eenhandmodus"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die program om uit te gaan"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die app om uit te gaan"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Begin eenhandmodus"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Verlaat eenhandmodus"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Instellings vir <xliff:g id="APP_NAME">%1$s</xliff:g>-borrels"</string> @@ -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..532ecc64358f 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -56,7 +56,7 @@ <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Каб выйсці, правядзіце па экране пальцам знізу ўверх або націсніце ў любым месцы над праграмай"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Запусціць рэжым кіравання адной рукой"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Выйсці з рэжыму кіравання адной рукой"</string> - <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налады ўсплывальных апавяшчэнняў у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налады ўсплывальных чатаў у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Дадатковае меню"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Зноў дадаць у стос"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ад праграмы \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string> @@ -70,16 +70,16 @@ <string name="bubbles_app_settings" msgid="3617224938701566416">"Налады \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Адхіліць апавяшчэнне"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не паказваць размову ў выглядзе ўсплывальных апавяшчэнняў"</string> - <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя апавяшчэнні"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя апавяшчэнні. Націсніце, каб адкрыць усплывальнае апавяшчэнне. Перацягніце яго, каб перамясціць."</string> - <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Кіруйце ўсплывальнымі апавяшчэннямі ў любы час"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Каб выключыць усплывальныя апавяшчэнні з гэтай праграмы, націсніце \"Кіраваць\""</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя чаты"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя чаты. Націсніце, каб адкрыць усплывальны чат. Перацягніце яго, каб перамясціць."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Кіруйце ўсплывальнымі чатамі"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Каб выключыць усплывальныя чаты з гэтай праграмы, націсніце \"Кіраваць\""</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Зразумела"</string> - <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма нядаўніх усплывальных апавяшчэнняў"</string> - <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Нядаўнія і адхіленыя ўсплывальныя апавяшчэнні будуць паказаны тут"</string> - <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"Чат з выкарыстаннем усплывальных апавяшчэнняў"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма нядаўніх усплывальных чатаў"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Нядаўнія і адхіленыя ўсплывальныя чаты будуць паказаны тут"</string> + <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"Усплывальныя чаты"</string> <string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"Новыя размовы паказваюцца ў выглядзе значкоў у ніжнім вугле экрана. Націсніце на іх, каб разгарнуць. Перацягніце іх, калі хочаце закрыць."</string> - <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"Кіруйце наладамі ўсплывальных апавяшчэнняў у любы час"</string> + <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"Кіруйце наладамі ўсплывальных чатаў у любы час"</string> <string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"Каб кіраваць усплывальнымі апавяшчэннямі для праграм і размоў, націсніце тут"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string> @@ -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..150a6e642529 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -84,7 +84,7 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string> - <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci kvůli lepšímu zobrazení restartujete"</string> + <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci restartujete kvůli lepšímu zobrazení"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Změnit v Nastavení poměr stran této aplikace"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Změnit poměr stran"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s fotoaparátem?\nKlepnutím vyřešíte"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zavřít"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otevřít nabídku"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 2bdb29d67447..8878910a4d2c 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Luk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Åbn menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimér skærm"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tilpas skærm"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index e99d9d0c269a..7b91559e9179 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -48,8 +48,8 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % oben"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vollbild unten"</string> - <string name="accessibility_split_left" msgid="1713683765575562458">"Links teilen"</string> - <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts teilen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Links positionieren"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts positionieren"</string> <string name="accessibility_split_top" msgid="2789329702027147146">"Oben teilen"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Unten teilen"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Einhandmodus wird verwendet"</string> @@ -62,7 +62,7 @@ <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> von <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aus <xliff:g id="APP_NAME">%2$s</xliff:g> und <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> weiteren"</string> <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Nach oben links verschieben"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach rechts oben verschieben"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach oben rechts verschieben"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Nach unten links verschieben"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Nach unten rechts verschieben"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> maximieren"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Schließen"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menü öffnen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Bildschirm maximieren"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Bildschirm teilen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index d8bb740535fc..14e5e2f87ab8 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Άνοιγμα μενού"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 2525b321d9d7..cb9ee4f6b6b3 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 5e1b274705dd..7427b62679be 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 0623bef925e2..8498807f9fdb 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 9fe77ddf7e28..406c1f37c455 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir el menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index b88f215eb54e..0583d79da127 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 529b6d10b3c6..70547f566ea6 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sule"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ava menüü"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Kuva täisekraanil"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Kuva poolel ekraanil"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 7438f4240eae..4be35eac6c1f 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Itxi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ireki menua"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zatitu pantaila"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index f7fcb2162603..32d5f5f34fb8 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"بستن"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string> <string name="expand_menu_text" msgid="3847736164494181168">"باز کردن منو"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"بزرگ کردن صفحه"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"بزرگ کردن صفحه"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 400107317637..6f03545e5542 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sulje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Avaa valikko"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Suurenna näyttö"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Jaa näyttö"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index b54f9cf2f15d..3492f136c4f9 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -53,7 +53,7 @@ <string name="accessibility_split_top" msgid="2789329702027147146">"Diviser dans la partie supérieure"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Diviser dans la partie inférieure"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode Une main"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'application"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'appli"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode Une main"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode Une main"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Paramètres pour les bulles de l\'application <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> @@ -62,9 +62,9 @@ <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> autres"</string> <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Déplacer dans coin sup. gauche"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer dans coin sup. droit"</string> - <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer dans coin inf. gauche"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer dans coin inf. droit"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer en haut à droite"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer en bas à gauche"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"développer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"réduire <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Agrandir l\'écran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aligner l\'écran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 357ff91df06d..4002e4d04d51 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -52,7 +52,7 @@ <string name="accessibility_split_right" msgid="8441001008181296837">"Affichée à droite"</string> <string name="accessibility_split_top" msgid="2789329702027147146">"Affichée en haut"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Affichée en haut"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode une main"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utilisation du mode une main"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran de bas en haut ou appuyez n\'importe où au-dessus de l\'application"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode une main"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode une main"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mettre en plein écran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fractionner l\'écran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index a62190754129..c371f7f62feb 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -61,10 +61,10 @@ <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Engadir de novo á pilla"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> máis"</string> - <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover á parte super. esquerda"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover á parte superior dereita"</string> - <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover á parte infer. esquerda"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover á parte inferior dereita"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover arriba á esquerda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba á dereita"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abaixo á esquerda"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abaixo á dereita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"despregar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Pechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar pantalla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 43c178f50d15..7e3d7a373be4 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string> <string name="expand_menu_text" msgid="3847736164494181168">"મેનૂ ખોલો"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"સ્ક્રીન કરો મોટી કરો"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"સ્ક્રીન સ્નૅપ કરો"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 9f6a57fa0d73..cd0f4e3618f7 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"बंद करें"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string> <string name="expand_menu_text" msgid="3847736164494181168">"मेन्यू खोलें"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 4378c5642f5c..27d4cfcf22d5 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -52,7 +52,7 @@ <string name="accessibility_split_right" msgid="8441001008181296837">"Podijeli desno"</string> <string name="accessibility_split_top" msgid="2789329702027147146">"Podijeli gore"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Podijeli dolje"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Upotreba načina rada jednom rukom"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izlaz prijeđite prstom od dna zaslona prema gore ili dodirnite bio gdje iznad aplikacije"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokretanje načina rada jednom rukom"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izlaz iz načina rada jednom rukom"</string> @@ -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..caa114fd6f88 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -95,7 +95,7 @@ <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"앱 위치를 조정하려면 앱 외부를 두 번 탭합니다."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string> <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string> - <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보도록 다시 시작할까요?"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보이도록 다시 시작할까요?"</string> <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"앱을 다시 시작하면 화면에 더 잘 맞게 볼 수는 있지만 진행 상황 또는 저장되지 않은 변경사항을 잃을 수도 있습니다."</string> <string name="letterbox_restart_cancel" msgid="1342209132692537805">"취소"</string> <string name="letterbox_restart_restart" msgid="8529976234412442973">"다시 시작"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"닫기"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string> <string name="expand_menu_text" msgid="3847736164494181168">"메뉴 열기"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"화면 최대화"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"화면 분할"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index b2a0a49b401a..302c0071a73a 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -71,7 +71,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Калкып чыкма билдирмени жабуу"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Жазышууда калкып чыкма билдирмелер көрүнбөсүн"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Калкып чыкма билдирмелер аркылуу маектешүү"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн таптап коюңуз. Жылдыруу үчүн сүйрөңүз."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн тийип коюңуз. Жылдыруу үчүн сүйрөңүз."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Калкып чыкма билдирмелерди каалаган убакта көзөмөлдөңүз"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Бул колдонмодогу калкып чыкма билдирмелерди өчүрүү үчүн \"Башкарууну\" басыңыз"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Түшүндүм"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Жабуу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Менюну ачуу"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды чоңойтуу"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды сүрөткө тартып алуу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 1cbdbd412631..a3519636b71f 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ປິດ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ເປີດເມນູ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ປັບຈໍໃຫຍ່ສຸດ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ສະແນັບໜ້າຈໍ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index d154c57704a1..e4dd7398f679 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Uždaryti"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Atidaryti meniu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index ce269503ceef..99aebf626322 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Aizvērt"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Atvērt izvēlni"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizēt ekrānu"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fiksēt ekrānu"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 9d69c50626be..c152c60fa631 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Затворете"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Отвори го менито"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Подели го екранот на половина"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index c0e83386d572..90275cdb517a 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string> <string name="expand_menu_text" msgid="3847736164494181168">"മെനു തുറക്കുക"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"സ്ക്രീൻ വലുതാക്കുക"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"സ്ക്രീൻ സ്നാപ്പ് ചെയ്യുക"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index ba5d283fb8a7..5e43506ab621 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -53,7 +53,7 @@ <string name="accessibility_split_top" msgid="2789329702027147146">"Дээд талд хуваах"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Доод талд хуваах"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Нэг гарын горимыг ашиглаж байна"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Гарахын тулд дэлгэцийн доод хэсгээс дээш шударч эсвэл апп дээр хүссэн газраа товшино уу"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Гарахын тулд дэлгэцийн доод хэсгээс дээш шударч эсвэл аппын дээр хүссэн газраа товшино уу"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Нэг гарын горимыг эхлүүлэх"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Нэг гарын горимоос гарах"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>-н бөмбөлгүүдийн тохиргоо"</string> @@ -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..6005be4fb5c9 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -85,7 +85,7 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string> <string name="restart_button_description" msgid="4564728020654658478">"Trykk for å starte denne appen på nytt og få en bedre visning"</string> - <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i innstillingene"</string> + <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i Innstillinger"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Endre høyde/bredde-forholdet"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Har du kameraproblemer?\nTrykk for å tilpasse"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ble ikke problemet løst?\nTrykk for å gå tilbake"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Lukk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Åpne menyen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimer skjermen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fest skjermen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 9a26b7e25187..a5bd2ab5c10b 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -69,7 +69,7 @@ <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> कोल्याप्स गर्नुहोस्"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string> - <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाइयोस्"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाउनुहोस्"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जुनसुकै बेला बबलहरू नियन्त्रण गर्नुहोस्"</string> @@ -99,7 +99,7 @@ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"यो एप तपाईंको स्क्रिनमा अझ राम्रोसँग देखियोस् भन्नाका लागि तपाईं सो एप रिस्टार्ट गर्न सक्नुहुन्छ तर तपाईंले अहिलेसम्म गरेका क्रियाकलाप वा सेभ गर्न बाँकी परिवर्तनहरू हट्न सक्छन्"</string> <string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द गर्नुहोस्"</string> <string name="letterbox_restart_restart" msgid="8529976234412442973">"रिस्टार्ट गर्नुहोस्"</string> - <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाइयोस्"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाउनुहोस्"</string> <string name="letterbox_reachability_reposition_text" msgid="3522042240665748268">"यो एप सार्न डबल\nट्याप गर्नुहोस्"</string> <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string> <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string> <string name="expand_menu_text" msgid="3847736164494181168">"मेनु खोल्नुहोस्"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index a38cb7547385..0cd27c5c1457 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Sluiten"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Menu openen"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Scherm maximaliseren"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Scherm halveren"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index e3097beb6166..bf751852a255 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ମେନୁ ଖୋଲନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ସ୍କ୍ରିନକୁ ସ୍ନାପ କରନ୍ତୁ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 3aea6f69749f..325c1e80c433 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string> <string name="expand_menu_text" msgid="3847736164494181168">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ਸਕ੍ਰੀਨ ਦਾ ਆਕਾਰ ਵਧਾਓ"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ਸਕ੍ਰੀਨ ਨੂੰ ਸਨੈਪ ਕਰੋ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index aec3722cefa5..a7648c8e323b 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zamknij"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otwórz menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index ba24d7b3eb07..e47d151337b2 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index f636da7997eb..1210fe8fda05 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -62,9 +62,9 @@ <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> do <xliff:g id="APP_NAME">%2$s</xliff:g> e mais<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>."</string> <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover p/ parte sup. esquerda"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover parte superior direita"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover p/ parte sup. direita"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover p/ parte infer. esquerda"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover parte inferior direita"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover p/ parte inf. direita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expandir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"reduzir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index ba24d7b3eb07..e47d151337b2 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index c20f350ae198..ae871f3dd42b 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Închide"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Deschide meniul"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizează fereastra"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Micșorează fereastra și fixeaz-o"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 49347d2d8086..971e146ba77e 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -61,10 +61,10 @@ <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Добавить обратно в стек"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> из приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\" и ещё <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> - <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Перенести в левый верхний угол"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перенести в правый верхний угол"</string> - <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перенести в левый нижний угол"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перенести в правый нижний угол"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Переместить в левый верхний угол"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Переместить в правый верхний угол"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Переместить в левый нижний угол"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Переместить в правый нижний угол"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"Развернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Свернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: настройки"</string> @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Закрыть"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Открыть меню"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Развернуть на весь экран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Свернуть"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index e5a974683e49..ef1381cbe635 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"වසන්න"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string> <string name="expand_menu_text" msgid="3847736164494181168">"මෙනුව විවෘත කරන්න"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"තිරය උපරිම කරන්න"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ස්නැප් තිරය"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index c2d20ddb0d3b..55a03122483b 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zavrieť"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Otvoriť ponuku"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovať obrazovku"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zobraziť polovicu obrazovky"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index cfe4480c6e1a..bb123dcdbfb6 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Zapri"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Odpri meni"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index cba98c2fb61a..c74a8cd23338 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Mbyll"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Hap menynë"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizo ekranin"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Regjistro ekranin"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 5031f5bf5d64..0694a973dc1e 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Затворите"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Отворите мени"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Повећај екран"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Уклопи екран"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 742be37b67ef..8e0bcfe91679 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Stäng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Öppna menyn"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximera skärmen"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fäst skärmen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 68a7262d0eaf..41180abcf712 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"Funga"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string> <string name="expand_menu_text" msgid="3847736164494181168">"Fungua Menyu"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Panua Dirisha kwenye Skrini"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Panga Madirisha kwenye Skrini"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index fe8fa057dc95..01ac78d984f3 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"மூடும்"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string> <string name="expand_menu_text" msgid="3847736164494181168">"மெனுவைத் திற"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 9be3f3354635..6224e72c19fe 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -117,4 +117,6 @@ <string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string> <string name="expand_menu_text" msgid="3847736164494181168">"మెనూను తెరవండి"</string> + <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్ను పెంచండి"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్ను స్నాప్ చేయండి"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index cd3bf6a626c0..fe0b74c469f4 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -64,7 +64,7 @@ <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ย้ายไปด้านซ้ายบน"</string> <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ย้ายไปด้านขวาบน"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ย้ายไปด้านซ้ายล่าง"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขาวล่าง"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขวาล่าง"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ขยาย <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ยุบ <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"การตั้งค่า <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> @@ -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 8baaf2f155af..c2ba064ac7b6 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -45,9 +45,6 @@ <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu --> <bool name="config_pipEnableResizeForMenu">true</bool> - <!-- PiP minimum size, which is a % based off the shorter side of display width and height --> - <fraction name="config_pipShortestEdgePercent">40%</fraction> - <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">1000</integer> @@ -91,11 +88,45 @@ 16x16 </string> + <!-- Default percentages for the PIP size logic. + 1. Determine max widths + Subtract width of system UI and default padding from the shortest edge of the device. + This is the max width. + 2. Calculate Default and Mins + Default is config_pipSystemPreferredDefaultSizePercent of max-width/height. + Min is config_pipSystemPreferredMinimumSizePercent of it. --> + <item name="config_pipSystemPreferredDefaultSizePercent" format="float" type="dimen">0.6</item> + <item name="config_pipSystemPreferredMinimumSizePercent" format="float" type="dimen">0.5</item> + <!-- Default percentages for the PIP size logic when the Display is close to square. + This is used instead when the display is square-ish, like fold-ables when unfolded, + to make sure that default PiP does not cover the hinge (halfway of the display). + 0. Determine if the display is square-ish + If min(displayWidth, displayHeight) / max(displayWidth, displayHeight) is greater than + config_pipSquareDisplayThresholdForSystemPreferredSize, we use the percent for + square display listed below. + 1. Determine max widths + Subtract width of system UI and default padding from the shortest edge of the device. + This is the max width. + 2. Calculate Default and Mins + Default is config_pipSystemPreferredDefaultSizePercentForSquareDisplay of max-width/height. + Min is config_pipSystemPreferredMinimumSizePercentForSquareDisplay of it. --> + <item name="config_pipSquareDisplayThresholdForSystemPreferredSize" + format="float" type="dimen">0.95</item> + <item name="config_pipSystemPreferredDefaultSizePercentForSquareDisplay" + format="float" type="dimen">0.5</item> + <item name="config_pipSystemPreferredMinimumSizePercentForSquareDisplay" + format="float" type="dimen">0.4</item> + <!-- The percentage of the screen width to use for the default width or height of picture-in-picture windows. Regardless of the percent set here, calculated size will never - be smaller than @dimen/default_minimal_size_pip_resizable_task. --> + be smaller than @dimen/default_minimal_size_pip_resizable_task. + This is used in legacy spec, use config_pipSystemPreferredDefaultSizePercent instead. --> <item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item> + <!-- PiP minimum size, which is a % based off the shorter side of display width and height. + This is used in legacy spec, use config_pipSystemPreferredMinimumSizePercent instead. --> + <fraction name="config_pipShortestEdgePercent">40%</fraction> + <!-- The default aspect ratio for picture-in-picture windows. --> <item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen"> 1.777778 @@ -145,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..595d34664cfa 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 --> @@ -247,13 +247,13 @@ <!-- Padding for the bubble popup view contents. --> <dimen name="bubble_popup_padding">24dp</dimen> <!-- The size of the caption bar inset at the top of bubble bar expanded view. --> - <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen> + <dimen name="bubble_bar_expanded_view_caption_height">36dp</dimen> <!-- The width of the caption bar at the top of bubble bar expanded view. --> - <dimen name="bubble_bar_expanded_view_caption_width">128dp</dimen> - <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. --> - <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> + <dimen name="bubble_bar_expanded_view_caption_width">80dp</dimen> + <!-- The height of the handle shown for the caption menu in the bubble bar expanded view. --> + <dimen name="bubble_bar_expanded_view_handle_height">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 +272,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 +421,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> @@ -451,17 +459,35 @@ start of this area. --> <dimen name="desktop_mode_customizable_caption_margin_end">152dp</dimen> + <!-- The default minimum allowed window width when resizing a window in desktop mode. --> + <dimen name="desktop_mode_minimum_window_width">386dp</dimen> + + <!-- The default minimum allowed window height when resizing a window in desktop mode. --> + <dimen name="desktop_mode_minimum_window_height">352dp</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 padding of the maximize menu in desktop mode. --> + <dimen name="desktop_mode_menu_padding">16dp</dimen> - <!-- The larger of the two corner radii of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_large_corner_radius">4dp</dimen> + <!-- The height of the buttons in the maximize menu. --> + <dimen name="desktop_mode_maximize_menu_button_height">52dp</dimen> - <!-- The smaller of the two corner radii of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_small_corner_radius">2dp</dimen> + <!-- The radius of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_radius">4dp</dimen> + + <!-- The radius of the layout outline around the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_outline_radius">6dp</dimen> + <!-- The stroke width of the outline around the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen> + <!-- The radius of the inner fill of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> + <!-- The padding between the outline and fill of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> @@ -502,18 +528,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> @@ -525,6 +562,28 @@ for corner drags without transition. --> <dimen name="desktop_mode_split_from_desktop_height">100dp</dimen> + <!-- The corner radius of a task that was dragged from fullscreen. --> + <dimen name="desktop_mode_dragged_task_radius">28dp</dimen> + + <!-- The corner radius of the app chip, maximize and close button's ripple drawable --> + <dimen name="desktop_mode_header_buttons_ripple_radius">16dp</dimen> + <!-- The vertical inset to apply to the app chip's ripple drawable --> + <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen> + + <!-- The corner radius of the maximize button's ripple drawable --> + <dimen name="desktop_mode_header_maximize_ripple_radius">18dp</dimen> + <!-- The vertical inset to apply to the maximize button's ripple drawable --> + <dimen name="desktop_mode_header_maximize_ripple_inset_vertical">4dp</dimen> + <!-- The horizontal inset to apply to the maximize button's ripple drawable --> + <dimen name="desktop_mode_header_maximize_ripple_inset_horizontal">6dp</dimen> + + <!-- The corner radius of the close button's ripple drawable --> + <dimen name="desktop_mode_header_close_ripple_radius">18dp</dimen> + <!-- The vertical inset to apply to the close button's ripple drawable --> + <dimen name="desktop_mode_header_close_ripple_inset_vertical">4dp</dimen> + <!-- The horizontal inset to apply to the close button's ripple drawable --> + <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen> + <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) --> <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item> <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) --> @@ -535,5 +594,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..47846746b205 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -182,6 +182,12 @@ <!-- Content description to tell the user a bubble has been dismissed. --> <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> + <!-- Label used to for bubbles shortcut [CHAR_LIMIT=10] --> + <string name="bubble_shortcut_label">Bubbles</string> + + <!-- Longer label used to for bubbles shortcut, shown if there is enough space [CHAR_LIMIT=25] --> + <string name="bubble_shortcut_long_label">Show Bubbles</string> + <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> <string name="restart_button_description">Tap to restart this app for a better view</string> @@ -280,4 +286,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/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java new file mode 100644 index 000000000000..4876f327a650 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java @@ -0,0 +1,184 @@ +/* + * 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.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; + +/** + * Constants for desktop mode feature + */ +public class DesktopModeStatus { + + /** + * Flag to indicate whether task resizing is veiled. + */ + private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_veiled_resizing", true); + + /** + * Flag to indicate is moving task to another display is enabled. + */ + public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_change_display", false); + + /** + * Flag to indicate whether to apply shadows to windows in desktop mode. + */ + private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean( + "persist.wm.debug.desktop_use_window_shadows", true); + + /** + * Flag to indicate whether to apply shadows to the focused window in desktop mode. + * + * Note: this flag is only relevant if USE_WINDOW_SHADOWS is false. + */ + private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean( + "persist.wm.debug.desktop_use_window_shadows_focused_window", false); + + /** + * 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); + + /** + * 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); + + /** Whether the desktop density override is enabled. */ + public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false); + + /** Override density for tasks when they're inside the desktop. */ + public static final int DESKTOP_DENSITY_OVERRIDE = + SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284); + + /** The minimum override density allowed for tasks inside the desktop. */ + private static final int DESKTOP_DENSITY_MIN = 100; + + /** The maximum override density allowed for tasks inside the desktop. */ + private static final int DESKTOP_DENSITY_MAX = 1000; + + /** + * 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(); + } + + /** + * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. + */ + public static boolean isVeiledResizeEnabled() { + return IS_VEILED_RESIZE_ENABLED; + } + + /** + * Return whether to use window shadows. + * + * @param isFocusedWindow whether the window to apply shadows to is focused + */ + public static boolean useWindowShadow(boolean isFocusedWindow) { + return USE_WINDOW_SHADOWS + || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow); + } + + /** + * Return whether to use rounded corners for windows. + */ + 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(); + } + + /** + * Return {@code true} if the override desktop density is enabled and valid. + */ + public static boolean useDesktopOverrideDensity() { + return isDesktopDensityOverrideEnabled() && isValidDesktopDensityOverrideSet(); + } + + /** + * Return {@code true} if the override desktop density is enabled. + */ + private static boolean isDesktopDensityOverrideEnabled() { + return DESKTOP_DENSITY_OVERRIDE_ENABLED; + } + + /** + * Return {@code true} if the override desktop density is set and within a valid range. + */ + private static boolean isValidDesktopDensityOverrideSet() { + return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN + && DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX; + } +} 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..dc022b4afd3b 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,17 @@ 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) || isClosingMode(mode); + } + + /** 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 is closing mode. */ + public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) { + return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; } /** Returns {@code true} if the transition has a display change. */ @@ -318,7 +327,7 @@ public class TransitionUtil { null, new Rect(change.getStartAbsBounds()), taskInfo, - change.getAllowEnterPip(), + change.isAllowEnterPip(), INVALID_WINDOW_TYPE ); target.setWillShowImeOnTarget( 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..3ded7d246499 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; @@ -174,6 +175,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements .setName("home_task_overlay_container") .setContainerLayer() .setHidden(false) + .setCallsite("ShellTaskOrganizer.mHomeTaskOverlayContainer") .build(); /** @@ -551,10 +553,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements // Notify the compat UI if the listener or task info changed. notifyCompatUI(taskInfo, newListener); } - if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) { - // Notify the recent tasks when a task changes windowing modes + final boolean windowModeChanged = + data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode(); + final boolean visibilityChanged = data.getTaskInfo().isVisible != taskInfo.isVisible; + if (windowModeChanged || visibilityChanged) { mRecentTasks.ifPresent(recentTasks -> - recentTasks.onTaskWindowingModeChanged(taskInfo)); + recentTasks.onTaskRunningInfoChanged(taskInfo)); } // TODO (b/207687679): Remove check for HOME once bug is fixed final boolean isFocusedOrHome = taskInfo.isFocused @@ -718,8 +722,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/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 0272f1cda6ef..b9868629e64b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -38,6 +38,7 @@ import android.view.animation.TranslateAnimation; import android.window.TransitionInfo; import com.android.internal.policy.TransitionAnimation; +import com.android.window.flags.Flags; import com.android.wm.shell.shared.TransitionUtil; /** Animation spec for ActivityEmbedding transition. */ @@ -202,7 +203,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadOpenAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change, isEnter); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -229,7 +230,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadCloseAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change, isEnter); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -261,8 +262,14 @@ class ActivityEmbeddingAnimationSpec { } @Nullable - private Animation loadCustomAnimation(@NonNull TransitionInfo info, boolean isEnter) { - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + private Animation loadCustomAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, boolean isEnter) { + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } if (options == null || options.getType() != ANIM_CUSTOM) { return null; } 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..b4ef9f0fc2ac 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; @@ -31,6 +32,7 @@ import android.os.IBinder; import android.util.ArrayMap; import android.view.SurfaceControl; import android.window.TransitionInfo; +import android.window.TransitionInfo.AnimationOptions; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -38,6 +40,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -90,6 +93,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( @@ -110,24 +119,39 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle return false; } - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); - if (options != null) { - // Scene-transition should be handled by app side. - if (options.getType() == ANIM_SCENE_TRANSITION) { + return shouldAnimateAnimationOptions(info); + } + + private boolean shouldAnimateAnimationOptions(@NonNull TransitionInfo info) { + if (!Flags.moveAnimationOptionsToChange()) { + return shouldAnimateAnimationOptions(info.getAnimationOptions()); + } + for (TransitionInfo.Change change : info.getChanges()) { + if (!shouldAnimateAnimationOptions(change.getAnimationOptions())) { + // If any of override animation is not supported, don't animate the transition. return false; } - // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition, - // and Activity#overrideActivityTransition are supported. - if (options.getType() == ANIM_CUSTOM) { - return true; - } - // Use default transition handler to animate other override animation. - return !isSupportedOverrideAnimation(options); } - return true; } + private boolean shouldAnimateAnimationOptions(@Nullable AnimationOptions options) { + if (options == null) { + return true; + } + // Scene-transition should be handled by app side. + if (options.getType() == ANIM_SCENE_TRANSITION) { + return false; + } + // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition, + // and Activity#overrideActivityTransition are supported. + if (options.getType() == ANIM_CUSTOM) { + return true; + } + // Use default transition handler to animate other override animation. + return !isSupportedOverrideAnimation(options); + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, 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/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java index 7749394b21d3..d754d04e6b33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java @@ -19,8 +19,6 @@ package com.android.wm.shell.back; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; -import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; - import android.annotation.NonNull; import android.graphics.Color; import android.graphics.Rect; @@ -45,6 +43,7 @@ public class BackAnimationBackground { private boolean mIsRequestingStatusBarAppearance; private boolean mBackgroundIsDark; private Rect mStartBounds; + private int mStatusbarHeight; public BackAnimationBackground(RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; @@ -56,9 +55,10 @@ public class BackAnimationBackground { * @param startRect The start bounds of the closing target. * @param color The background color. * @param transaction The animation transaction. + * @param statusbarHeight The height of the statusbar (in px). */ - public void ensureBackground( - Rect startRect, int color, @NonNull SurfaceControl.Transaction transaction) { + public void ensureBackground(Rect startRect, int color, + @NonNull SurfaceControl.Transaction transaction, int statusbarHeight) { if (mBackgroundSurface != null) { return; } @@ -80,6 +80,7 @@ public class BackAnimationBackground { .show(mBackgroundSurface); mStartBounds = startRect; mIsRequestingStatusBarAppearance = false; + mStatusbarHeight = statusbarHeight; } /** @@ -111,14 +112,14 @@ public class BackAnimationBackground { /** * Update back animation background with for the progress. * - * @param progress Progress value from {@link android.window.BackProgressAnimator} + * @param top The top coordinate of the closing target */ - public void onBackProgressed(float progress) { + public void customizeStatusBarAppearance(int top) { if (mCustomizer == null || mStartBounds.isEmpty()) { return; } - final boolean shouldCustomizeSystemBar = progress > UPDATE_SYSUI_FLAGS_THRESHOLD; + final boolean shouldCustomizeSystemBar = top > mStatusbarHeight / 2; if (shouldCustomizeSystemBar == mIsRequestingStatusBarAppearance) { return; } 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..7041ea307b0f 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 @@ -18,13 +18,9 @@ package com.android.wm.shell.back; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.predictiveBackSystemAnims; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; 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,7 +28,9 @@ 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.graphics.Rect; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Bundle; @@ -43,19 +41,19 @@ import android.os.SystemClock; import android.os.SystemProperties; 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; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; +import android.view.WindowManager; 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,13 @@ 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.animation.FlingAnimationUtils; +import com.android.wm.shell.R; 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,22 +79,14 @@ 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; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", SETTING_VALUE_ON) == SETTING_VALUE_ON; - public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms - public static final float FLING_SPEED_UP_FACTOR = 0.6f; - - /** - * The maximum additional progress in case of fling gesture. - * The end animation starts after the user lifts the finger from the screen, we continue to - * fire {@link BackEvent}s until the velocity reaches 0. - */ - private static final float MAX_FLING_PROGRESS = 0.3f; /* 30% of the screen */ /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); @@ -114,9 +105,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 final FlingAnimationUtils mFlingAnimationUtils; + private boolean mThresholdCrossed = false; + private boolean mPointersPilfered = false; + private final boolean mRequirePointerPilfer; /** Registry for the back animations */ private final ShellBackAnimationRegistry mShellBackAnimationRegistry; @@ -130,22 +121,25 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; + private final WindowManager mWindowManager; + @VisibleForTesting + final Rect mTouchableArea = new Rect(); /** * 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 +148,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mActiveCallback; + @Nullable + private RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( @@ -169,6 +165,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 +178,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,17 +219,16 @@ 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; - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - mFlingAnimationUtils = new FlingAnimationUtils.Builder(displayMetrics) - .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) - .build(); mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); mShellCommandHandler = shellCommandHandler; + mWindowManager = context.getSystemService(WindowManager.class); + updateTouchableArea(); } private void onInit() { @@ -240,6 +238,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 +288,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final BackAnimationImpl mBackAnimation = new BackAnimationImpl(); @Override + public void onConfigurationChanged(Configuration newConfig) { + mShellBackAnimationRegistry.onConfigurationChanged(newConfig); + updateTouchableArea(); + } + + private void updateTouchableArea() { + mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds()); + } + + @Override public Context getContext() { return mContext; } @@ -318,8 +327,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } @Override - public void onPilferPointers() { - BackAnimationController.this.onPilferPointers(); + public void onThresholdCrossed() { + BackAnimationController.this.onThresholdCrossed(); } @Override @@ -341,6 +350,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,23 +413,34 @@ 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)); + if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) { + tryPilferPointers(); + } + } else if (shouldDispatchToAnimator) { + tryPilferPointers(); } } + private boolean isAppProgressGenerationAllowed() { + return mBackNavigationInfo.isAppProgressGenerationAllowed() + && mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea); + } + /** * Called when a new motion event needs to be transferred to this * {@link BackAnimationController} @@ -426,7 +453,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 +489,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 +508,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 +537,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,11 +550,15 @@ 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. cancelLatencyTracking(); tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); + if (!isAppProgressGenerationAllowed()) { + tryPilferPointers(); + } } } @@ -557,10 +602,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 +637,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 +648,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 +665,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (callback == null) { + if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null + && isAppProgressGenerationAllowed())) { return; } try { @@ -691,7 +680,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 +701,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 +717,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 +729,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 +739,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 +799,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 +813,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 +844,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 +856,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 +888,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 +926,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 +989,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."); - 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); - } - } + if (!validateAnimationTargets(apps)) { + Log.e(TAG, "Invalid animation targets!"); 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 +1040,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..a3111b31a2f9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -0,0 +1,586 @@ +/* + * 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.util.TimeUtils +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.dynamicanimation.animation.FloatValueHolder +import com.android.internal.dynamicanimation.animation.SpringAnimation +import com.android.internal.dynamicanimation.animation.SpringForce +import com.android.internal.jank.Cuj +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.internal.policy.SystemBarUtils +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 val tempRectF = RectF() + + private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + private var statusbarHeight = SystemBarUtils.getStatusBarHeight(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() + protected 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 + + private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE) + private var lastPostCommitFlingScale = SPRING_SCALE + private val postCommitFlingSpring = SpringForce(SPRING_SCALE) + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + protected var gestureProgress = 0f + + /** 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 [startClosingRect] and [targetClosingRect] to define the movement + * of the closingTarget during pre-commit phase. + */ + abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int) + + /** + * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement + * of the enteringTarget during pre-commit phase. + */ + abstract fun preparePreCommitEnteringRectMovement() + + /** + * Subclasses must provide a duration (in ms) for the post-commit part of the animation + */ + abstract fun getPostCommitAnimationDuration(): Long + + /** + * 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) + statusbarHeight = SystemBarUtils.getStatusBarHeight(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) + + preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge) + preparePreCommitEnteringRectMovement() + + background.ensureBackground( + closingTarget!!.windowConfiguration.bounds, + getBackgroundColor(), + transaction, + statusbarHeight + ) + 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) + gestureProgress = 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() + background.customizeStatusBarAppearance(currentClosingRect.top.toInt()) + } + + 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(velocity: Float) { + if ( + closingTarget?.leash == null || + enteringTarget?.leash == null || + !enteringTarget!!.leash.isValid || + !closingTarget!!.leash.isValid + ) { + finishAnimation() + return + } + + // kick off spring animation with the current velocity from the pre-commit phase, this + // affects the scaling of the closing and/or opening activity during post-commit + val startVelocity = + if (gestureProgress < 0.1f) -DEFAULT_FLING_VELOCITY else -velocity * SPRING_SCALE + val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE) + .setStartVelocity(startVelocity.coerceIn(-MAX_FLING_VELOCITY, 0f)) + .setStartValue(SPRING_SCALE) + .setSpring(postCommitFlingSpring) + flingAnimation.start() + // do an animation-frame immediately to prevent idle frame + flingAnimation.doAnimationFrame(choreographer.lastFrameTimeNanos / TimeUtils.NANOS_PER_MS) + + val valueAnimator = + ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration()) + 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) + if (!triggerBack) transaction.setAlpha(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 + lastPostCommitFlingScale = SPRING_SCALE + gestureProgress = 0f + } + + protected fun applyTransform( + leash: SurfaceControl?, + rect: RectF, + alpha: Float, + baseTransformation: Transformation? = null, + flingMode: FlingMode = FlingMode.NO_FLING + ) { + if (leash == null || !leash.isValid) return + tempRectF.set(rect) + if (flingMode != FlingMode.NO_FLING) { + lastPostCommitFlingScale = min( + postCommitFlingScale.value / SPRING_SCALE, + if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale + ) + // apply an additional scale to the closing target to account for fling velocity + tempRectF.scaleCentered(lastPostCommitFlingScale) + } + val scale = tempRectF.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(tempRectF.left, tempRectF.top) + transaction + .setAlpha(leash, 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(progressAnimator.velocity) + } + } + + 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 SPRING_SCALE = 100f + private const val MAX_FLING_VELOCITY = 1000f + private const val DEFAULT_FLING_VELOCITY = 120f + } + + enum class FlingMode { + NO_FLING, + + /** + * This is used for the closing target in custom cross-activity back animations. When the + * back gesture is flung, the closing target shrinks a bit further with a spring motion. + */ + FLING_SHRINK, + + /** + * This is used for the closing and opening target in the default cross-activity back + * animation. When the back gesture is flung, the closing and opening targets shrink a + * bit further and then bounce back with a spring motion. + */ + FLING_BOUNCE + } +} + +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..381914a58cf2 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; @@ -46,10 +48,11 @@ import android.window.BackProgressAnimator; import android.window.IOnBackInvokedCallback; import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.policy.SystemBarUtils; 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 +82,8 @@ 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; + private int mStatusbarHeight; // The closing window properties. private final Rect mClosingStartRect = new Rect(); @@ -91,7 +95,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(); @@ -112,11 +116,21 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { @Inject public CrossTaskBackAnimation(Context context, BackAnimationBackground background) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); mBackAnimationRunner = new BackAnimationRunner( new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK); mBackground = background; mContext = context; + loadResources(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + loadResources(); + } + + private void loadResources() { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + mStatusbarHeight = SystemBarUtils.getStatusBarHeight(mContext); } private static float mapRange(float value, float min, float max) { @@ -142,7 +156,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { // Draw background. mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - BACKGROUNDCOLOR, mTransaction); + BACKGROUNDCOLOR, mTransaction, mStatusbarHeight); mInterWindowMargin = mContext.getResources() .getDimension(R.dimen.cross_task_back_inter_window_margin); mVerticalMargin = mContext.getResources() @@ -192,9 +206,9 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius); applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius); - mTransaction.apply(); + applyTransaction(); - mBackground.onBackProgressed(progress); + mBackground.customizeStatusBarAppearance((int) scaledTop); } private void updatePostCommitClosingAnimation(float progress) { @@ -242,6 +256,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 +274,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { if (mBackground != null) { mBackground.removeBackground(mTransaction); } - - mTransaction.apply(); + applyTransaction(); mBackInProgress = false; mTransformMatrix.reset(); mClosingCurrentRect.setEmpty(); @@ -275,8 +293,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 +321,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) { mBackground.resetStatusBarCustomization(); } - mTransaction.apply(); + applyTransaction(); }); valueAnimator.addListener(new AnimatorListenerAdapter() { @@ -326,6 +342,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..c738ce542f8a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt @@ -0,0 +1,302 @@ +/* + * 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.BackEvent +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 +import kotlin.math.max +import kotlin.math.min + +/** 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() + + 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 preparePreCommitClosingRectMovement(swipeEdge: Int) { + startClosingRect.set(backAnimRect) + + // scale closing target to the left for right-hand-swipe and to the right for + // left-hand-swipe + targetClosingRect.set(startClosingRect) + targetClosingRect.scaleCentered(MAX_SCALE) + val offset = if (swipeEdge != BackEvent.EDGE_RIGHT) { + startClosingRect.right - targetClosingRect.right - displayBoundsMargin + } else { + -targetClosingRect.left + displayBoundsMargin + } + targetClosingRect.offset(offset, 0f) + } + + override fun preparePreCommitEnteringRectMovement() { + // No movement for the entering rect + startEnteringRect.set(startClosingRect) + targetEnteringRect.set(startClosingRect) + } + + override fun getPostCommitAnimationDuration(): Long { + return min( + MAX_POST_COMMIT_ANIM_DURATION, max(closeAnimation!!.duration, enterAnimation!!.duration) + ) + } + + override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation { + 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 + + val closingProgress = closeAnimation!!.getPostCommitProgress(linearProgress) + applyTransform( + closingTarget!!.leash, + currentClosingRect, + closingProgress, + closeAnimation!!, + FlingMode.FLING_SHRINK + ) + val enteringProgress = MathUtils.lerp( + gestureProgress * PRE_COMMIT_MAX_PROGRESS, + 1f, + enterAnimation!!.getPostCommitProgress(linearProgress) + ) + applyTransform( + enteringTarget!!.leash, + currentEnteringRect, + enteringProgress, + enterAnimation!!, + FlingMode.NO_FLING + ) + applyTransaction() + } + + private fun applyTransform( + leash: SurfaceControl, + rect: RectF, + progress: Float, + animation: Animation, + flingMode: FlingMode + ) { + transformation.clear() + animation.getTransformationAt(progress, transformation) + applyTransform(leash, rect, transformation.alpha, transformation, flingMode) + } + + override fun finishAnimation() { + closeAnimation?.reset() + closeAnimation = null + enterAnimation?.reset() + enterAnimation = null + transformation.clear() + 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 + } + + private fun Animation.getPostCommitProgress(linearProgress: Float): Float { + return when (duration) { + 0L -> 1f + else -> min( + 1f, + getPostCommitAnimationDuration() / min( + MAX_POST_COMMIT_ANIM_DURATION, + duration + ).toFloat() * linearProgress + ) + } + } + + class AnimationLoadResult { + var closeAnimation: Animation? = null + var enterAnimation: Animation? = null + var backgroundColor = 0 + } + + companion object { + private const val PRE_COMMIT_MAX_PROGRESS = 0.2f + private const val MAX_POST_COMMIT_ANIM_DURATION = 2000L + } +} + +/** 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..3b5eb3613d2a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt @@ -0,0 +1,113 @@ +/* + * 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 android.window.BackEvent +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.EMPHASIZED + private val enteringStartOffset = + context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) + override val allowEnteringYShift = true + + override fun preparePreCommitClosingRectMovement(swipeEdge: Int) { + 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 (swipeEdge != BackEvent.EDGE_RIGHT) { + targetClosingRect.offset( + startClosingRect.right - targetClosingRect.right - displayBoundsMargin, + 0f + ) + } + } + + 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 getPostCommitAnimationDuration() = POST_COMMIT_DURATION + + override fun onGestureCommitted(velocity: Float) { + // 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(velocity) + } + + override fun onPostCommitProgress(linearProgress: Float) { + super.onPostCommitProgress(linearProgress) + val closingAlpha = max(1f - linearProgress * 5, 0f) + val progress = postCommitInterpolator.getInterpolation(linearProgress) + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + applyTransform( + closingTarget?.leash, + currentClosingRect, + closingAlpha, + flingMode = FlingMode.FLING_BOUNCE + ) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + applyTransform( + enteringTarget?.leash, + currentEnteringRect, + 1f, + flingMode = FlingMode.FLING_BOUNCE + ) + applyTransaction() + } + + + companion object { + private const val POST_COMMIT_DURATION = 450L + } +} 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/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index da530d740d48..1279fc42c066 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -305,6 +305,7 @@ public class Bubble implements BubbleViewProvider { getUser().getIdentifier(), getPackageName(), getTitle(), + getAppName(), isImportantConversation()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 96aaf02cb5e3..d2c36e6b637c 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 @@ -86,12 +86,15 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.util.CollectionUtils; 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; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.bubbles.properties.BubbleProperties; +import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -101,13 +104,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 +173,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 +460,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; } } @@ -474,11 +479,16 @@ public class BubbleController implements ConfigurationChangeListener, mDisplayController.addDisplayChangingController( (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { - // This is triggered right before the rotation is applied - if (fromRotation != toRotation) { + Rect newScreenBounds = new Rect(); + if (newDisplayAreaInfo != null) { + newScreenBounds = + newDisplayAreaInfo.configuration.windowConfiguration.getBounds(); + } + // This is triggered right before the rotation or new screen size is applied + if (fromRotation != toRotation || !newScreenBounds.equals(mScreenBounds)) { if (mStackView != null) { // Layout listener set on stackView will update the positioner - // once the rotation is applied + // once the rotation or screen change is applied mStackView.onOrientationChanged(); } } @@ -503,6 +513,10 @@ public class BubbleController implements ConfigurationChangeListener, } mCurrentProfiles = userProfiles; + if (Flags.enableRetrievableBubbles()) { + registerShortcutBroadcastReceiver(); + } + mShellController.addConfigurationChangeListener(this); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES, this::createExternalInterface, this); @@ -510,7 +524,7 @@ public class BubbleController implements ConfigurationChangeListener, } private ExternalInterfaceBinder createExternalInterface() { - return new BubbleController.IBubblesImpl(this); + return new IBubblesImpl(this); } @VisibleForTesting @@ -584,21 +598,15 @@ public class BubbleController implements ConfigurationChangeListener, * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ void hideCurrentInputMethod() { + mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */); int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); try { mBarService.hideCurrentInputMethodForBubbles(displayId); } catch (RemoteException e) { - e.printStackTrace(); + Log.e(TAG, "Failed to hide IME", e); } } - private void openBubbleOverflow() { - ensureBubbleViewsAndWindowCreated(); - mBubbleData.setShowingOverflow(true); - mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); - mBubbleData.setExpanded(true); - } - /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). @@ -708,6 +716,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 @@ -950,6 +993,25 @@ public class BubbleController implements ConfigurationChangeListener, } }; + private void registerShortcutBroadcastReceiver() { + IntentFilter shortcutFilter = new IntentFilter(); + shortcutFilter.addAction(BubbleShortcutHelper.ACTION_SHOW_BUBBLES); + ProtoLog.d(WM_SHELL_BUBBLES, "register broadcast receive for bubbles shortcut"); + mContext.registerReceiver(mShortcutBroadcastReceiver, shortcutFilter, + Context.RECEIVER_NOT_EXPORTED); + } + + private final BroadcastReceiver mShortcutBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ProtoLog.v(WM_SHELL_BUBBLES, "receive broadcast to show bubbles %s", + intent.getAction()); + if (BubbleShortcutHelper.ACTION_SHOW_BUBBLES.equals(intent.getAction())) { + mMainExecutor.execute(() -> showBubblesFromShortcut()); + } + } + }; + /** * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been * added in the meantime. @@ -1129,16 +1191,52 @@ 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 to collapse/expand - * @param isBeingDragged whether the bubble is being dragged + * + * @param bubbleKey key of the bubble being dragged */ - 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 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 location location where bubble was released + * @param topOnScreen top coordinate of the bubble bar on the screen after release + */ + public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) { + mBubblePositioner.setBubbleBarLocation(location); + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); + 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); } } @@ -1178,8 +1276,8 @@ public class BubbleController implements ConfigurationChangeListener, * <p>This is used by external callers (launcher). */ @VisibleForTesting - public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) { - mBubblePositioner.setBubbleBarPosition(bubbleBarBounds); + public void expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen) { + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); @@ -1222,8 +1320,7 @@ public class BubbleController implements ConfigurationChangeListener, } if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { // already in the stack - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(b); } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { // promote it out of the overflow promoteBubbleFromOverflow(b); @@ -1234,20 +1331,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) { @@ -1318,43 +1416,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); } @@ -1383,8 +1483,9 @@ public class BubbleController implements ConfigurationChangeListener, SynchronousScreenCaptureListener screenCaptureListener) { try { ScreenCapture.CaptureArgs args = null; - if (mStackView != null) { - ViewRootImpl viewRoot = mStackView.getViewRootImpl(); + View viewToUse = mStackView != null ? mStackView : mLayerView; + if (viewToUse != null) { + ViewRootImpl viewRoot = viewToUse.getViewRootImpl(); if (viewRoot != null) { SurfaceControl bubbleLayer = viewRoot.getSurfaceControl(); if (bubbleLayer != null) { @@ -1476,6 +1577,12 @@ public class BubbleController implements ConfigurationChangeListener, Log.w(TAG, "Tried to add a bubble to the stack but the stack is null"); } }; + } else if (mBubbleData.isExpanded() && mBubbleData.getSelectedBubble() != null) { + callback = b -> { + if (b.getKey().equals(mBubbleData.getSelectedBubbleKey())) { + mLayerView.showExpandedView(b); + } + }; } for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) { Bubble bubble = mBubbleData.getBubbles().get(i); @@ -1698,7 +1805,7 @@ public class BubbleController implements ConfigurationChangeListener, if (groupKey == null) { return bubbleChildren; } - for (Bubble bubble : mBubbleData.getActiveBubbles()) { + for (Bubble bubble : mBubbleData.getBubbles()) { if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) { bubbleChildren.add(bubble); } @@ -1792,6 +1899,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. */ @@ -1824,6 +1940,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 @@ -1862,7 +1983,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", @@ -1871,13 +1992,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(); @@ -2129,6 +2254,34 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Show bubbles UI when triggered via shortcut. + * + * <p>When there are bubbles visible, expands the top-most bubble. When there are no bubbles + * visible, opens the bubbles overflow UI. + */ + public void showBubblesFromShortcut() { + if (isStackExpanded()) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: stack visible, skip"); + return; + } + if (mBubbleData.getSelectedBubble() != null) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: open selected bubble"); + expandStackWithSelectedBubble(); + return; + } + BubbleViewProvider bubbleToSelect = CollectionUtils.firstOrNull(mBubbleData.getBubbles()); + if (bubbleToSelect == null) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: no bubbles"); + // make sure overflow bubbles are loaded + loadOverflowBubblesFromDisk(); + bubbleToSelect = mBubbleData.getOverflow(); + } + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: select and open %s", + bubbleToSelect.getKey()); + mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); + } + + /** * Description of current bubble state. */ private void dump(PrintWriter pw, String prefix) { @@ -2136,6 +2289,7 @@ public class BubbleController implements ConfigurationChangeListener, pw.print(prefix); pw.println(" currentUserId= " + mCurrentUserId); pw.print(prefix); pw.println(" isStatusBarShade= " + mIsStatusBarShade); pw.print(prefix); pw.println(" isShowingAsBubbleBar= " + isShowingAsBubbleBar()); + pw.print(prefix); pw.println(" isImeVisible= " + mBubblePositioner.isImeVisible()); pw.println(); mBubbleData.dump(pw); @@ -2234,15 +2388,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; @@ -2257,6 +2415,8 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + // Unregister the listeners to ensure any binder death recipients are unlinked + mListener.unregister(); } @Override @@ -2270,16 +2430,9 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void showBubble(String key, Rect bubbleBarBounds) { + public void showBubble(String key, int topOnScreen) { mMainExecutor.execute( - () -> mController.expandStackAndSelectBubbleFromLauncher( - key, bubbleBarBounds)); - } - - @Override - public void removeBubble(String key) { - mMainExecutor.execute( - () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE)); + () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); } @Override @@ -2293,8 +2446,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, int topOnScreen) { + mMainExecutor.execute(() -> mController.stopBubbleDrag(location, topOnScreen)); + } + + @Override + public void dragBubbleToDismiss(String key) { + mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key)); } @Override @@ -2302,6 +2465,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 updateBubbleBarTopOnScreen(int topOnScreen) { + mMainExecutor.execute(() -> { + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); + if (mLayerView != null) mLayerView.updateExpandedView(); + }); + } } private class BubblesImpl implements Bubbles { @@ -2417,17 +2594,6 @@ public class BubbleController implements ConfigurationChangeListener, private CachedState mCachedState = new CachedState(); - private IBubblesImpl mIBubbles; - - @Override - public IBubbles createExternalInterface() { - if (mIBubbles != null) { - mIBubbles.invalidate(); - } - mIBubbles = new IBubblesImpl(BubbleController.this); - return mIBubbles; - } - @Override public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey); @@ -2612,6 +2778,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.isImeVisible(); + } + 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..761e02598460 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()); @@ -252,10 +256,16 @@ public class BubbleData { } /** - * Returns a bubble bar update populated with the current list of active bubbles. + * Returns a bubble bar update populated with the current list of active bubbles, expanded, + * and selected state. */ public BubbleBarUpdate getInitialStateForBubbleBar() { - return mStateChange.getInitialState(); + BubbleBarUpdate initialState = mStateChange.getInitialState(); + initialState.bubbleBarLocation = mPositioner.getBubbleBarLocation(); + initialState.expanded = mExpanded; + initialState.expandedChanged = mExpanded; // only matters if we're expanded + initialState.selectedBubbleKey = getSelectedBubbleKey(); + return initialState; } public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { @@ -321,13 +331,16 @@ public class BubbleData { return mSelectedBubble; } - public BubbleOverflow getOverflow() { - return mOverflow; + /** + * Returns the key of the selected bubble, or null if no bubble is selected. + */ + @Nullable + public String getSelectedBubbleKey() { + return mSelectedBubble != null ? mSelectedBubble.getKey() : null; } - /** Return a read-only current active bubble lists. */ - public List<Bubble> getActiveBubbles() { - return Collections.unmodifiableList(mBubbles); + public BubbleOverflow getOverflow() { + return mOverflow; } public void setExpanded(boolean expanded) { @@ -363,6 +376,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 +421,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 +511,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. @@ -586,7 +602,7 @@ public class BubbleData { List<Bubble> removedBubbles = filterAllBubbles(bubble -> userId == bubble.getUser().getIdentifier()); for (Bubble b : removedBubbles) { - doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED); + doRemove(b.getKey(), Bubbles.DISMISS_USER_ACCOUNT_REMOVED); } if (!removedBubbles.isEmpty()) { dispatchPendingChanges(); @@ -662,13 +678,12 @@ public class BubbleData { || reason == Bubbles.DISMISS_SHORTCUT_REMOVED || reason == Bubbles.DISMISS_PACKAGE_REMOVED || reason == Bubbles.DISMISS_USER_CHANGED - || reason == Bubbles.DISMISS_USER_REMOVED; + || reason == Bubbles.DISMISS_USER_ACCOUNT_REMOVED; int indexToRemove = indexForKey(key); 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 +693,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 +793,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; @@ -897,6 +916,9 @@ public class BubbleData { ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); } mSelectedBubble = bubble; + if (isOverflow) { + mShowingOverflow = true; + } mStateChange.selectedBubble = bubble; mStateChange.selectionChanged = true; } @@ -1216,9 +1238,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..c7ccd50af550 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 @@ -479,31 +482,38 @@ public class BubbleExpandedView extends LinearLayout { mPointerWidth, mPointerHeight, true /* pointLeft */)); mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal( mPointerWidth, mPointerHeight, false /* pointLeft */)); - if (mPointerView != null) { - updatePointerView(); - } + updatePointerViewIfExists(); + updateManageButtonIfExists(); + } - if (mManageButton != null) { - int visibility = mManageButton.getVisibility(); - removeView(mManageButton); - ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), - com.android.internal.R.style.Theme_DeviceDefault_DayNight); - mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate( - R.layout.bubble_manage_button, this /* parent */, false /* attach */); - addView(mManageButton); - mManageButton.setVisibility(visibility); - post(() -> { - int touchAreaHeight = - getResources().getDimensionPixelSize( - R.dimen.bubble_manage_button_touch_area_height); - Rect r = new Rect(); - mManageButton.getHitRect(r); - int extraTouchArea = (touchAreaHeight - r.height()) / 2; - r.top -= extraTouchArea; - r.bottom += extraTouchArea; - setTouchDelegate(new TouchDelegate(r, mManageButton)); - }); + + /** + * Reinflate manage button if {@link #mManageButton} is initialized. + * Does nothing otherwise. + */ + private void updateManageButtonIfExists() { + if (mManageButton == null) { + return; } + int visibility = mManageButton.getVisibility(); + removeView(mManageButton); + ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), + com.android.internal.R.style.Theme_DeviceDefault_DayNight); + mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate( + R.layout.bubble_manage_button, this /* parent */, false /* attach */); + addView(mManageButton); + mManageButton.setVisibility(visibility); + post(() -> { + int touchAreaHeight = + getResources().getDimensionPixelSize( + R.dimen.bubble_manage_button_touch_area_height); + Rect r = new Rect(); + mManageButton.getHitRect(r); + int extraTouchArea = (touchAreaHeight - r.height()) / 2; + r.top -= extraTouchArea; + r.bottom += extraTouchArea; + setTouchDelegate(new TouchDelegate(r, mManageButton)); + }); } void updateFontSize() { @@ -545,11 +555,18 @@ public class BubbleExpandedView extends LinearLayout { if (mTaskView != null) { mTaskView.setCornerRadius(mCornerRadius); } - updatePointerView(); + updatePointerViewIfExists(); + updateManageButtonIfExists(); } - /** Updates the size and visuals of the pointer. **/ - private void updatePointerView() { + /** + * Updates the size and visuals of the pointer if {@link #mPointerView} is initialized. + * Does nothing otherwise. + */ + private void updatePointerViewIfExists() { + if (mPointerView == null) { + return; + } LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { lp.width = mPointerHeight; @@ -668,6 +685,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 +720,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); + } } /** @@ -1045,7 +1069,7 @@ public class BubbleExpandedView extends LinearLayout { // Post because we need the width of the view post(() -> { mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; - updatePointerView(); + updatePointerViewIfExists(); if (showVertically) { mPointerPos.y = bubbleCenter - (mPointerWidth / 2f); if (!isRtl) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index b0d3cc4a5d5c..3d9bf032c1b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -29,6 +29,7 @@ interface BubbleExpandedViewManager { fun setAppBubbleTaskId(key: String, taskId: Int) fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean + fun hideCurrentInputMethod() companion object { /** @@ -73,6 +74,10 @@ interface BubbleExpandedViewManager { override fun isStackExpanded(): Boolean = controller.isStackExpanded override fun isShowingAsBubbleBar(): Boolean = controller.isShowingAsBubbleBar + + override fun hideCurrentInputMethod() { + controller.hideCurrentInputMethod() + } } } } 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..2382545ab324 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,7 +97,8 @@ public class BubblePositioner { private PointF mRestingStackPosition; private boolean mShowingInBubbleBar; - private final Rect mBubbleBarBounds = new Rect(); + private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; + private int mBubbleBarTopOnScreen; public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; @@ -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 isImeVisible() { + 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,11 +830,33 @@ public class BubblePositioner { mShowingInBubbleBar = showingInBubbleBar; } + public void setBubbleBarLocation(BubbleBarLocation location) { + mBubbleBarLocation = location; + } + + public BubbleBarLocation getBubbleBarLocation() { + return mBubbleBarLocation; + } + /** - * Sets the position of the bubble bar in display coordinates. + * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right */ - public void setBubbleBarPosition(Rect bubbleBarBounds) { - mBubbleBarBounds.set(bubbleBarBounds); + public boolean isBubbleBarOnLeft() { + return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl()); + } + + /** + * Set top coordinate of bubble bar on screen + */ + public void setBubbleBarTopOnScreen(int topOnScreen) { + mBubbleBarTopOnScreen = topOnScreen; + } + + /** + * Returns the top coordinate of bubble bar on screen + */ + public int getBubbleBarTopOnScreen() { + return mBubbleBarTopOnScreen; } /** @@ -815,14 +870,45 @@ 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; + return mBubbleBarTopOnScreen - 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..09bec8c37b9a 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; @@ -133,6 +134,8 @@ public class BubbleStackView extends FrameLayout private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; + private static final float OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT = 0.5f; + private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; /** Minimum alpha value for scrim when alpha is being changed via drag */ @@ -144,6 +147,15 @@ public class BubbleStackView extends FrameLayout */ private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; + /** + * Percent of the bubble that is hidden while stashed. + */ + private static final float PERCENT_HIDDEN_WHEN_STASHED = 0.55f; + /** + * How long to wait to animate the stack for stashing. + */ + private static final int ANIMATE_STASH_DELAY = 700; + private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = new PhysicsAnimator.SpringConfig( StackAnimationController.IME_ANIMATION_STIFFNESS, @@ -334,7 +346,7 @@ public class BubbleStackView extends FrameLayout pw.println("Expanded bubble state:"); pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); - final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); + final BubbleExpandedView expandedView = getExpandedView(); if (expandedView != null) { pw.println(" expandedViewVis: " + expandedView.getVisibility()); @@ -449,17 +461,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 +490,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 +548,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 +683,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,10 +732,17 @@ public class BubbleStackView extends FrameLayout mDismissView.hide(); } - mIsDraggingStack = false; + onDraggingEnded(); // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + animateStashedState(false /* stashImmediately */); + } + + @Override + public void onCancel(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY) { + animateStashedState(false /* stashImmediately */); } }; @@ -791,10 +817,11 @@ public class BubbleStackView extends FrameLayout private float getScrimAlphaForDrag(float dragAmount) { // dragAmount should be negative as we allow scroll up only - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { float alphaRange = BUBBLE_EXPANDED_SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG; - int dragMax = mExpandedBubble.getExpandedView().getContentHeight(); + int dragMax = expandedView.getContentHeight(); float dragFraction = dragAmount / dragMax; return Math.max(BUBBLE_EXPANDED_SCRIM_ALPHA - alphaRange * dragFraction, @@ -856,6 +883,7 @@ public class BubbleStackView extends FrameLayout } }; + private boolean mShowingOverflow; private BubbleOverflow mBubbleOverflow; private StackEducationView mStackEduView; private StackEducationView.Manager mStackEducationViewManager; @@ -884,18 +912,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 +955,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 +1013,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 +1040,7 @@ public class BubbleStackView extends FrameLayout WindowManager.class))); onDisplaySizeChanged(); mExpandedAnimationController.updateResources(); + mExpandedAnimationController.onOrientationChanged(); mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); @@ -1089,6 +1111,7 @@ public class BubbleStackView extends FrameLayout } else { maybeShowStackEdu(); } + onDraggingEnded(); }); animate() @@ -1100,33 +1123,35 @@ public class BubbleStackView extends FrameLayout mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { // We need to be Z ordered on top in order for alpha animations to work. - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); - mExpandedBubble.getExpandedView().setAnimating(true); + expandedView.setSurfaceZOrderedOnTop(true); + expandedView.setAnimating(true); mExpandedViewContainer.setVisibility(VISIBLE); } } @Override public void onAnimationEnd(Animator animation) { - if (mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null // The surface needs to be Z ordered on top for alpha values to work on the // TaskView, and if we're temporarily hidden, we are still on the screen // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha // = 0f remains in effect. && !mExpandedViewTemporarilyHidden) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - mExpandedBubble.getExpandedView().setAnimating(false); + expandedView.setSurfaceZOrderedOnTop(false); + expandedView.setAnimating(false); } } }); mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { float alpha = (float) valueAnimator.getAnimatedValue(); - mExpandedBubble.getExpandedView().setContentAlpha(alpha); - mExpandedBubble.getExpandedView().setBackgroundAlpha(alpha); + expandedView.setContentAlpha(alpha); + expandedView.setBackgroundAlpha(alpha); } }); @@ -1146,6 +1171,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 +1237,59 @@ public class BubbleStackView extends FrameLayout } }; + /** + * Animates the bubble stack to stash along the edge of the screen. + * + * @param stashImmediately whether the stash should happen immediately or without delay. + */ + private void animateStashedState(boolean stashImmediately) { + if (!Flags.enableBubbleStashing()) return; + + removeCallbacks(mAnimateStashedState); + + postDelayed(mAnimateStashedState, stashImmediately ? 0 : ANIMATE_STASH_DELAY); + } + + private final Runnable mAnimateStashedState = () -> { + if (mFlyout.getVisibility() != View.VISIBLE + && !mIsDraggingStack + && !isExpansionAnimating() + && !isExpanded() + && !isStackEduVisible()) { + // To calculate a distance, bubble stack needs to be moved to become stashed, + // we need to take into account that the bubble stack is positioned on the edge + // of the available screen rect, which can be offset by system bars and cutouts. + final float amountOffscreen = mBubbleSize - (mBubbleSize * PERCENT_HIDDEN_WHEN_STASHED); + if (mStackAnimationController.isStackOnLeftSide()) { + int availableRectOffsetX = + mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left; + mBubbleContainer + .animate() + .translationX(-(amountOffscreen + availableRectOffsetX)) + .start(); + } else { + int availableRectOffsetX = + mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right; + mBubbleContainer.animate() + .translationX(amountOffscreen - availableRectOffsetX) + .start(); + } + } + }; + + 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); @@ -1309,7 +1395,7 @@ public class BubbleStackView extends FrameLayout } final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) - && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null; + && getExpandedView() != null; ProtoLog.d(WM_SHELL_BUBBLES, "Show manage edu=%b", shouldShow); if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) { Log.w(TAG, "Want to show manage edu, but it is forced hidden"); @@ -1336,9 +1422,9 @@ public class BubbleStackView extends FrameLayout * Show manage education if was not showing before. */ private void showManageEdu() { - if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) return; - mManageEduView.show(mExpandedBubble.getExpandedView(), - mStackAnimationController.isStackOnLeftSide()); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView == null) return; + mManageEduView.show(expandedView, mStackAnimationController.isStackOnLeftSide()); } @VisibleForTesting @@ -1442,24 +1528,66 @@ 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() { + int visibility = GONE; + if (mShowingOverflow) { + if (mIsExpanded || mBubbleData.isShowingOverflow()) { + visibility = VISIBLE; + } + } + if (Flags.enableRetrievableBubbles()) { + if (BubbleOverflow.KEY.equals(mBubbleData.getSelectedBubbleKey()) + && !mBubbleData.hasBubbles()) { + // Hide overflow bubble icon if it is the only bubble + visibility = GONE; + } + } + mBubbleOverflow.setVisible(visibility); + } + + 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 +1647,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 +1814,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 +1867,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(); @@ -1814,6 +1946,11 @@ public class BubbleStackView extends FrameLayout return mExpandedBubble; } + @Nullable + private BubbleExpandedView getExpandedView() { + return mExpandedBubble != null ? mExpandedBubble.getExpandedView() : null; + } + // via BubbleData.Listener @SuppressLint("ClickableViewAccessibility") void addBubble(Bubble bubble) { @@ -1854,7 +1991,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 +2049,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 +2080,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()); @@ -2007,13 +2130,11 @@ public class BubbleStackView extends FrameLayout // If we're expanded, screenshot the currently expanded bubble (before expanding the newly // selected bubble) so we can animate it out. - if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null - && !mExpandedViewTemporarilyHidden) { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - // Before screenshotting, have the real TaskView show on top of other surfaces - // so that the screenshot doesn't flicker on top of it. - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); - } + BubbleExpandedView expandedView = getExpandedView(); + if (mIsExpanded && expandedView != null && !mExpandedViewTemporarilyHidden) { + // Before screenshotting, have the real TaskView show on top of other surfaces + // so that the screenshot doesn't flicker on top of it. + expandedView.setSurfaceZOrderedOnTop(true); try { screenshotAnimatingOutBubbleIntoSurface((success) -> { @@ -2033,11 +2154,18 @@ public class BubbleStackView extends FrameLayout private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; - mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView()); + mExpandedViewAnimationController.setExpandedView(getExpandedView()); if (mIsExpanded) { hideCurrentInputMethod(); + if (Flags.enableRetrievableBubbles()) { + if (mBubbleData.getBubbles().size() == 1) { + // First bubble, check if overflow visibility needs to change + updateOverflowVisibility(); + } + } + // Make the container of the expanded view transparent before removing the expanded view // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the // expanded view becomes visible on the screen. See b/126856255 @@ -2106,6 +2234,16 @@ public class BubbleStackView extends FrameLayout } /** + * Check if we only have overflow expanded. Which is the case when we are launching bubbles from + * background. + */ + private boolean isOnlyOverflowExpanded() { + boolean overflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals( + mExpandedBubble.getKey()); + return overflowExpanded && !mBubbleData.hasBubbles(); + } + + /** * Monitor for swipe up gesture that is used to collapse expanded view */ void startMonitoringSwipeUpGesture() { @@ -2200,7 +2338,7 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.addView(bubble.getIconView(), index, new LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); - updateBubbleShadows(false /* showForAllBubbles */); + updateBubbleShadows(mIsExpanded); requestUpdate(); } } @@ -2215,7 +2353,6 @@ public class BubbleStackView extends FrameLayout * not. */ void hideCurrentInputMethod() { - mPositioner.setImeVisible(false, 0); mManager.hideCurrentInputMethod(); } @@ -2325,7 +2462,7 @@ public class BubbleStackView extends FrameLayout ProtoLog.d(WM_SHELL_BUBBLES, "animateExpansion, expandedBubble=%s", mExpandedBubble != null ? mExpandedBubble.getKey() : "null"); cancelDelayedExpandCollapseSwitchAnimations(); - final boolean showVertically = mPositioner.showBubblesVertically(); + mIsExpanded = true; if (isStackEduVisible()) { mStackEduView.hide(true /* fromExpansion */); @@ -2333,14 +2470,25 @@ public class BubbleStackView extends FrameLayout beforeExpandedViewAnimation(); showScrim(true, null /* runnable */); - updateZOrder(); - updateBadges(false /* setBadgeForCollapsedStack */); + updateBubbleShadows(mIsExpanded); mBubbleContainer.setActiveController(mExpandedAnimationController); updateOverflowVisibility(); + + if (Flags.enableRetrievableBubbles() && isOnlyOverflowExpanded()) { + animateOverflowExpansion(); + } else { + animateBubbleExpansion(); + } + } + + private void animateBubbleExpansion() { + updateBadges(false /* setBadgeForCollapsedStack */); updatePointerPosition(false /* forIme */); + if (Flags.enableBubbleStashing()) { + mBubbleContainer.animate().translationX(0).start(); + } mExpandedAnimationController.expandFromStack(() -> { - if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { + if (mIsExpanded && getExpandedView() != null) { maybeShowManageEdu(); } updateOverflowDotVisibility(true /* expanding */); @@ -2351,63 +2499,67 @@ 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); + final boolean showVertically = mPositioner.showBubblesVertically(); // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles // that are animating farther, so that the expanded view doesn't move as much. final float relevantStackPosition = showVertically ? 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); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setContentAlpha(0f); - mExpandedBubble.getExpandedView().setBackgroundAlpha(0f); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { + expandedView.setContentAlpha(0f); + expandedView.setBackgroundAlpha(0f); // We'll be starting the alpha animation after a slight delay, so set this flag early // here. - mExpandedBubble.getExpandedView().setAnimating(true); + expandedView.setAnimating(true); } mDelayedAnimation = () -> { @@ -2437,10 +2589,9 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { mExpandedViewContainer.setAnimationMatrix(null); afterExpandedViewAnimation(); - if (mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView() - .setSurfaceZOrderedOnTop(false); + BubbleExpandedView expView = getExpandedView(); + if (expView != null) { + expView.setSurfaceZOrderedOnTop(false); } }) .start(); @@ -2448,6 +2599,47 @@ public class BubbleStackView extends FrameLayout mMainExecutor.executeDelayed(mDelayedAnimation, startDelay); } + /** + * Animate expansion of overflow view when it is shown from the bubble shortcut. + * <p> + * Animates the view with a scale originating from the center of the view. + */ + private void animateOverflowExpansion() { + PointF bubbleXY = mPositioner.getExpandedBubbleXY(0, getState()); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(translationY); + mExpandedViewContainer.setAlpha(1f); + + boolean stackOnLeft = mPositioner.isStackOnLeft(getStackPosition()); + float width = mPositioner.getTaskViewContentWidth(stackOnLeft); + float height = mPositioner.getExpandedViewHeight(mExpandedBubble); + float scale = 1f - OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT; + // Scale from the center of the view + mExpandedViewContainerMatrix.setScale(scale, scale, width / 2f, height / 2f); + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + mExpandedViewAlphaAnimator.start(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .addUpdateListener((target, values) -> { + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + }).withEndActions(() -> { + mExpandedViewContainer.setAnimationMatrix(null); + afterExpandedViewAnimation(); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { + expandedView.setSurfaceZOrderedOnTop(false); + } + }).start(); + } + private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); ProtoLog.d(WM_SHELL_BUBBLES, "animateCollapse"); @@ -2474,15 +2666,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,21 +2687,23 @@ public class BubbleStackView extends FrameLayout mManageEduView.hide(); } - updateZOrder(); updateBadges(true /* setBadgeForCollapsedStack */); afterExpandedViewAnimation(); if (previouslySelected != null) { previouslySelected.setTaskViewVisibility(false); } mExpandedViewAnimationController.reset(); + animateStashedState(false /* stashImmediately */); }; - mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after, + collapsePosition); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != 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 // animating. But updates the internal state for the content to be hidden after // animation completes. - mExpandedBubble.getExpandedView().setContentVisibility(false); + expandedView.setContentVisibility(false); } } @@ -2599,10 +2795,10 @@ public class BubbleStackView extends FrameLayout // expanded view animation might not actually set the z ordering for the // expanded view correctly, because the view may still be temporarily // hidden. So set it again here. - BubbleExpandedView bev = mExpandedBubble.getExpandedView(); - if (bev != null) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - mExpandedBubble.getExpandedView().setAnimating(false); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { + expandedView.setSurfaceZOrderedOnTop(false); + expandedView.setAnimating(false); } }) .start(); @@ -2674,13 +2870,13 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded) { mExpandedViewAnimationController.animateForImeVisibilityChange(visible); - if (mPositioner.showBubblesVertically() - && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expandedView = getExpandedView(); + if (mPositioner.showBubblesVertically() && expandedView != null) { float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex, getState()).y; float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY); - mExpandedBubble.getExpandedView().setImeVisible(visible); - if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) { + expandedView.setImeVisible(visible); + if (!expandedView.isUsingMaxHeight()) { mExpandedViewContainer.animate().translationY(newExpandedViewTop); } List<Animator> animList = new ArrayList<>(); @@ -2901,6 +3097,7 @@ public class BubbleStackView extends FrameLayout } // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + animateStashedState(true /* stashImmediately */); }; // Suppress the dot when we are animating the flyout. @@ -2993,6 +3190,13 @@ public class BubbleStackView extends FrameLayout outRect.left -= mBubbleTouchPadding; outRect.right += mBubbleTouchPadding; outRect.bottom += mBubbleTouchPadding; + if (Flags.enableBubbleStashing()) { + if (mStackOnLeftOrWillBe) { + outRect.right += mBubbleTouchPadding; + } else { + outRect.left -= mBubbleTouchPadding; + } + } } } else { mBubbleContainer.getBoundsOnScreen(outRect); @@ -3029,7 +3233,8 @@ public class BubbleStackView extends FrameLayout // This should not happen, since the manage menu is only visible when there's an expanded // bubble. If we end up in this state, just hide the menu immediately. - if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView == null) { mManageMenu.setVisibility(View.INVISIBLE); mManageMenuScrim.setVisibility(INVISIBLE); mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */); @@ -3075,8 +3280,8 @@ public class BubbleStackView extends FrameLayout } } - if (mExpandedBubble.getExpandedView().getTaskView() != null) { - mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage + if (expandedView.getTaskView() != null) { + expandedView.getTaskView().setObscuredTouchRect(mShowingManage ? new Rect(0, 0, getWidth(), getHeight()) : null); } @@ -3086,8 +3291,8 @@ public class BubbleStackView extends FrameLayout // When the menu is open, it should be at these coordinates. The menu pops out to the right // in LTR and to the left in RTL. - mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); - final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin(); + expandedView.getManageButtonBoundsOnScreen(mTempRect); + final float margin = expandedView.getManageButtonMargin(); final float targetX = isLtr ? mTempRect.left - margin : mTempRect.right + margin - mManageMenu.getWidth(); @@ -3111,9 +3316,10 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { View child = mManageMenu.getChildAt(0); child.requestAccessibilityFocus(); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expView = getExpandedView(); + if (expView != null) { // Update the AV's obscured touchable region for the new state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + expView.updateObscuredTouchableRegion(); } }) .start(); @@ -3128,9 +3334,10 @@ public class BubbleStackView extends FrameLayout .spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f) .withEndActions(() -> { mManageMenu.setVisibility(View.INVISIBLE); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expView = getExpandedView(); + if (expView != null) { // Update the AV's obscured touchable region for the new state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + expView.updateObscuredTouchableRegion(); } }) .start(); @@ -3157,9 +3364,8 @@ public class BubbleStackView extends FrameLayout private void updateExpandedBubble() { mExpandedViewContainer.removeAllViews(); - if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - BubbleExpandedView bev = mExpandedBubble.getExpandedView(); + BubbleExpandedView bev = getExpandedView(); + if (mIsExpanded && bev != null) { bev.setContentVisibility(false); bev.setAnimating(!mIsExpansionAnimating); mExpandedViewContainerMatrix.setScaleX(0f); @@ -3187,9 +3393,8 @@ public class BubbleStackView extends FrameLayout } private void updateManageButtonListener() { - if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - BubbleExpandedView bev = mExpandedBubble.getExpandedView(); + BubbleExpandedView bev = getExpandedView(); + if (mIsExpanded && bev != null) { bev.setManageClickListener((view) -> { showManageMenu(true /* show */); }); @@ -3206,14 +3411,13 @@ public class BubbleStackView extends FrameLayout * expanded bubble. */ private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { - if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + final BubbleExpandedView animatingOutExpandedView = getExpandedView(); + if (!mIsExpanded || animatingOutExpandedView == null) { // You can't animate null. onComplete.accept(false); return; } - final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView(); - // Release the previous screenshot if it hasn't been released already. if (mAnimatingOutBubbleBuffer != null) { releaseAnimatingOutBubbleBuffer(); @@ -3245,8 +3449,7 @@ public class BubbleStackView extends FrameLayout mAnimatingOutSurfaceContainer.setTranslationX(translationX); mAnimatingOutSurfaceContainer.setTranslationY(0); - final int[] taskViewLocation = - mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen(); + final int[] taskViewLocation = animatingOutExpandedView.getTaskViewLocationOnScreen(); final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); // Translate the surface to overlap the real TaskView. @@ -3308,15 +3511,15 @@ public class BubbleStackView extends FrameLayout int[] paddings = mPositioner.getExpandedViewContainerPadding( mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), getState()); mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); - mExpandedBubble.getExpandedView().updateTaskViewContentWidth(); - mExpandedBubble.getExpandedView().updateView( - mExpandedViewContainer.getLocationOnScreen()); + expandedView.updateTaskViewContentWidth(); + expandedView.updateView(mExpandedViewContainer.getLocationOnScreen()); updatePointerPosition(false /* forIme */); } @@ -3327,19 +3530,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 +3567,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++) { @@ -3395,7 +3592,8 @@ public class BubbleStackView extends FrameLayout * the pointer is animated to the location. */ private void updatePointerPosition(boolean forIme) { - if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + BubbleExpandedView expandedView = getExpandedView(); + if (mExpandedBubble == null || expandedView == null) { return; } int index = getBubbleIndex(mExpandedBubble); @@ -3406,7 +3604,7 @@ public class BubbleStackView extends FrameLayout float bubblePosition = mPositioner.showBubblesVertically() ? position.y : position.x; - mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, + expandedView.setPointerPosition(bubblePosition, mStackOnLeftOrWillBe, forIme /* animate */); } @@ -3414,8 +3612,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; } /** @@ -3427,7 +3626,7 @@ public class BubbleStackView extends FrameLayout */ int getBubbleIndex(@Nullable BubbleViewProvider provider) { if (provider == null) { - return 0; + return -1; } return mBubbleContainer.indexOfChild(provider.getIconView()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 21b70b8e32da..0b66bcb6930e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -161,6 +161,11 @@ public class BubbleTaskViewHelper { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; + if (mBubble != null && mBubble.isAppBubble()) { + // Let the controller know sooner what the taskId is. + mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); + } + // With the task org, the taskAppeared callback will only happen once the task has // already drawn mListener.onTaskCreated(); 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..82af88d03b19 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; @@ -60,7 +61,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED, + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_ACCOUNT_REMOVED, DISMISS_SWITCH_TO_STACK}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason { @@ -81,7 +82,7 @@ public interface Bubbles { int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; int DISMISS_RELOAD_FROM_DISK = 15; - int DISMISS_USER_REMOVED = 16; + int DISMISS_USER_ACCOUNT_REMOVED = 16; int DISMISS_SWITCH_TO_STACK = 17; /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ @@ -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..1db556c04180 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 @@ -30,16 +31,21 @@ interface IBubbles { oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; - oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3; + oneway void showBubble(in String key, in int topOnScreen) = 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 updateBubbleBarTopOnScreen(in int topOnScreen) = 10; + + oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 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..8e58db198b13 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,14 @@ 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 availableRect = mPositioner.getAvailableRect(); + float pivotX = mPositioner.isBubbleBarOnLeft() ? availableRect.left : availableRect.right; + float pivotY = mPositioner.getBubbleBarTopOnScreen(); + matrix.setScale(scale, scale, pivotX, pivotY); + } + /** * Animate the expanded bubble when it is being dragged */ @@ -477,7 +480,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/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 271fb9abce6a..972dce51e02b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -82,6 +82,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private static final int INVALID_TASK_ID = -1; private BubbleExpandedViewManager mManager; + private BubblePositioner mPositioner; private boolean mIsOverflow; private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; @@ -160,6 +161,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView) { mManager = expandedViewManager; + mPositioner = positioner; mIsOverflow = isOverflow; if (mIsOverflow) { @@ -207,7 +209,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override public void onDismissBubble(Bubble bubble) { - mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED); + mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); } }); mHandleView.setOnClickListener(view -> { @@ -290,15 +292,27 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } /** - * Hides the current modal menu view or collapses the bubble stack. - * Called from {@link BubbleBarLayerView} + * Hides the current modal menu if it is visible + * @return {@code true} if menu was visible and is hidden */ - public void hideMenuOrCollapse() { + public boolean hideMenuIfVisible() { if (mMenuViewController.isMenuVisible()) { - mMenuViewController.hideMenu(/* animated = */ true); - } else { - mManager.collapseStack(); + mMenuViewController.hideMenu(true /* animated */); + return true; + } + return false; + } + + /** + * Hides the IME if it is visible + * @return {@code true} if IME was visible + */ + public boolean hideImeIfVisible() { + if (mPositioner.isImeVisible()) { + mManager.hideCurrentInputMethod(); + return true; } + return false; } /** Updates the bubble shown in the expanded view. */ 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..fa1091c63d00 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,31 @@ class BubbleBarExpandedViewDragController( private inner class MagnetListener : MagnetizedObject.MagnetListener { override fun onStuckToTarget( - target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*> + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*> ) { isStuckToDismiss = true + pinController.onStuckToDismissTarget() } 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) } 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/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index 2b7a0706b4de..d54a6b002e43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -37,15 +37,11 @@ import com.android.wm.shell.R; */ public class BubbleBarHandleView extends View { private static final long COLOR_CHANGE_DURATION = 120; - - // The handle view is currently rendered as 3 evenly spaced dots. - private int mDotSize; - private int mDotSpacing; // Path used to draw the dots private final Path mPath = new Path(); - private @ColorInt int mHandleLightColor; - private @ColorInt int mHandleDarkColor; + private final @ColorInt int mHandleLightColor; + private final @ColorInt int mHandleDarkColor; private @Nullable ObjectAnimator mColorChangeAnim; public BubbleBarHandleView(Context context) { @@ -63,10 +59,8 @@ public class BubbleBarHandleView extends View { public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mDotSize = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_dot_size); - mDotSpacing = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_dot_spacing); + final int handleHeight = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_height); mHandleLightColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_light); mHandleDarkColor = ContextCompat.getColor(getContext(), @@ -76,27 +70,13 @@ public class BubbleBarHandleView extends View { setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - final int handleCenterX = view.getWidth() / 2; final int handleCenterY = view.getHeight() / 2; - final int handleTotalWidth = mDotSize * 3 + mDotSpacing * 2; - final int handleLeft = handleCenterX - handleTotalWidth / 2; - final int handleTop = handleCenterY - mDotSize / 2; - final int handleBottom = handleTop + mDotSize; - RectF dot1 = new RectF( - handleLeft, handleTop, - handleLeft + mDotSize, handleBottom); - RectF dot2 = new RectF( - dot1.right + mDotSpacing, handleTop, - dot1.right + mDotSpacing + mDotSize, handleBottom - ); - RectF dot3 = new RectF( - dot2.right + mDotSpacing, handleTop, - dot2.right + mDotSpacing + mDotSize, handleBottom - ); + final int handleTop = handleCenterY - handleHeight / 2; + final int handleBottom = handleTop + handleHeight; + final int radius = handleHeight / 2; + RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom); mPath.reset(); - mPath.addOval(dot1, Path.Direction.CW); - mPath.addOval(dot2, Path.Direction.CW); - mPath.addOval(dot3, Path.Direction.CW); + mPath.addRoundRect(handle, radius, radius, Path.Direction.CW); outline.setPath(mPath); } }); 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..badc40997902 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,7 +117,22 @@ public class BubbleBarLayerView extends FrameLayout setUpDismissView(); - setOnClickListener(view -> hideMenuOrCollapse()); + 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 -> hideModalOrCollapse()); } @Override @@ -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(); @@ -203,19 +217,22 @@ public class BubbleBarLayerView extends FrameLayout @Override public void onBackPressed() { - hideMenuOrCollapse(); + hideModalOrCollapse(); } }); + 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,40 +341,40 @@ 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 */ - private void hideMenuOrCollapse() { + /** Hides the current modal education/menu view, IME or collapses the expanded view */ + private void hideModalOrCollapse() { if (mEducationViewController.isEducationVisible()) { mEducationViewController.hideEducation(/* animated = */ true); - } else if (isExpanded() && mExpandedView != null) { - mExpandedView.hideMenuOrCollapse(); - } else { - mBubbleController.collapseStack(); + return; + } + if (isExpanded() && mExpandedView != null) { + boolean menuHidden = mExpandedView.hideMenuIfVisible(); + if (menuHidden) { + return; + } + boolean imeHidden = mExpandedView.hideImeIfVisible(); + if (imeHidden) { + return; + } } + mBubbleController.collapseStack(); } /** 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 +403,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/bubbles/shortcut/BubbleShortcutHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt new file mode 100644 index 000000000000..efa12383f188 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt @@ -0,0 +1,40 @@ +/* + * 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.shortcut + +import android.content.Context +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Icon +import com.android.wm.shell.R + +/** Helper class for creating a shortcut to open bubbles */ +object BubbleShortcutHelper { + const val SHORTCUT_ID = "bubbles_shortcut_id" + const val ACTION_SHOW_BUBBLES = "com.android.wm.shell.bubbles.action.SHOW_BUBBLES" + + /** Create a shortcut that launches [ShowBubblesActivity] */ + fun createShortcut(context: Context, icon: Icon): ShortcutInfo { + return ShortcutInfo.Builder(context, SHORTCUT_ID) + .setIntent(ShowBubblesActivity.createIntent(context)) + .setActivity(ShowBubblesActivity.createComponent(context)) + .setShortLabel(context.getString(R.string.bubble_shortcut_label)) + .setLongLabel(context.getString(R.string.bubble_shortcut_long_label)) + .setLongLived(true) + .setIcon(icon) + .build() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt new file mode 100644 index 000000000000..a124f95d7431 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt @@ -0,0 +1,52 @@ +/* + * 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.shortcut + +import android.app.Activity +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Bundle +import com.android.wm.shell.Flags +import com.android.wm.shell.R +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES +import com.android.wm.shell.util.KtProtoLog + +/** Activity to create a shortcut to open bubbles */ +class CreateBubbleShortcutActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Flags.enableRetrievableBubbles()) { + KtProtoLog.d(WM_SHELL_BUBBLES, "Creating a shortcut for bubbles") + createShortcut() + } + finish() + } + + private fun createShortcut() { + val icon = Icon.createWithResource(this, R.drawable.ic_bubbles_shortcut_widget) + // TODO(b/340337839): shortcut shows the sysui icon + val shortcutInfo = BubbleShortcutHelper.createShortcut(this, icon) + val shortcutManager = getSystemService(ShortcutManager::class.java) + val shortcutIntent = shortcutManager?.createShortcutResultIntent(shortcutInfo) + if (shortcutIntent != null) { + setResult(RESULT_OK, shortcutIntent) + } else { + setResult(RESULT_CANCELED) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt new file mode 100644 index 000000000000..ae7940ca1b65 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.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.bubbles.shortcut + +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.android.wm.shell.Flags +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES +import com.android.wm.shell.util.KtProtoLog + +/** Activity that sends a broadcast to open bubbles */ +class ShowBubblesActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Flags.enableRetrievableBubbles()) { + val intent = + Intent().apply { + action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES + // Set the package as the receiver is not exported + `package` = packageName + } + KtProtoLog.v(WM_SHELL_BUBBLES, "Sending broadcast to show bubbles") + sendBroadcast(intent) + } + finish() + } + + companion object { + /** Create intent to launch this activity */ + fun createIntent(context: Context): Intent { + return Intent(context, ShowBubblesActivity::class.java).apply { + action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES + } + } + + /** Create component for this activity */ + fun createComponent(context: Context): ComponentName { + return ComponentName(context, ShowBubblesActivity::class.java) + } + } +} 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/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index ad01d0fa311a..f4ac5f260fcd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -220,6 +220,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + boolean mImeRequestedVisible = + (WindowInsets.Type.defaultVisible() & WindowInsets.Type.ime()) != 0; InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -247,8 +249,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return; } - updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, - WindowInsets.Type.ime())); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, + WindowInsets.Type.ime())); + } final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME); final Rect newFrame = newSource != null ? newSource.getFrame() : null; @@ -287,32 +291,63 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchImeControlTargetChanged(mDisplayId, hasImeSourceControl); } - if (hasImeSourceControl) { + boolean pendingImeStartAnimation = false; + boolean canAnimate; + if (android.view.inputmethod.Flags.refactorInsetsController()) { + canAnimate = hasImeSourceControl && imeSourceControl.getLeash() != null; + } else { + canAnimate = hasImeSourceControl; + } + + boolean positionChanged = false; + if (canAnimate) { if (mAnimation != null) { final Point lastSurfacePosition = hadImeSourceControl ? mImeSourceControl.getSurfacePosition() : null; - final boolean positionChanged = - !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); - if (positionChanged) { - startAnimation(mImeShowing, true /* forceRestart */, - SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED); - } + positionChanged = !imeSourceControl.getSurfacePosition().equals( + lastSurfacePosition); } else { if (!haveSameLeash(mImeSourceControl, imeSourceControl)) { applyVisibilityToLeash(imeSourceControl); + + if (android.view.inputmethod.Flags.refactorInsetsController()) { + pendingImeStartAnimation = true; + } } if (!mImeShowing) { removeImeSurface(); } } - } else if (mAnimation != null) { + } else if (!android.view.inputmethod.Flags.refactorInsetsController() + && mAnimation != null) { + // we don"t want to cancel the hide animation, when the control is lost, but + // continue the bar to slide to the end (even without visible IME) mAnimation.cancel(); } + if (positionChanged) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // For showing the IME, the leash has to be available first. Hiding + // the IME happens directly via {@link #hideInsets} (triggered by + // setImeInputTargetRequestedVisibility) while the leash is not gone + // yet. + pendingImeStartAnimation = true; + } else { + startAnimation(mImeShowing, true /* forceRestart */, + SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED); + } + } if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { mImeSourceControl.release(SurfaceControl::release); } mImeSourceControl = imeSourceControl; + + if (android.view.inputmethod.Flags.refactorInsetsController()) { + if (pendingImeStartAnimation) { + startAnimation(true, true /* forceRestart */, + null /* statsToken */); + } + } } private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) { @@ -354,6 +389,20 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged // Do nothing } + @Override + // TODO(b/335404678): pass control target + public void setImeInputTargetRequestedVisibility(boolean visible) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + mImeRequestedVisible = visible; + // In the case that the IME becomes visible, but we have the control with leash + // already (e.g., when focussing an editText in activity B, while and editText in + // activity A is focussed), we will not get a call of #insetsControlChanged, and + // therefore have to start the show animation from here + startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */, + null /* TODO statsToken */); + } + } + /** * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. */ @@ -402,6 +451,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged private void startAnimation(final boolean show, final boolean forceRestart, @NonNull final ImeTracker.Token statsToken) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + if (mImeSourceControl == null || mImeSourceControl.getLeash() == null) { + if (DEBUG) Slog.d(TAG, "No leash available, not starting the animation."); + return; + } + } final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); if (imeSource == null || mImeSourceControl == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); @@ -463,10 +518,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mAnimation.addUpdateListener(animation -> { SurfaceControl.Transaction t = mTransactionPool.acquire(); float value = (float) animation.getAnimatedValue(); - t.setPosition(mImeSourceControl.getLeash(), x, value); - final float alpha = (mAnimateAlpha || isFloating) - ? (value - hiddenY) / (shownY - hiddenY) : 1.f; - t.setAlpha(mImeSourceControl.getLeash(), alpha); + if (!android.view.inputmethod.Flags.refactorInsetsController() || ( + mImeSourceControl != null && mImeSourceControl.getLeash() != null)) { + t.setPosition(mImeSourceControl.getLeash(), x, value); + final float alpha = (mAnimateAlpha || isFloating) + ? (value - hiddenY) / (shownY - hiddenY) : 1.f; + t.setAlpha(mImeSourceControl.getLeash(), alpha); + } dispatchPositionChanged(mDisplayId, imeTop(value), t); t.apply(); mTransactionPool.release(t); @@ -480,8 +538,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationStart(Animator animation) { + ValueAnimator valueAnimator = (ValueAnimator) animation; + float value = (float) valueAnimator.getAnimatedValue(); SurfaceControl.Transaction t = mTransactionPool.acquire(); - t.setPosition(mImeSourceControl.getLeash(), x, startY); + t.setPosition(mImeSourceControl.getLeash(), x, value); if (DEBUG) { Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" + imeTop(hiddenY) + "->" + imeTop(shownY) @@ -491,7 +551,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; final float alpha = (mAnimateAlpha || isFloating) - ? (startY - hiddenY) / (shownY - hiddenY) + ? (value - hiddenY) / (shownY - hiddenY) : 1.f; t.setAlpha(mImeSourceControl.getLeash(), alpha); if (mAnimationDirection == DIRECTION_SHOW) { @@ -502,7 +562,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (DEBUG_IME_VISIBILITY) { EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, - mDisplayId, mAnimationDirection, alpha, startY , endY, + mDisplayId, mAnimationDirection, alpha, value, endY, Objects.toString(mImeSourceControl.getLeash()), Objects.toString(mImeSourceControl.getInsetsHint()), Objects.toString(mImeSourceControl.getSurfacePosition()), @@ -525,17 +585,25 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationEnd(Animator animation) { + boolean hasLeash = + mImeSourceControl != null && mImeSourceControl.getLeash() != null; if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); SurfaceControl.Transaction t = mTransactionPool.acquire(); if (!mCancelled) { - t.setPosition(mImeSourceControl.getLeash(), x, endY); - t.setAlpha(mImeSourceControl.getLeash(), 1.f); + if (!android.view.inputmethod.Flags.refactorInsetsController() + || hasLeash) { + t.setPosition(mImeSourceControl.getLeash(), x, endY); + t.setAlpha(mImeSourceControl.getLeash(), 1.f); + } } dispatchEndPositioning(mDisplayId, mCancelled, t); if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); - t.hide(mImeSourceControl.getLeash()); + if (!android.view.inputmethod.Flags.refactorInsetsController() + || hasLeash) { + t.hide(mImeSourceControl.getLeash()); + } removeImeSurface(); ImeTracker.forLogging().onHidden(mStatsToken); } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) { @@ -548,9 +616,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, mDisplayId, mAnimationDirection, endY, - Objects.toString(mImeSourceControl.getLeash()), - Objects.toString(mImeSourceControl.getInsetsHint()), - Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString( + mImeSourceControl != null ? mImeSourceControl.getLeash() + : "null"), + Objects.toString(mImeSourceControl != null + ? mImeSourceControl.getInsetsHint() : "null"), + Objects.toString(mImeSourceControl != null + ? mImeSourceControl.getSurfacePosition() : "null"), Objects.toString(mImeFrame)); } t.apply(); 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..1fb0e1745e3e 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; @@ -199,6 +199,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } + private void setImeInputTargetRequestedVisibility(boolean visible) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.setImeInputTargetRequestedVisibility(visible); + } + } + @BinderThread private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @@ -240,6 +250,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan PerDisplay.this.hideInsets(types, fromIme, statsToken); }); } + + @Override + public void setImeInputTargetRequestedVisibility(boolean visible) + throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.setImeInputTargetRequestedVisibility(visible); + }); + } } } @@ -291,5 +309,12 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan */ default void hideInsets(@InsetsType int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {} + + /** + * Called to set the requested visibility of the IME in DisplayImeController. Invoked by + * {@link com.android.server.wm.DisplayContent.RemoteInsetsControlTarget}. + * @param visible requested status of the IME + */ + default void setImeInputTargetRequestedVisibility(boolean visible) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java deleted file mode 100644 index b29058b1f204..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2021 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; - -import android.Manifest; -import android.util.Slog; - -import java.util.function.Consumer; - -/** - * Helpers for working with executors - */ -public class ExecutorUtils { - - /** - * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given - * callback. - */ - public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, - String log, Consumer<T> callback) { - executeRemoteCallWithTaskPermission(controllerInstance, log, callback, - false /* blocking */); - } - - /** - * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given - * callback. - */ - public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, - String log, Consumer<T> callback, boolean blocking) { - if (controllerInstance == null) return; - - final RemoteCallable<T> controller = controllerInstance; - controllerInstance.getContext().enforceCallingPermission( - Manifest.permission.MANAGE_ACTIVITY_TASKS, log); - if (blocking) { - try { - controllerInstance.getRemoteCallExecutor().executeBlocking(() -> { - callback.accept((T) controller); - }); - } catch (InterruptedException e) { - Slog.e("ExecutorUtils", "Remote call failed", e); - } - } else { - controllerInstance.getRemoteCallExecutor().execute(() -> { - callback.accept((T) controller); - }); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java index aa5b0cb628e1..d6f4d81b44f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -16,7 +16,11 @@ package com.android.wm.shell.common; +import android.Manifest; import android.os.IBinder; +import android.util.Slog; + +import java.util.function.Consumer; /** * An interface for binders which can be registered to be sent to other processes. @@ -31,4 +35,40 @@ public interface ExternalInterfaceBinder { * Returns the IBinder to send. */ IBinder asBinder(); + + /** + * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given + * callback. + */ + default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, + String log, Consumer<T> callback) { + executeRemoteCallWithTaskPermission(controllerInstance, log, callback, + false /* blocking */); + } + + /** + * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given + * callback. + */ + default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, + String log, Consumer<T> callback, boolean blocking) { + if (controllerInstance == null) return; + + final RemoteCallable<T> controller = controllerInstance; + controllerInstance.getContext().enforceCallingPermission( + Manifest.permission.MANAGE_ACTIVITY_TASKS, log); + if (blocking) { + try { + controllerInstance.getRemoteCallExecutor().executeBlocking(() -> { + callback.accept((T) controller); + }); + } catch (InterruptedException e) { + Slog.e("ExternalInterfaceBinder", "Remote call failed", e); + } + } else { + controllerInstance.getRemoteCallExecutor().execute(() -> { + callback.accept((T) controller); + }); + } + } } 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/RemoteCallable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java index 30f535ba940c..0d90fb7e60fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java @@ -19,7 +19,7 @@ package com.android.wm.shell.common; import android.content.Context; /** - * An interface for controllers that can receive remote calls. + * An interface for controllers (of type T) that can receive remote calls. */ public interface RemoteCallable<T> { /** 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..ef33b3830e45 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,11 +349,11 @@ 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, - InsetsSourceControl[] activeControls) {} + InsetsSourceControl.Array activeControls) {} @Override public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {} @@ -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..eec24683db8a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt @@ -0,0 +1,210 @@ +/* + * 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 initialLocationOnLeft = false + private var onLeft = false + private var dismissZone: RectF? = null + private var stuckToDismissTarget = false + 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) { + this.initialLocationOnLeft = initialLocationOnLeft + 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 + + val wasOnLeft = onLeft + onLeft = x < screenCenterX + if (wasOnLeft != onLeft) { + onLocationChange(if (onLeft) LEFT else RIGHT) + } else if (stuckToDismissTarget) { + // Moved out of the dismiss view back to initial side, if we have a drop target, show it + getDropTargetView()?.apply { animateIn() } + } + // Make sure this gets cleared + stuckToDismissTarget = false + } + + /** Signal the controller that view has been dragged to dismiss view. */ + fun onStuckToDismissTarget() { + stuckToDismissTarget = true + // Notify that location may be reset + val shouldResetLocation = onLeft != initialLocationOnLeft + if (shouldResetLocation) { + onLeft = initialLocationOnLeft + listener?.onChange(if (onLeft) LEFT else RIGHT) + } + getDropTargetView()?.apply { + animateOut { + if (shouldResetLocation) { + updateLocation(if (onLeft) LEFT else RIGHT) + } + } + } + } + + /** 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/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java index 24608d651d06..829af08e612a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java @@ -45,10 +45,12 @@ public class BubbleInfo implements Parcelable { private Icon mIcon; @Nullable private String mTitle; + @Nullable + private String mAppName; private boolean mIsImportantConversation; public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon, - int userId, String packageName, @Nullable String title, + int userId, String packageName, @Nullable String title, @Nullable String appName, boolean isImportantConversation) { mKey = key; mFlags = flags; @@ -57,6 +59,7 @@ public class BubbleInfo implements Parcelable { mUserId = userId; mPackageName = packageName; mTitle = title; + mAppName = appName; mIsImportantConversation = isImportantConversation; } @@ -68,6 +71,7 @@ public class BubbleInfo implements Parcelable { mUserId = source.readInt(); mPackageName = source.readString(); mTitle = source.readString(); + mAppName = source.readString(); mIsImportantConversation = source.readBoolean(); } @@ -102,6 +106,11 @@ public class BubbleInfo implements Parcelable { return mTitle; } + @Nullable + public String getAppName() { + return mAppName; + } + public boolean isImportantConversation() { return mIsImportantConversation; } @@ -161,6 +170,7 @@ public class BubbleInfo implements Parcelable { parcel.writeInt(mUserId); parcel.writeString(mPackageName); parcel.writeString(mTitle); + parcel.writeString(mAppName); parcel.writeBoolean(mIsImportantConversation); } 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/desktopmode/DesktopModeTransitionSource.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl new file mode 100644 index 000000000000..c968e809bf61 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.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.desktopmode; + +parcelable DesktopModeTransitionSource;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt new file mode 100644 index 000000000000..dbbf178613b5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt @@ -0,0 +1,54 @@ +/* + * 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.desktopmode + +import android.os.Parcel +import android.os.Parcelable + +/** Transition source types for Desktop Mode. */ +enum class DesktopModeTransitionSource : Parcelable { + /** Transitions that originated as a consequence of task dragging. */ + TASK_DRAG, + /** Transitions that originated from an app from Overview. */ + APP_FROM_OVERVIEW, + /** Transitions that originated from app handle menu button */ + APP_HANDLE_MENU_BUTTON, + /** Transitions that originated as a result of keyboard shortcuts. */ + KEYBOARD_SHORTCUT, + /** Transitions with source unknown. */ + UNKNOWN; + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object { + @JvmField + val CREATOR = + object : Parcelable.Creator<DesktopModeTransitionSource> { + override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource { + return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN + } + + override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size) + } + } +} 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/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl index b5f25433f9aa..e77987963b48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl @@ -53,9 +53,11 @@ interface IPip { * @param destinationBounds the destination bounds the PiP window lands into * @param overlay an optional overlay to fade out after entering PiP * @param appBounds the bounds used to set the buffer size of the optional content overlay + * @param sourceRectHint the bounds to show in the transition to PiP */ oneway void stopSwipePipToHome(int taskId, in ComponentName componentName, - in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2; + in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds, + in Rect sourceRectHint) = 2; /** * Notifies the swiping Activity to PiP onto home transition is aborted diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt index 18c7bdd6d5ba..7eb0f267b312 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.common.pip import android.content.Context import android.content.res.Resources -import android.os.SystemProperties import android.util.Size import com.android.wm.shell.R import java.io.PrintWriter @@ -36,30 +35,81 @@ class PhoneSizeSpecSource( private var mOverrideMinSize: Size? = null - /** Default and minimum percentages for the PIP size logic. */ - private val mDefaultSizePercent: Float - private val mMinimumSizePercent: Float + /** + * Default percentages for the PIP size logic. + * 1. Determine max widths + * Subtract width of system UI and default padding from the shortest edge of the device. + * This is the max width. + * 2. Calculate Default and Mins + * Default is mSystemPreferredDefaultSizePercent of max-width/height. + * Min is mSystemPreferredMinimumSizePercent of it. + * + * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead. + */ + private var mSystemPreferredDefaultSizePercent = 0.6f + /** Minimum percentages for the PIP size logic. */ + private var mSystemPreferredMinimumSizePercent = 0.5f + + /** Threshold to categorize the Display as square, calculated as min(w, h) / max(w, h). */ + private var mSquareDisplayThresholdForSystemPreferredSize = 0.95f + /** + * Default percentages for the PIP size logic when the Display is square-ish. + * This is used instead when the display is square-ish, like fold-ables when unfolded, + * to make sure that default PiP does not cover the hinge (halfway of the display). + * 1. Determine max widths + * Subtract width of system UI and default padding from the shortest edge of the device. + * This is the max width. + * 2. Calculate Default and Mins + * Default is mSystemPreferredDefaultSizePercent of max-width/height. + * Min is mSystemPreferredMinimumSizePercent of it. + * + * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead. + */ + private var mSystemPreferredDefaultSizePercentForSquareDisplay = 0.5f + /** Minimum percentages for the PIP size logic. */ + private var mSystemPreferredMinimumSizePercentForSquareDisplay = 0.4f + + private val mIsSquareDisplay + get() = minOf(pipDisplayLayoutState.displayLayout.width(), + pipDisplayLayoutState.displayLayout.height()).toFloat() / + maxOf(pipDisplayLayoutState.displayLayout.width(), + pipDisplayLayoutState.displayLayout.height()) > + mSquareDisplayThresholdForSystemPreferredSize + private val mPreferredDefaultSizePercent + get() = if (mIsSquareDisplay) mSystemPreferredDefaultSizePercentForSquareDisplay else + mSystemPreferredDefaultSizePercent + + private val mPreferredMinimumSizePercent + get() = if (mIsSquareDisplay) mSystemPreferredMinimumSizePercentForSquareDisplay else + mSystemPreferredMinimumSizePercent /** Aspect ratio that the PIP size spec logic optimizes for. */ private var mOptimizedAspectRatio = 0f init { - mDefaultSizePercent = SystemProperties - .get("com.android.wm.shell.pip.phone.def_percentage", "0.6").toFloat() - mMinimumSizePercent = SystemProperties - .get("com.android.wm.shell.pip.phone.min_percentage", "0.5").toFloat() - reloadResources() } private fun reloadResources() { - val res: Resources = context.getResources() + val res: Resources = context.resources mDefaultMinSize = res.getDimensionPixelSize( R.dimen.default_minimal_size_pip_resizable_task) mOverridableMinSize = res.getDimensionPixelSize( R.dimen.overridable_minimal_size_pip_resizable_task) + mSystemPreferredDefaultSizePercent = res.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercent) + mSystemPreferredMinimumSizePercent = res.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercent) + + mSquareDisplayThresholdForSystemPreferredSize = res.getFloat( + R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize) + mSystemPreferredDefaultSizePercentForSquareDisplay = res.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay) + mSystemPreferredMinimumSizePercentForSquareDisplay = res.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay) + val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio) // make sure the optimized aspect ratio is valid with a default value to fall back to mOptimizedAspectRatio = if (requestedOptAspRatio > 1) { @@ -128,7 +178,7 @@ class PhoneSizeSpecSource( return minSize } val maxSize = getMaxSize(aspectRatio) - val defaultWidth = Math.max(Math.round(maxSize.width * mDefaultSizePercent), + val defaultWidth = Math.max(Math.round(maxSize.width * mPreferredDefaultSizePercent), minSize.width) val defaultHeight = Math.round(defaultWidth / aspectRatio) return Size(defaultWidth, defaultHeight) @@ -146,8 +196,8 @@ class PhoneSizeSpecSource( return adjustOverrideMinSizeToAspectRatio(aspectRatio)!! } val maxSize = getMaxSize(aspectRatio) - var minWidth = Math.round(maxSize.width * mMinimumSizePercent) - var minHeight = Math.round(maxSize.height * mMinimumSizePercent) + var minWidth = Math.round(maxSize.width * mPreferredMinimumSizePercent) + var minHeight = Math.round(maxSize.height * mPreferredMinimumSizePercent) // make sure the calculated min size is not smaller than the allowed default min size if (aspectRatio > 1f) { @@ -244,8 +294,8 @@ class PhoneSizeSpecSource( pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize) pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize) pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize) - pw.println(innerPrefix + "mDefaultSizePercent=" + mDefaultSizePercent) - pw.println(innerPrefix + "mMinimumSizePercent=" + mMinimumSizePercent) + pw.println(innerPrefix + "mDefaultSizePercent=" + mPreferredDefaultSizePercent) + pw.println(innerPrefix + "mMinimumSizePercent=" + mPreferredMinimumSizePercent) pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio) } }
\ No newline at end of file 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/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java index ed42117a55af..d5e47187dac2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java @@ -115,6 +115,12 @@ public class PipDisplayLayoutState { mDisplayLayout.rotateTo(mContext.getResources(), targetRotation); } + /** Returns the current display rotation of this layout state. */ + @Surface.Rotation + public int getRotation() { + return mDisplayLayout.rotation(); + } + /** Get the current display id */ public int getDisplayId() { return mDisplayId; 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..a09720dd6a70 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,15 @@ 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.graphics.Rect import android.os.RemoteException +import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Pair @@ -135,7 +139,48 @@ object PipUtils { } } + + /** + * Returns a fake source rect hint for animation purposes when app-provided one is invalid. + * Resulting adjusted source rect hint lets the app icon in the content overlay to stay visible. + */ + @JvmStatic + fun getEnterPipWithOverlaySrcRectHint(appBounds: Rect, aspectRatio: Float): Rect { + val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height() + val width: Int + val height: Int + var left = 0 + var top = 0 + if (appBoundsAspRatio < aspectRatio) { + width = appBounds.width() + height = Math.round(width / aspectRatio) + top = (appBounds.height() - height) / 2 + } else { + height = appBounds.height() + width = Math.round(height * aspectRatio) + left = (appBounds.width() - width) / 2 + } + return Rect(left, top, left + width, top + height) + } + + 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/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index f9a286ec804f..bc6ed1f63c8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -84,6 +84,8 @@ public class DividerSnapAlgorithm { private final int mMinimalSizeResizableTask; private final int mTaskHeightInMinimizedMode; private final float mFixedRatio; + /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */ + private final boolean mAllowFlexibleSplitRatios; private boolean mIsHorizontalDivision; /** The first target which is still splitting the screen */ @@ -144,6 +146,8 @@ public class DividerSnapAlgorithm { com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1); mMinimalSizeResizableTask = res.getDimensionPixelSize( com.android.internal.R.dimen.default_minimal_size_resizable_task); + mAllowFlexibleSplitRatios = res.getBoolean( + com.android.internal.R.bool.config_flexibleSplitRatios); mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize( com.android.internal.R.dimen.task_height_of_minimized_mode) : 0; calculateTargets(isHorizontalDivision, dockSide); @@ -349,6 +353,9 @@ public class DividerSnapAlgorithm { ? mDisplayHeight - mInsets.bottom : mDisplayWidth - mInsets.right; int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; + if (mAllowFlexibleSplitRatios) { + size = Math.max(size, mMinimalSizeResizableTask); + } int topPosition = start + size; int bottomPosition = end - size - mDividerSize; addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax); 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 a87116ea4670..c2242a8b87fa 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); @@ -336,6 +336,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { setTouching(); mStartPos = touchPos; mMoving = false; + // This triggers initialization of things like the resize veil in preparation for + // showing it when the user moves the divider past the slop, and has to be done + // before onStartDragging() which starts the jank interaction tracing + mSplitLayout.updateDividerBounds(mSplitLayout.getDividerPosition(), + false /* shouldUseParallaxEffect */); mSplitLayout.onStartDragging(); break; case MotionEvent.ACTION_MOVE: @@ -345,9 +350,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); + mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */); } break; case MotionEvent.ACTION_UP: @@ -363,7 +368,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 +477,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 dae62ac74483..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; @@ -30,7 +31,6 @@ import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -56,7 +56,13 @@ import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Consumer; /** - * Handles split decor like showing resizing hint for a specific split. + * Handles additional layers over a running task in a split pair, for example showing a veil with an + * app icon when the task is being resized (usually to hide weird layouts while the app is being + * stretched). One SplitDecorManager is initialized on each window. + * <br> + * Currently, we show a veil when: + * a) Task is resizing down from a fullscreen window. + * b) Task is being stretched past its original bounds. */ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); @@ -77,7 +83,11 @@ public class SplitDecorManager extends WindowlessWindowManager { private boolean mShown; private boolean mIsResizing; - private final Rect mOldBounds = new Rect(); + /** The original bounds of the main task, captured at the beginning of a resize transition. */ + private final Rect mOldMainBounds = new Rect(); + /** The original bounds of the side task, captured at the beginning of a resize transition. */ + private final Rect mOldSideBounds = new Rect(); + /** The current bounds of the main task, mid-resize. */ private final Rect mResizingBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; @@ -109,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; } @@ -128,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); } @@ -184,29 +193,38 @@ public class SplitDecorManager extends WindowlessWindowManager { mResizingIconView = null; mIsResizing = false; mShown = false; - mOldBounds.setEmpty(); + mOldMainBounds.setEmpty(); + mOldSideBounds.setEmpty(); mResizingBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, - boolean immediately) { + boolean immediately, float[] veilColor) { if (mResizingIconView == null) { return; } if (!mIsResizing) { mIsResizing = true; - mOldBounds.set(newBounds); + mOldMainBounds.set(newBounds); + mOldSideBounds.set(sideBounds); } mResizingBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; - final boolean show = - newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height(); - final boolean update = show != mShown; + // Show a veil when: + // a) Task is resizing down from a fullscreen window. + // b) Task is being stretched past its original bounds. + final boolean isResizingDownFromFullscreen = + mOldSideBounds.width() <= 1 || mOldSideBounds.height() <= 1; + final boolean isStretchingPastOriginalBounds = + newBounds.width() > mOldMainBounds.width() + || newBounds.height() > mOldMainBounds.height(); + final boolean showVeil = isResizingDownFromFullscreen || isStretchingPastOriginalBounds; + final boolean update = showVeil != mShown; if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) { // If we need to animate and animator still running, cancel it before we ensure both // background and icon surfaces are non null for next animation. @@ -216,18 +234,18 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); - t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + t.setColor(mBackgroundLeash, veilColor) .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } if (mGapBackgroundLeash == null && !immediately) { final boolean isLandscape = newBounds.height() == sideBounds.height(); - final int left = isLandscape ? mOldBounds.width() : 0; - final int top = isLandscape ? 0 : mOldBounds.height(); + final int left = isLandscape ? mOldMainBounds.width() : 0; + final int top = isLandscape ? 0 : mOldMainBounds.height(); mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); // Fill up another side bounds area. - t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) + t.setColor(mGapBackgroundLeash, veilColor) .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) .setPosition(mGapBackgroundLeash, left, top) .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); @@ -251,12 +269,12 @@ public class SplitDecorManager extends WindowlessWindowManager { if (update) { if (immediately) { - t.setVisibility(mBackgroundLeash, show); - t.setVisibility(mIconLeash, show); + t.setVisibility(mBackgroundLeash, showVeil); + t.setVisibility(mIconLeash, showVeil); } else { - startFadeAnimation(show, false, null); + startFadeAnimation(showVeil, false, null); } - mShown = show; + mShown = showVeil; } } @@ -309,7 +327,8 @@ public class SplitDecorManager extends WindowlessWindowManager { mIsResizing = false; mOffsetX = 0; mOffsetY = 0; - mOldBounds.setEmpty(); + mOldMainBounds.setEmpty(); + mOldSideBounds.setEmpty(); mResizingBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { @@ -346,14 +365,14 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { t.remove(mScreenshot); } - mTempRect.set(mOldBounds); + mTempRect.set(mOldMainBounds); mTempRect.offsetTo(0, 0); mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, Integer.MAX_VALUE - 1); @@ -364,7 +383,7 @@ public class SplitDecorManager extends WindowlessWindowManager { public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { if (screenshot == null || !screenshot.isValid()) return; - if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -465,9 +484,4 @@ public class SplitDecorManager extends WindowlessWindowManager { mIcon = null; } } - - private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { - final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); - return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); - } } 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 6b2d544c192a..8ced76fd23af 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) { + void updateDividerBounds(int position, boolean shouldUseParallaxEffect) { updateBounds(position); mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, - mSurfaceEffectPolicy.mParallaxOffset.y); + 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,24 @@ 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() { + public void flingDividerToCenter(@Nullable Runnable finishCallback) { 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 */); + if (finishCallback != null) { + finishCallback.run(); + } + }); } @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 +652,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .setDuration(duration); mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mDividerFlingAnimator.addUpdateListener( - animation -> updateDivideBounds((int) animation.getAnimatedValue())); + animation -> updateDividerBounds( + (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */) + ); mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -897,7 +904,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, * SurfaceControl, SurfaceControl, boolean) */ - void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY); + void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, + boolean shouldUseParallaxEffect); /** * Calls when finish resizing the split bounds. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index f9259e79472e..e8226051b672 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,8 +16,6 @@ package com.android.wm.shell.common.split; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED; - import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -26,25 +24,18 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import android.app.ActivityManager; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Intent; -import android.content.pm.LauncherApps; -import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Rect; -import android.os.UserHandle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.util.ArrayUtils; import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; -import java.util.Arrays; -import java.util.List; - /** Helper utility class for split screen components to use. */ public class SplitScreenUtils { /** Reverse the split position. */ @@ -137,4 +128,10 @@ public class SplitScreenUtils { return isLandscape; } } + + /** Returns the specified background color that matches a RunningTaskInfo. */ + public static Color getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 8fb9bda539a0..5d121c23c6e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -143,6 +143,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { /** * Releases the surface control of the current {@link DividerView} and tear down the view * hierarchy. + * @param t If supplied, the surface removal will be bundled with this Transaction. If + * called with null, removes the surface immediately. */ void release(@Nullable SurfaceControl.Transaction t) { if (mDividerView != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt new file mode 100644 index 000000000000..6781d08c9904 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("AppCompatUtils") + +package com.android.wm.shell.compatui + +import android.app.TaskInfo +fun isSingleTopActivityTranslucent(task: TaskInfo) = + task.isTopActivityTransparent && task.numActivities == 1 + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java index fa2e23647a39..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; @@ -194,6 +194,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi return mHideSizeCompatRestartButtonTolerance; } + int getDefaultHideRestartButtonTolerance() { + return MAX_PERCENTAGE_VAL; + } + boolean getHasSeenLetterboxEducation(int userId) { return mLetterboxEduSharedPreferences .getBoolean(dontShowLetterboxEduKey(userId), /* default= */ false); 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 86571cf9c622..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; @@ -188,6 +188,11 @@ public class CompatUIController implements OnDisplaysChangedListener, */ private boolean mHasShownUserAspectRatioSettingsButton = false; + /** + * This is true when the rechability education is displayed for the first time. + */ + private boolean mIsFirstReachabilityEducationRunning; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -252,9 +257,35 @@ public class CompatUIController implements OnDisplaysChangedListener, removeLayouts(taskInfo.taskId); return; } - + // We're showing the first reachability education so we ignore incoming TaskInfo + // until the education flow has completed or we double tap. + if (mIsFirstReachabilityEducationRunning) { + return; + } + if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { + if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) { + createOrUpdateLetterboxEduLayout(taskInfo, taskListener); + } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + // In this case the app is letterboxed and the letterbox education + // is disabled. In this case we need to understand if it's the first + // time we show the reachability education. When this is happening + // we need to ignore all the incoming TaskInfo until the education + // completes. If we come from a double tap we follow the normal flow. + final boolean topActivityPillarboxed = + taskInfo.appCompatTaskInfo.isTopActivityPillarboxed(); + final boolean isFirstTimeHorizontalReachabilityEdu = topActivityPillarboxed + && !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(taskInfo); + final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed + && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo); + if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) { + mIsFirstReachabilityEducationRunning = true; + mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId); + createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + return; + } + } + } createOrUpdateCompatLayout(taskInfo, taskListener); - createOrUpdateLetterboxEduLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); @@ -589,6 +620,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { // We need to update the UI otherwise it will not be shown until the user relaunches the app + mIsFirstReachabilityEducationRunning = false; createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); } 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 dbf7186def8a..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; -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; @@ -217,14 +227,30 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @VisibleForTesting boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) { - if (!Flags.allowHideScmButton()) { + // Always show button if display is phone sized. + if (!Flags.allowHideScmButton() || taskInfo.configuration.smallestScreenWidthDp + < LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP) { + return true; + } + + final int letterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth; + final int letterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight; + final Rect stableBounds = getTaskStableBounds(); + final int appWidth = stableBounds.width(); + final int appHeight = stableBounds.height(); + // App is floating, should always show restart button. + if (appWidth > letterboxWidth && appHeight > letterboxHeight) { return true; } - final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); - final int letterboxArea = computeArea(appCompatTaskInfo.topActivityLetterboxWidth, - appCompatTaskInfo.topActivityLetterboxHeight); - final int taskArea = computeArea(taskBounds.width(), taskBounds.height()); + // If app fills the width of the display, don't show restart button (for landscape apps) + // if device has a custom tolerance value. + if (mHideScmTolerance != mCompatUIConfiguration.getDefaultHideRestartButtonTolerance() + && appWidth == letterboxWidth) { + return false; + } + + final int letterboxArea = letterboxWidth * letterboxHeight; + final int taskArea = appWidth * appHeight; if (letterboxArea == 0 || taskArea == 0) { return false; } @@ -232,13 +258,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { return percentageAreaOfLetterboxInTask < mHideScmTolerance; } - private int computeArea(int width, int height) { - if (width == 0 || height == 0) { - return 0; - } - return width * height; - } - private void updateVisibilityOfViews() { if (mLayout == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 835f1af85c51..07082a558744 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -53,7 +53,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { private final ShellExecutor mMainExecutor; - private boolean mIsActivityLetterboxed; + private boolean mIsLetterboxDoubleTapEnabled; private int mLetterboxVerticalPosition; @@ -91,7 +91,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { Function<Integer, Integer> disappearTimeSupplier) { super(context, taskInfo, syncQueue, taskListener, displayLayout); final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -119,7 +119,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { - return mIsActivityLetterboxed + return mIsLetterboxDoubleTapEnabled && (mLetterboxVerticalPosition != -1 || mLetterboxHorizontalPosition != -1); } @@ -142,13 +142,13 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { - final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed; + final boolean prevIsLetterboxDoubleTapEnabled = mIsLetterboxDoubleTapEnabled; final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition; final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition; final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -162,7 +162,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { mHasLetterboxSizeChanged = prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight; - if (mHasUserDoubleTapped || prevIsActivityLetterboxed != mIsActivityLetterboxed + if (mHasUserDoubleTapped || prevIsLetterboxDoubleTapEnabled != mIsLetterboxDoubleTapEnabled || prevLetterboxVerticalPosition != mLetterboxVerticalPosition || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition || prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth @@ -249,7 +249,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { && (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); - if (mIsActivityLetterboxed && (eligibleForDisplayHorizontalEducation + if (mIsLetterboxDoubleTapEnabled && (eligibleForDisplayHorizontalEducation || eligibleForDisplayVerticalEducation)) { int availableWidth = getTaskBounds().width() - mTopActivityLetterboxWidth; int availableHeight = getTaskBounds().height() - mTopActivityLetterboxHeight; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java index 7c280994042b..8fb4bdbea933 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -237,7 +237,8 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract final int letterboxWidth = taskInfo.topActivityLetterboxWidth; // App is not visibly letterboxed if it covers status bar/bottom insets or matches the // stable bounds, so don't show the button - if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth) { + if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth + && !taskInfo.isUserFullscreenOverrideEnabled) { 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..609e5af5c5b0 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,13 @@ 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.recents.TaskStackTransitionObserver; +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 +108,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 +329,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()); } // @@ -616,12 +620,13 @@ public abstract class WMShellBaseModule { TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( RecentTasksController.create(context, shellInit, shellController, shellCommandHandler, taskStackListener, activityTaskManager, - desktopModeTaskRepository, mainExecutor)); + desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor)); } @BindsOptionalOf @@ -673,6 +678,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 +704,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 +869,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 +893,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 +912,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(); @@ -901,6 +926,19 @@ public abstract class WMShellBaseModule { } // + // Task Stack + // + + @WMSingleton + @Provides + static TaskStackTransitionObserver provideTaskStackTransitionObserver( + Lazy<Transitions> transitions, + ShellInit shellInit + ) { + return new TaskStackTransitionObserver(transitions, shellInit); + } + + // // Misc // @@ -930,6 +968,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..87bd84017dee 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; @@ -115,9 +121,9 @@ import java.util.Optional; */ @Module( includes = { - WMShellBaseModule.class, - PipModule.class, - ShellBackAnimationModule.class, + WMShellBaseModule.class, + PipModule.class, + ShellBackAnimationModule.class, }) public abstract class WMShellModule { @@ -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,47 @@ 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, + Optional<RecentTasksController> recentTasksController) { 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, recentTasksController.orElse(null)); } @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 +565,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( - Transitions transitions) { + Transitions transitions, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new EnterDesktopTaskTransitionHandler(transitions); } @@ -562,6 +593,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 +664,8 @@ 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..677fd5deffd3 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,10 +55,12 @@ 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; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Module; @@ -192,11 +193,12 @@ public abstract class Pip1Module { PipBoundsState pipBoundsState, PipDisplayLayoutState pipDisplayLayoutState, PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + HomeTransitionObserver homeTransitionObserver, Optional<SplitScreenController> splitScreenOptional) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, - splitScreenOptional); + homeTransitionObserver, splitScreenOptional); } @WMSingleton 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..696831747865 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,30 @@ 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.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -62,15 +71,19 @@ 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 @Provides static Optional<PipController> providePipController(Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, @@ -78,14 +91,18 @@ 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(); } else { return Optional.ofNullable(PipController.create( - context, shellInit, shellController, displayController, displayInsetsController, - pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, - mainExecutor)); + context, shellInit, shellCommandHandler, shellController, displayController, + displayInsetsController, pipBoundsState, pipBoundsAlgorithm, + pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, + pipTransitionState, mainExecutor)); } } @@ -93,8 +110,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 +126,48 @@ public abstract class Pip2Module { return new PhonePipMenuController(context, pipBoundsState, pipMediaController, systemWindows, pipUiEventLogger, mainExecutor, mainHandler); } + + + @WMSingleton + @Provides + static PipTouchHandler providePipTouchHandler(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + 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, shellCommandHandler, 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 providePipTransitionState(@ShellMainThread Handler handler) { + return new PipTransitionState(handler); + } } 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..31c8f1e45007 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,8 @@ package com.android.wm.shell.desktopmode; import android.graphics.Region; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -49,8 +50,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, DesktopModeTransitionSource transitionSource); /** Called when requested to go to fullscreen from the current focused desktop app. */ - void moveFocusedTaskToFullscreen(int displayId); + void moveFocusedTaskToFullscreen(int displayId, DesktopModeTransitionSource transitionSource); + + /** 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/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 95d47146e834..9192e6ed3175 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -20,9 +20,7 @@ import com.android.internal.util.FrameworkStatsLog import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.util.KtProtoLog -/** - * Event logger for logging desktop mode session events - */ +/** Event logger for logging desktop mode session events */ class DesktopModeEventLogger { /** * Logs the enter of desktop mode having session id [sessionId] and the reason [enterReason] for @@ -32,13 +30,16 @@ class DesktopModeEventLogger { KtProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging session enter, session: %s reason: %s", - sessionId, enterReason.name + sessionId, + enterReason.name ) - FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID, + FrameworkStatsLog.write( + DESKTOP_MODE_ATOM_ID, /* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, /* enterReason */ enterReason.reason, /* exitReason */ 0, - /* session_id */ sessionId) + /* session_id */ sessionId + ) } /** @@ -49,13 +50,16 @@ class DesktopModeEventLogger { KtProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging session exit, session: %s reason: %s", - sessionId, exitReason.name + sessionId, + exitReason.name ) - FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID, + FrameworkStatsLog.write( + DESKTOP_MODE_ATOM_ID, /* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT, /* enterReason */ 0, /* exitReason */ exitReason.reason, - /* session_id */ sessionId) + /* session_id */ sessionId + ) } /** @@ -66,9 +70,11 @@ class DesktopModeEventLogger { KtProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task added, session: %s taskId: %s", - sessionId, taskUpdate.instanceId + sessionId, + taskUpdate.instanceId ) - FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID, + FrameworkStatsLog.write( + DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, /* instance_id */ @@ -84,7 +90,8 @@ class DesktopModeEventLogger { /* task_y */ taskUpdate.taskY, /* session_id */ - sessionId) + sessionId + ) } /** @@ -95,9 +102,11 @@ class DesktopModeEventLogger { KtProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task remove, session: %s taskId: %s", - sessionId, taskUpdate.instanceId + sessionId, + taskUpdate.instanceId ) - FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID, + FrameworkStatsLog.write( + DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, /* instance_id */ @@ -113,7 +122,8 @@ class DesktopModeEventLogger { /* task_y */ taskUpdate.taskY, /* session_id */ - sessionId) + sessionId + ) } /** @@ -124,9 +134,11 @@ class DesktopModeEventLogger { KtProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task info changed, session: %s taskId: %s", - sessionId, taskUpdate.instanceId + sessionId, + taskUpdate.instanceId ) - FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID, + FrameworkStatsLog.write( + DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, /* instance_id */ @@ -142,7 +154,8 @@ class DesktopModeEventLogger { /* task_y */ taskUpdate.taskY, /* session_id */ - sessionId) + sessionId + ) } companion object { @@ -160,12 +173,8 @@ class DesktopModeEventLogger { * stats/atoms/desktopmode/desktopmode_extensions_atoms.proto */ enum class EnterReason(val reason: Int) { - UNKNOWN_ENTER( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER - ), - OVERVIEW( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW - ), + UNKNOWN_ENTER(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER), + OVERVIEW(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW), APP_HANDLE_DRAG( FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_HANDLE_DRAG ), @@ -178,9 +187,10 @@ class DesktopModeEventLogger { KEYBOARD_SHORTCUT_ENTER( FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER ), - SCREEN_ON( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON - ); + SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON), + APP_FROM_OVERVIEW( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_FROM_OVERVIEW + ), } /** @@ -188,12 +198,8 @@ class DesktopModeEventLogger { * stats/atoms/desktopmode/desktopmode_extensions_atoms.proto */ enum class ExitReason(val reason: Int) { - UNKNOWN_EXIT( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT - ), - DRAG_TO_EXIT( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT - ), + UNKNOWN_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT), + DRAG_TO_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT), APP_HANDLE_MENU_BUTTON_EXIT( FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__APP_HANDLE_MENU_BUTTON_EXIT ), @@ -201,18 +207,14 @@ class DesktopModeEventLogger { FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__KEYBOARD_SHORTCUT_EXIT ), RETURN_HOME_OR_OVERVIEW( - FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME - ), - TASK_FINISHED( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW ), - SCREEN_OFF( - FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF - ) + TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED), + SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF) } private const val DESKTOP_MODE_ATOM_ID = FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED private const val DESKTOP_MODE_TASK_UPDATE_ATOM_ID = FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE } -}
\ No newline at end of file +} 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..641952b28bfb --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -0,0 +1,394 @@ +/* + * 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.internal.protolog.common.ProtoLog +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.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +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() + + // Caching whether the previous transition was exit to overview. + private var wasPreviousTransitionExitToOverview: Boolean = false + + // 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.isExitToRecentsTransition() && 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 + ) + wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition() + } + + 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 = + when { + transitionInfo.type == WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON + transitionInfo.type == Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> + EnterReason.APP_HANDLE_DRAG + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> + EnterReason.APP_HANDLE_MENU_BUTTON + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW -> + EnterReason.APP_FROM_OVERVIEW + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT -> + EnterReason.KEYBOARD_SHORTCUT_ENTER + // NOTE: the below condition also applies for EnterReason quickswitch + transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows. + // TODO(b/346564416): Modify logging for cancelled recents once it transition is + // changed. Also see how to account to time difference between actual enter time and + // time of this log. Also account for the missed session when exit happens just after + // a cancelled recents. + wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW + transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT + else -> { + ProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "Unknown enter reason for transition type ${transitionInfo.type}", + transitionInfo.type + ) + EnterReason.UNKNOWN_ENTER + } + } + + /** Get [ExitReason] for this session exit */ + private fun getExitReason(transitionInfo: TransitionInfo): ExitReason = + when { + transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF + transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> + ExitReason.APP_HANDLE_MENU_BUTTON_EXIT + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> + ExitReason.KEYBOARD_SHORTCUT_EXIT + transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW + else -> { + ProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "Unknown exit reason for transition type ${transitionInfo.type}", + transitionInfo.type + ) + 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.isExitToRecentsTransition(): Boolean { + return this.type == WindowManager.TRANSIT_TO_FRONT && + this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS + } +} 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..1a6ca0efa748 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 @@ -16,13 +16,11 @@ package com.android.wm.shell.desktopmode -import android.window.WindowContainerTransaction +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter -/** - * Handles the shell commands for the DesktopTasksController. - */ +/** Handles the shell commands for the DesktopTasksController. */ class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : ShellCommandHandler.ShellCommandActionHandler { @@ -36,7 +34,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 @@ -51,18 +56,40 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return false } - val taskId = try { - args[1].toInt() - } catch (e: NumberFormatException) { - pw.println("Error: task id should be an integer") + val taskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + return controller.moveToDesktop(taskId, transitionSource = UNKNOWN) + } + + 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 } - return controller.moveToDesktop(taskId, WindowContainerTransaction()) + val taskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + controller.moveToNextDisplay(taskId) + return true } override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { pw.println("$prefix moveToDesktop <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") + pw.println("$prefix moveToNextDisplay <taskId> ") + pw.println("$prefix Move a task with given id to next display.") } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java deleted file mode 100644 index 22ba70860587..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ /dev/null @@ -1,107 +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.desktopmode; - -import android.os.SystemProperties; - -import com.android.window.flags.Flags; - -/** - * Constants for desktop mode feature - */ -public class DesktopModeStatus { - - /** - * Flag to indicate whether task resizing is veiled. - */ - private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean( - "persist.wm.debug.desktop_veiled_resizing", true); - - /** - * Flag to indicate is moving task to another display is enabled. - */ - 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. - */ - private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean( - "persist.wm.debug.desktop_use_window_shadows", true); - - /** - * Flag to indicate whether to apply shadows to the focused window in desktop mode. - * - * Note: this flag is only relevant if USE_WINDOW_SHADOWS is false. - */ - private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean( - "persist.wm.debug.desktop_use_window_shadows_focused_window", false); - - /** - * Flag to indicate whether to apply shadows to 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 - */ - public static boolean isEnabled() { - return Flags.enableDesktopWindowingMode(); - } - - /** - * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. - */ - public static boolean isVeiledResizeEnabled() { - return IS_VEILED_RESIZE_ENABLED; - } - - /** - * 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 - */ - public static boolean useWindowShadow(boolean isFocusedWindow) { - return USE_WINDOW_SHADOWS - || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow); - } - - /** - * Return whether to use rounded corners for windows. - */ - public static boolean useRoundedCorners() { - return USE_ROUNDED_CORNERS; - } -} 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..7d01580ecb6e 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 @@ -29,9 +32,7 @@ import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer -/** - * Keeps track of task data related to desktop mode. - */ +/** Keeps track of task data related to desktop mode. */ class DesktopModeTaskRepository { /** Task data that is tracked per display */ @@ -44,16 +45,20 @@ class DesktopModeTaskRepository { */ val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), - var stashed: Boolean = false + val minimizedTasks: ArraySet<Int> = ArraySet(), + // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). + val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) - // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). - private val freeformTasksInZOrder = mutableListOf<Int>() + // Token of the current wallpaper activity, used to remove it when the last task is removed + var wallpaperActivityToken: WindowContainerToken? = null private val activeTasksListeners = ArraySet<ActiveTasksListener>() // Track visible tasks separately because a task may be part of the desktop but not visible. 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 @@ -77,20 +82,13 @@ class DesktopModeTaskRepository { activeTasksListeners.add(activeTasksListener) } - /** - * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. - */ - fun addVisibleTasksListener( - visibleTasksListener: VisibleTasksListener, - executor: Executor - ) { + /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ + fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { 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) } } } @@ -107,9 +105,7 @@ class DesktopModeTaskRepository { } } - /** - * Create a new merged region representative of all exclusion regions in all desktop tasks. - */ + /** Create a new merged region representative of all exclusion regions in all desktop tasks. */ private fun calculateDesktopExclusionRegion(): Region { val desktopExclusionRegion = Region() desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion -> @@ -118,16 +114,12 @@ class DesktopModeTaskRepository { return desktopExclusionRegion } - /** - * Remove a previously registered [ActiveTasksListener] - */ + /** Remove a previously registered [ActiveTasksListener] */ fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.remove(activeTasksListener) } - /** - * Remove a previously registered [VisibleTasksListener] - */ + /** Remove a previously registered [VisibleTasksListener] */ fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { visibleTasksListeners.remove(visibleTasksListener) } @@ -177,39 +169,62 @@ class DesktopModeTaskRepository { return result } - /** - * Check if a task with the given [taskId] was marked as an active task - */ + /** Check if a task with the given [taskId] was marked as an active task */ fun isActiveTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.activeTasks.contains(taskId) } } - /** - * Whether a task is visible. - */ + /** Whether a task is visible. */ fun isVisibleTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.visibleTasks.contains(taskId) } } - /** - * Get a set of the active tasks for given [displayId] - */ + /** 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] */ fun getActiveTasks(displayId: Int): ArraySet<Int> { return ArraySet(displayData[displayId]?.activeTasks) } /** - * Get a list of freeform tasks, ordered from top-bottom (top at index 0). + * 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. */ - // TODO(b/278084491): pass in display id - fun getFreeformTasksInZOrder(): List<Int> { - return freeformTasksInZOrder + fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> { + val activeTasks = getActiveTasks(displayId) + val allTasksInZOrder = getFreeformTasksInZOrder(displayId) + 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). */ + fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> = + ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList()) + /** * Updates whether a freeform task with this id is visible or not and notifies listeners. * @@ -222,20 +237,32 @@ class DesktopModeTaskRepository { val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId } for (otherDisplayId in otherDisplays) { if (displayData[otherDisplayId].visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners(otherDisplayId, - displayData[otherDisplayId].visibleTasks.size) + notifyVisibleTaskListeners( + otherDisplayId, + 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 +271,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", @@ -264,9 +287,7 @@ class DesktopModeTaskRepository { } } - /** - * Get number of tasks that are marked as visible on given [displayId] - */ + /** Get number of tasks that are marked as visible on given [displayId] */ fun getVisibleTaskCount(displayId: Int): Int { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, @@ -276,41 +297,62 @@ class DesktopModeTaskRepository { return displayData[displayId]?.visibleTasks?.size ?: 0 } - /** - * Add (or move if it already exists) the task to the top of the ordered list. - */ - fun addOrMoveFreeformTaskToTop(taskId: Int) { + /** Add (or move if it already exists) the task to the top of the ordered list. */ + // TODO(b/342417921): Identify if there is additional checks needed to move tasks for + // multi-display scenarios. + fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: add or move task to top taskId=%d", + "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d", + displayId, taskId ) - if (freeformTasksInZOrder.contains(taskId)) { - freeformTasksInZOrder.remove(taskId) - } - freeformTasksInZOrder.add(0, taskId) + displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) + displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) } - /** - * Remove the task from the ordered list. - */ - fun removeFreeformTask(taskId: Int) { + /** 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. */ + fun removeFreeformTask(displayId: Int, taskId: Int) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remove freeform task from ordered list taskId=%d", + "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d", + displayId, taskId ) - freeformTasksInZOrder.remove(taskId) + displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) + boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() + "DesktopTaskRepo: remaining freeform tasks: %s", + displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "" ) } /** * Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been - * accepted by desktopGestureExclusionListener, it will be updated in the - * appropriate classes. + * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes. */ fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) { desktopExclusionRegions.put(taskId, taskExclusionRegions) @@ -320,9 +362,9 @@ class DesktopModeTaskRepository { } /** - * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion - * has been accepted by desktopGestureExclusionListener, it will be updated in the - * appropriate classes. + * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has + * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate + * classes. */ fun removeExclusionRegion(taskId: Int) { desktopExclusionRegions.delete(taskId) @@ -331,38 +373,20 @@ class DesktopModeTaskRepository { } } - /** - * Update stashed status on display with id [displayId] - */ - 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) } - } - } + /** Removes and returns the bounds saved before maximizing the given task. */ + fun removeBoundsBeforeMaximize(taskId: Int): Rect? { + return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) } - /** - * Check if display with id [displayId] has desktop tasks stashed - */ - fun isStashed(displayId: Int): Boolean { - return displayData[displayId]?.stashed ?: false + /** Saves the bounds of the given task before maximizing. */ + fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) { + boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) } internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") dumpDisplayData(pw, innerPrefix) - pw.println("${innerPrefix}freeformTasksInZOrder=${freeformTasksInZOrder.toDumpString()}") pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}") pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}") } @@ -373,7 +397,9 @@ 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}") + pw.println( + "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}" + ) } } @@ -381,9 +407,7 @@ class DesktopModeTaskRepository { * Defines interface for classes that can listen to changes for active tasks in desktop mode. */ interface ActiveTasksListener { - /** - * Called when the active tasks change in desktop mode. - */ + /** Called when the active tasks change in desktop mode. */ fun onActiveTasksChanged(displayId: Int) {} } @@ -391,15 +415,8 @@ class DesktopModeTaskRepository { * Defines interface for classes that can listen to changes for visible tasks in desktop mode. */ interface VisibleTasksListener { - /** - * Called when the desktop changes the number of visible freeform tasks. - */ + /** 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/DesktopModeTransitionTypes.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt new file mode 100644 index 000000000000..b24bd10eaa0d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt @@ -0,0 +1,95 @@ +/* + * 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.view.WindowManager.TransitionType +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TYPES + +/** + * Contains desktop mode [TransitionType]s (extended from [TRANSIT_DESKTOP_MODE_TYPES]) and helper + * methods. + */ +object DesktopModeTransitionTypes { + + const val TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 1 + const val TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW = TRANSIT_DESKTOP_MODE_TYPES + 2 + const val TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 3 + const val TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 4 + const val TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG = TRANSIT_DESKTOP_MODE_TYPES + 5 + const val TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 6 + const val TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 7 + const val TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 8 + + /** Return whether the [TransitionType] corresponds to a transition to enter desktop mode. */ + @JvmStatic + fun @receiver:TransitionType Int.isEnterDesktopModeTransition(): Boolean { + return this in + listOf( + TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, + TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, + TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, + TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN + ) + } + + /** + * Returns corresponding desktop mode enter [TransitionType] for a + * [DesktopModeTransitionSource]. + */ + @JvmStatic + @TransitionType + fun DesktopModeTransitionSource.getEnterTransitionType(): Int { + return when (this) { + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -> + TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON + DesktopModeTransitionSource.APP_FROM_OVERVIEW -> + TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW + DesktopModeTransitionSource.KEYBOARD_SHORTCUT -> + TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT + else -> TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN + } + } + + /** Return whether the [TransitionType] corresponds to a transition to exit desktop mode. */ + @JvmStatic + fun @receiver:TransitionType Int.isExitDesktopModeTransition(): Boolean { + return this in + listOf( + TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG, + TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON, + TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT, + TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN + ) + } + + /** + * Returns corresponding desktop mode exit [TransitionType] for a [DesktopModeTransitionSource]. + */ + @JvmStatic + @TransitionType + fun DesktopModeTransitionSource.getExitTransitionType(): Int { + return when (this) { + DesktopModeTransitionSource.TASK_DRAG -> TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -> + TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON + DesktopModeTransitionSource.KEYBOARD_SHORTCUT -> + TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT + else -> TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN + } + } +} 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..a9d4e5f3216e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.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.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" + } +} 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..217b1d356122 --- /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..ed0d2b87b03f 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 @@ -17,10 +17,10 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -35,6 +35,7 @@ import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.drawable.LayerDrawable; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -98,6 +99,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 +138,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 +157,7 @@ public class DesktopModeVisualIndicator { region.union(new Rect(0, -captionHeight, layout.width(), - edgeTransitionHeight)); + transitionHeight)); } return region; } @@ -184,7 +186,7 @@ public class DesktopModeVisualIndicator { // In freeform, keep the top corners clear. int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM ? mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : + com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : -captionHeight; region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height())); return region; @@ -221,6 +223,7 @@ public class DesktopModeVisualIndicator { mLeash = builder .setName("Desktop Mode Visual Indicator") .setContainerLayer() + .setCallsite("DesktopModeVisualIndicator.createView") .build(); t.show(mLeash); final WindowManager.LayoutParams lp = @@ -310,12 +313,14 @@ public class DesktopModeVisualIndicator { private static class VisualIndicatorAnimator extends ValueAnimator { private static final int FULLSCREEN_INDICATOR_DURATION = 200; private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f; - private static final float INDICATOR_FINAL_OPACITY = 0.7f; + private static final float INDICATOR_FINAL_OPACITY = 0.35f; + private static final int MAXIMUM_OPACITY = 255; - /** Determines how this animator will interact with the view's alpha: - * Fade in, fade out, or no change to alpha + /** + * Determines how this animator will interact with the view's alpha: + * Fade in, fade out, or no change to alpha */ - private enum AlphaAnimType{ + private enum AlphaAnimType { ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM } @@ -362,10 +367,10 @@ public class DesktopModeVisualIndicator { * Create animator for visual indicator changing type (i.e., fullscreen to freeform, * freeform to split, etc.) * - * @param view the view for this indicator + * @param view the view for this indicator * @param displayLayout information about the display the transitioning task is currently on - * @param origType the original indicator type - * @param newType the new indicator type + * @param origType the original indicator type + * @param newType the new indicator type */ private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view, @NonNull DisplayLayout displayLayout, IndicatorType origType, @@ -454,7 +459,11 @@ public class DesktopModeVisualIndicator { * @param fraction current animation fraction */ private void updateIndicatorAlpha(float fraction, View view) { - view.setAlpha(fraction * INDICATOR_FINAL_OPACITY); + final LayerDrawable drawable = (LayerDrawable) view.getBackground(); + drawable.findDrawableByLayerId(R.id.indicator_stroke) + .setAlpha((int) (MAXIMUM_OPACITY * fraction)); + drawable.findDrawableByLayerId(R.id.indicator_solid) + .setAlpha((int) (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY)); } /** @@ -462,7 +471,7 @@ public class DesktopModeVisualIndicator { */ private static Rect getMaxBounds(Rect startBounds) { return new Rect((int) (startBounds.left - - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), + - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), (int) (startBounds.top - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())), (int) (startBounds.right 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..c5111d68881d 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,18 +40,20 @@ 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 import com.android.wm.shell.common.DisplayLayout -import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper @@ -59,15 +62,22 @@ 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.desktopmode.DesktopModeTransitionSource 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.RecentTasksController 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.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE +import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity +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,83 +87,99 @@ 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 /** Handles moving tasks in and out of desktop */ class DesktopTasksController( - private val context: Context, - shellInit: ShellInit, - private val shellCommandHandler: ShellCommandHandler, - private val shellController: ShellController, - private val displayController: DisplayController, - private val shellTaskOrganizer: ShellTaskOrganizer, - private val syncQueue: SyncTransactionQueue, - private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val dragAndDropController: DragAndDropController, - private val transitions: Transitions, - private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, - private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, - private val toggleResizeDesktopTaskTransitionHandler: - ToggleResizeDesktopTaskTransitionHandler, - private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, - private val desktopModeTaskRepository: DesktopModeTaskRepository, - private val launchAdjacentController: LaunchAdjacentController, - private val recentsTransitionHandler: RecentsTransitionHandler, - private val multiInstanceHelper: MultiInstanceHelper, - @ShellMainThread private val mainExecutor: ShellExecutor -) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, + private val context: Context, + shellInit: ShellInit, + private val shellCommandHandler: ShellCommandHandler, + private val shellController: ShellController, + private val displayController: DisplayController, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val syncQueue: SyncTransactionQueue, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val dragAndDropController: DragAndDropController, + private val transitions: Transitions, + private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, + private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, + private val toggleResizeDesktopTaskTransitionHandler: 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, + private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, + private val recentTasksController: RecentTasksController? +) : + RemoteCallable<DesktopTasksController>, + Transitions.TransitionHandler, DragAndDropController.DragAndDropListener { private val desktopMode: DesktopModeImpl private var visualIndicator: DesktopModeVisualIndicator? = null private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) - - private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> { - t: SurfaceControl.Transaction -> - visualIndicator?.releaseVisualIndicator(t) - visualIndicator = null - } - private val taskVisibilityListener = object : VisibleTasksListener { - override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { - launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0 + private val mOnAnimationFinishedCallback = + Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> + visualIndicator?.releaseVisualIndicator(t) + visualIndicator = null } - } - private val dragToDesktopStateListener = object : DragToDesktopStateListener { - override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) + private val taskVisibilityListener = + object : VisibleTasksListener { + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { + launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0 + } } + private val dragToDesktopStateListener = + object : DragToDesktopStateListener { + override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { + removeVisualIndicator(tx) + } - override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) - } + override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) { + removeVisualIndicator(tx) + } - private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null + private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { + visualIndicator?.releaseVisualIndicator(tx) + visualIndicator = null + } } - } + private val sysUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) private val transitionAreaHeight - get() = context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_height - ) + get() = + context.resources.getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height + ) private val transitionAreaWidth - get() = context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_width - ) + get() = + context.resources.getDimensionPixelSize( + 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 +187,7 @@ 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 +211,16 @@ class DesktopTasksController( dragAndDropController.addListener(this) } + @VisibleForTesting + fun getVisualIndicator(): DesktopModeVisualIndicator? { + return visualIndicator + } + + // TODO(b/347289970): Consider replacing with API + private fun isSystemUIApplication(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.baseActivity?.packageName == sysUIPackageName + } + fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) @@ -205,11 +240,12 @@ 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) - } + val handler = + remoteTransition?.let { + OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) + } transitions.startTransition(transitionType, wct, handler).also { t -> handler?.setTransition(t) } @@ -218,47 +254,19 @@ 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, transitionSource: DesktopModeTransitionSource) { val allFocusedTasks = shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> taskInfo.isFocused && - (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && - taskInfo.activityType != ACTIVITY_TYPE_HOME + (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || + taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && + taskInfo.activityType != ACTIVITY_TYPE_HOME } if (allFocusedTasks.isNotEmpty()) { when (allFocusedTasks.size) { @@ -266,20 +274,22 @@ 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] - moveToDesktop(splitFocusedTask) + } else { + allFocusedTasks[0] + } + moveToDesktop(splitFocusedTask, transitionSource = transitionSource) } 1 -> { // Fullscreen case where we move the current focused task. - moveToDesktop(allFocusedTasks[0].taskId) + moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource) } else -> { KtProtoLog.w( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: Cannot enter desktop, expected less " + - "than 3 focused tasks but found %d", + "than 3 focused tasks but found %d", allFocusedTasks.size ) } @@ -289,22 +299,71 @@ class DesktopTasksController( /** Move a task with given `taskId` to desktop */ fun moveToDesktop( - taskId: Int, - wct: WindowContainerTransaction = WindowContainerTransaction() + taskId: Int, + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, ): Boolean { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { - task -> moveToDesktop(task, wct) - } ?: return false + moveToDesktop(it, wct, transitionSource) + } + ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource) return true } - /** - * Move a task to desktop - */ + private fun moveToDesktopFromNonRunningTask( + taskId: Int, + wct: WindowContainerTransaction, + transitionSource: DesktopModeTransitionSource, + ): Boolean { + recentTasksController?.findTaskInBackground(taskId)?.let { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToDesktopFromNonRunningTask taskId=%d", + taskId + ) + // TODO(342378842): Instead of using default display, support multiple displays + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId) + addMoveToDesktopChangesNonRunningTask(wct, taskId) + // TODO(343149901): Add DPI changes for task launch + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) + addPendingMinimizeTransition(transition, taskToMinimize) + return true + } + ?: return false + } + + private fun addMoveToDesktopChangesNonRunningTask( + wct: WindowContainerTransaction, + taskId: Int + ) { + val options = ActivityOptions.makeBasic() + options.launchWindowingMode = WINDOWING_MODE_FREEFORM + wct.startTask(taskId, options.toBundle()) + } + + /** Move a task to desktop */ fun moveToDesktop( - task: RunningTaskInfo, - wct: WindowContainerTransaction = WindowContainerTransaction() + task: RunningTaskInfo, + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, ) { + 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 + } + if (isSystemUIApplication(task)) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: Cannot enter desktop, " + + "systemUI top activity found." + ) + return + } KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: moveToDesktop taskId=%d", @@ -312,32 +371,34 @@ 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, transitionSource) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } } /** - * The first part of the animated drag to desktop transition. This is - * followed with a call to [finalizeDragToDesktop] or [cancelDragToDesktop]. + * The first part of the animated drag to desktop transition. This is followed with a call to + * [finalizeDragToDesktop] or [cancelDragToDesktop]. */ fun startDragToDesktop( - taskInfo: RunningTaskInfo, - dragToDesktopValueAnimator: MoveToDesktopAnimator, + taskInfo: RunningTaskInfo, + dragToDesktopValueAnimator: MoveToDesktopAnimator, ) { KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: startDragToDesktop taskId=%d", - taskInfo.taskId + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: startDragToDesktop taskId=%d", + taskInfo.taskId ) dragToDesktopTransitionHandler.startDragToDesktopTransition( - taskInfo.taskId, - dragToDesktopValueAnimator + taskInfo.taskId, + dragToDesktopValueAnimator ) } @@ -347,48 +408,59 @@ class DesktopTasksController( */ private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) { KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: finalizeDragToDesktop taskId=%d", - taskInfo.taskId + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: finalizeDragToDesktop taskId=%d", + taskInfo.taskId ) 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) } } /** - * Perform needed cleanup transaction once animation is complete. Bounds need to be set - * here instead of initial wct to both avoid flicker and to have task bounds to use for - * the staging animation. + * Perform needed cleanup transaction once animation is complete. Bounds need to be set here + * instead of initial wct to both avoid flicker and to have task bounds to use for the staging + * animation. * * @param taskInfo task entering split that requires a bounds update */ 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) { + fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> - moveToFullscreenWithAnimation(task, task.positionInParent) + moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) } } /** 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) } + fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { + getFocusedFreeformTask(displayId)?.let { + moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) } } @@ -401,7 +473,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 +486,11 @@ 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() } } @@ -429,21 +504,31 @@ class DesktopTasksController( "DesktopTasksController: cancelDragToDesktop taskId=%d", task.taskId ) - dragToDesktopTransitionHandler.cancelDragToDesktopTransition() + dragToDesktopTransitionHandler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) } - private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) { + private fun moveToFullscreenWithAnimation( + task: RunningTaskInfo, + position: Point, + transitionSource: DesktopModeTransitionSource + ) { KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToFullscreen with animation taskId=%d", - task.taskId + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToFullscreen with animation taskId=%d", + task.taskId ) val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { exitDesktopTaskTransitionHandler.startTransition( - Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback) + transitionSource, + wct, + position, + mOnAnimationFinishedCallback + ) } else { shellTaskOrganizer.applyTransaction(wct) releaseVisualIndicator() @@ -465,8 +550,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) } @@ -476,12 +563,12 @@ class DesktopTasksController( * Move task to the next display. * * Queries all current known display ids and sorts them in ascending order. Then iterates - * through the list and looks for the display id that is larger than the display id for - * the passed in task. If a display with a higher id is not found, iterates through the list and + * through the list and looks for the display id that is larger than the display id for the + * passed in task. If a display with a higher id is not found, iterates through the list and * finds the first display id that is not the display id for the passed in task. * - * If a display matching the above criteria is found, re-parents the task to that display. - * No-op if no such display is found. + * If a display matching the above criteria is found, re-parents the task to that display. No-op + * if no such display is found. */ fun moveToNextDisplay(taskId: Int) { val task = shellTaskOrganizer.getRunningTaskInfo(taskId) @@ -489,8 +576,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 +603,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 +630,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 +642,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 +678,57 @@ 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 +742,52 @@ 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) + private fun bringDesktopAppsToFrontBeforeShowingNewTask( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int + ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) - // First move home to front and then other tasks on top of it - moveHomeTaskToFront(wct) + private fun bringDesktopAppsToFront( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int? = null + ): RunningTaskInfo? { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", + newTaskIdInFront ?: "null" + ) - 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) } + if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Add translucent wallpaper activity to show the wallpaper underneath + addWallpaperActivity(wct) + } else { + // Move home to front + moveHomeTaskToFront(wct) + } + + 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 +797,33 @@ 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 +870,8 @@ 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})" @@ -710,7 +889,7 @@ class DesktopTasksController( } // Only handle fullscreen or freeform tasks triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && - triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { + triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { reason = "windowingMode not handled (${triggerTask.windowingMode})" false } @@ -720,30 +899,34 @@ class DesktopTasksController( if (!shouldHandleRequest) { KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: skipping handleRequest reason=%s", - reason + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: skipping handleRequest reason=%s", + reason ) return null } - val result = triggerTask?.let { task -> - when { - // If display has tasks stashed, handle as stashed launch - desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) - // Check if fullscreen task should be updated - task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) - // Check if freeform task should be updated - task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) - else -> { - null + val result = + triggerTask?.let { task -> + when { + request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) + // Check if the task has a top transparent activity + shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task) + // Check if the task has a top systemUI activity + isSystemUIApplication(task) -> handleIncompatibleTaskLaunch(task) + // Check if fullscreen task should be updated + task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) + // Check if freeform task should be updated + task.isFreeform -> handleFreeformTaskLaunch(task, transition) + else -> { + null + } } } - } KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: handleRequest result=%s", - result ?: "null" + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: handleRequest result=%s", + result ?: "null" ) return result } @@ -753,82 +936,128 @@ class DesktopTasksController( * This is intended to be used when desktop mode is part of another animation but isn't, itself, * animating. */ - fun syncSurfaceState( - info: TransitionInfo, - finishTransaction: SurfaceControl.Transaction - ) { + fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) { // Add rounded corners to freeform windows if (!DesktopModeStatus.useRoundedCorners()) { return } val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) info.changes - .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } - .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } + .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } + .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } - private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + // TODO(b/347289970): Consider replacing with API + 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" + - " taskId=%d", - task.taskId + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: switch freeform task to fullscreen oon transition" + + " taskId=%d", + task.taskId ) return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task) + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) } } - return null + val wct = WindowContainerTransaction() + if (useDesktopOverrideDensity()) { + wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) + } + // Desktop Mode is showing and we're launching a new Task - we might need to minimize + // a Task. + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + if (taskToMinimize != null) { + addPendingMinimizeTransition(transition, taskToMinimize) + return wct + } + return if (wct.isEmpty) null else wct } - 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" + - " taskId=%d", - task.taskId + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: switch fullscreen task to freeform on transition" + + " taskId=%d", + task.taskId ) 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 + /** + * If a task is not compatible with desktop mode freeform, it should always be launched in + * fullscreen. + */ + private fun handleIncompatibleTaskLaunch(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) { - // Display windowing is freeform, set to undefined and inherit it - WINDOWING_MODE_UNDEFINED - } else { - 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()) { - wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi()) + if (useDesktopOverrideDensity()) { + wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) } } @@ -836,16 +1065,18 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode - val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) { - // Display windowing is fullscreen, set to undefined and inherit it - WINDOWING_MODE_UNDEFINED - } else { - 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 { + WINDOWING_MODE_FULLSCREEN + } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.setBounds(taskInfo.token, Rect()) - if (isDesktopDensityOverrideSet()) { + if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } } @@ -854,32 +1085,82 @@ class DesktopTasksController( * Adds split screen changes to a transaction. Note that bounds are not reset here due to * animation; see {@link onDesktopSplitSelectAnimComplete} */ - private fun addMoveToSplitChanges( - 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) + private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { + // 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) + // If a drag to desktop is in progress, we want to enter split select + // even if the requesting task is already in split. + val isDragging = dragToDesktopTransitionHandler.inProgress + val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging + if (shouldRequestSplit) { + if (isDragging) { + releaseVisualIndicator() + val cancelState = if (leftOrTop) { + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT + } else { + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT + } + dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) + } else { + val wct = WindowContainerTransaction() + addMoveToSplitChanges(wct, taskInfo) + splitScreenController.requestEnterSplitSelect( + taskInfo, + wct, + if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT, + taskInfo.configuration.windowConfiguration.bounds + ) + } } } @@ -887,10 +1168,6 @@ class DesktopTasksController( return context.resources.displayMetrics.densityDpi } - private fun getDesktopDensityDpi(): Int { - return DESKTOP_DENSITY_OVERRIDE - } - /** Creates a new instance of the external interface to pass to another process. */ private fun createExternalInterface(): ExternalInterfaceBinder { return IDesktopModeImpl(this) @@ -903,9 +1180,9 @@ class DesktopTasksController( /** * Perform checks required on drag move. Create/release fullscreen indicator as needed. - * Different sources for x and y coordinates are used due to different needs for each: - * We want split transitions to be based on input coordinates but fullscreen transition - * to be based on task edge coordinate. + * Different sources for x and y coordinates are used due to different needs for each: We want + * split transitions to be based on input coordinates but fullscreen transition to be based on + * task edge coordinate. * * @param taskInfo the task being dragged. * @param taskSurface SurfaceControl of dragged task. @@ -927,20 +1204,25 @@ 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 +1233,55 @@ 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, + DesktopModeTransitionSource.TASK_DRAG + ) + } + 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() @@ -982,26 +1294,39 @@ class DesktopTasksController( * @param y height of drag, to be checked against status bar height. */ fun onDragPositioningEndThroughStatusBar( - taskInfo: RunningTaskInfo, - freeformBounds: Rect + inputCoordinates: PointF, + taskInfo: RunningTaskInfo, ) { - finalizeDragToDesktop(taskInfo, freeformBounds) - } - - private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int { - return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + 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 -> { + requestSplit(taskInfo, leftOrTop = true) + } + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + requestSplit(taskInfo, leftOrTop = false) + } + } } - /** - * Update the exclusion region for a specified task - */ + /** Update the exclusion region for a specified task */ fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) { desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion) } - /** - * Remove a previously tracked exclusion region for a specified task. - */ + /** Remove a previously tracked exclusion region for a specified task. */ fun removeExclusionRegionForTask(taskId: Int) { desktopModeTaskRepository.removeExclusionRegion(taskId) } @@ -1022,10 +1347,7 @@ class DesktopTasksController( * @param listener the listener to add. * @param callbackExecutor the executor to call the listener on. */ - fun setTaskRegionListener( - listener: Consumer<Region>, - callbackExecutor: Executor - ) { + fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) { desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor) } @@ -1050,14 +1372,16 @@ class DesktopTasksController( } // Start a new transition to launch the app - val opts = ActivityOptions.makeBasic().apply { - launchWindowingMode = WINDOWING_MODE_FREEFORM - pendingIntentLaunchFlags = - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK - setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true - } + val opts = + ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + pendingIntentLaunchFlags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED + ) + isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + } val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) @@ -1083,8 +1407,8 @@ class DesktopTasksController( @ExternalThread private inner class DesktopModeImpl : DesktopMode { override fun addVisibleTasksListener( - listener: VisibleTasksListener, - callbackExecutor: Executor + listener: VisibleTasksListener, + callbackExecutor: Executor ) { mainExecutor.execute { this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor) @@ -1092,25 +1416,35 @@ class DesktopTasksController( } override fun addDesktopGestureExclusionRegionListener( - listener: Consumer<Region>, - callbackExecutor: Executor + listener: Consumer<Region>, + callbackExecutor: Executor ) { mainExecutor.execute { this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor) } } - override fun enterDesktop(displayId: Int) { + override fun moveFocusedTaskToDesktop( + displayId: Int, + transitionSource: DesktopModeTransitionSource + ) { mainExecutor.execute { - this@DesktopTasksController.enterDesktop(displayId) + this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource) } } - override fun moveFocusedTaskToFullscreen(displayId: Int) { + override fun moveFocusedTaskToFullscreen( + displayId: Int, + transitionSource: DesktopModeTransitionSource + ) { mainExecutor.execute { - this@DesktopTasksController.enterFullscreen(displayId) + this@DesktopTasksController.enterFullscreen(displayId, transitionSource) } } + + override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) { + mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) } + } } /** The interface for calls from outside the host process. */ @@ -1119,46 +1453,35 @@ class DesktopTasksController( IDesktopMode.Stub(), ExternalInterfaceBinder { private lateinit var remoteListener: - SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> + SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> - private val listener: VisibleTasksListener = object : VisibleTasksListener { - override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { - KtProtoLog.v( + private val listener: VisibleTasksListener = + object : VisibleTasksListener { + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { + KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d", displayId, visibleTasksCount - ) - remoteListener.call { - l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount) + ) + remoteListener.call { 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 { remoteListener = - SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( - controller, - { c -> - c.desktopModeTaskRepository.addVisibleTasksListener( - listener, - c.mainExecutor - ) - }, - { c -> - c.desktopModeTaskRepository.removeVisibleTasksListener(listener) - } - ) + SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( + controller, + { c -> + c.desktopModeTaskRepository.addVisibleTasksListener( + listener, + c.mainExecutor + ) + }, + { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) } + ) } /** Invalidates this instance, preventing future calls from updating the controller. */ @@ -1168,36 +1491,31 @@ class DesktopTasksController( } override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "showDesktopApps" - ) { c -> c.showDesktopApps(displayId, remoteTransition) } + executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> + c.showDesktopApps(displayId, remoteTransition) + } } - override fun stashDesktopApps(displayId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "stashDesktopApps" - ) { c -> c.stashDesktopApps(displayId) } + override fun showDesktopApp(taskId: Int) { + executeRemoteCallWithTaskPermission(controller, "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 { val result = IntArray(1) - ExecutorUtils.executeRemoteCallWithTaskPermission( + executeRemoteCallWithTaskPermission( controller, "getVisibleTaskCount", { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, @@ -1207,43 +1525,41 @@ class DesktopTasksController( } override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { - ExecutorUtils.executeRemoteCallWithTaskPermission( + executeRemoteCallWithTaskPermission( controller, "onDesktopSplitSelectAnimComplete" - ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) } + ) { c -> + c.onDesktopSplitSelectAnimComplete(taskInfo) + } } override fun setTaskListener(listener: IDesktopTaskListener?) { KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "IDesktopModeImpl: set task listener=%s", - listener ?: "null" + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: set task listener=%s", + listener ?: "null" ) - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "setTaskListener" - ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } + executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> + listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() + } + } + + override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) { + executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c -> + c.moveToDesktop(taskId, transitionSource = transitionSource) + } } } companion object { - private val DESKTOP_DENSITY_OVERRIDE = - SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284) - private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) - @JvmField - val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties - .getInt("persist.wm.debug.freeform_initial_bounds_scale", 75) / 100f - - /** - * Check if desktop density override is enabled - */ - @JvmStatic - fun isDesktopDensityOverrideSet(): Boolean { - return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE - } + val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = + SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f } /** The positions on a screen that a task can snap to. */ - enum class SnapPosition { RIGHT, LEFT } + enum class SnapPosition { + RIGHT, + LEFT + } } 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..d99b724c936f 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 @@ -4,17 +4,22 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.RectEvaluator import android.animation.ValueAnimator +import android.app.ActivityManager.RunningTaskInfo 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 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.content.Intent import android.content.Intent.FILL_IN_COMPONENT +import android.graphics.PointF import android.graphics.Rect +import android.os.Bundle import android.os.IBinder import android.os.SystemClock import android.view.SurfaceControl @@ -25,6 +30,10 @@ 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.common.split.SplitScreenConstants.SplitPosition import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.splitscreen.SplitScreenController @@ -45,29 +54,28 @@ import java.util.function.Supplier * gesture. */ class DragToDesktopTransitionHandler( - private val context: Context, - private val transitions: Transitions, - private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val transactionSupplier: Supplier<SurfaceControl.Transaction> + private val context: Context, + private val transitions: Transitions, + private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val transactionSupplier: Supplier<SurfaceControl.Transaction> ) : TransitionHandler { constructor( - context: Context, - transitions: Transitions, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + context: Context, + transitions: Transitions, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer ) : this( - context, - transitions, - rootTaskDisplayAreaOrganizer, - Supplier { SurfaceControl.Transaction() } + context, + transitions, + rootTaskDisplayAreaOrganizer, + Supplier { SurfaceControl.Transaction() } ) private val rectEvaluator = RectEvaluator(Rect()) - private val launchHomeIntent = Intent(Intent.ACTION_MAIN) - .addCategory(Intent.CATEGORY_HOME) + private val launchHomeIntent = Intent(Intent.ACTION_MAIN).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 +83,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 @@ -99,48 +110,55 @@ class DragToDesktopTransitionHandler( * after one of the "end" or "cancel" transitions is merged into this transition. */ fun startDragToDesktopTransition( - taskId: Int, - dragToDesktopAnimator: MoveToDesktopAnimator, + taskId: Int, + dragToDesktopAnimator: MoveToDesktopAnimator, ) { if (inProgress) { KtProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DragToDesktop: Drag to desktop transition already in progress." + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DragToDesktop: Drag to desktop transition already in progress." ) return } - val options = ActivityOptions.makeBasic().apply { - setTransientLaunch() - setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis()) - pendingIntentCreatorBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - } - val pendingIntent = PendingIntent.getActivity( + val options = + ActivityOptions.makeBasic().apply { + setTransientLaunch() + setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis()) + pendingIntentCreatorBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + } + val pendingIntent = + PendingIntent.getActivity( context, 0 /* requestCode */, launchHomeIntent, FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, options.toBundle() - ) + ) val wct = WindowContainerTransaction() - wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) - val startTransitionToken = transitions - .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this) - - transitionState = if (isSplitTask(taskId)) { - TransitionState.FromSplit( + 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 - ) - } else { - TransitionState.FromFullscreen( + startTransitionToken = startTransitionToken, + otherSplitTask = otherTask + ) + } else { + TransitionState.FromFullscreen( draggedTaskId = taskId, dragAnimator = dragToDesktopAnimator, startTransitionToken = startTransitionToken - ) - } + ) + } } /** @@ -149,20 +167,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) } /** @@ -172,7 +190,7 @@ class DragToDesktopTransitionHandler( * outside the desktop drop zone and is instead dropped back into the status bar region that * means the user wants to remain in their current windowing mode. */ - fun cancelDragToDesktopTransition() { + fun cancelDragToDesktopTransition(cancelState: CancelState) { if (!inProgress) { // Don't attempt to cancel a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully @@ -186,13 +204,32 @@ class DragToDesktopTransitionHandler( clearState() return } - state.cancelled = true - if (state.draggedTaskChange != null) { + state.cancelState = cancelState + + if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { // Regular case, transient launch of Home happened as is waiting for the cancel // transient to start and merge. Animate the cancellation (scale back to original // bounds) first before actually starting the cancel transition so that the wallpaper // is visible behind the animating task. startCancelAnimation() + } else if ( + state.draggedTaskChange != null && + (cancelState == CancelState.CANCEL_SPLIT_LEFT || + cancelState == CancelState.CANCEL_SPLIT_RIGHT) + ) { + // We have a valid dragged task, but the animation will be handled by + // SplitScreenController; request the transition here. + @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + val wct = WindowContainerTransaction() + restoreWindowOrder(wct, state) + state.startTransitionFinishTransaction?.apply() + state.startTransitionFinishCb?.onTransitionFinished(null /* wct */) + requestSplitFromScaledTask(splitPosition, wct) + clearState() } else { // There's no dragged task, this can happen when the "cancel" happened too quickly // before the "start" transition is even ready (like on a fling gesture). The @@ -203,16 +240,65 @@ class DragToDesktopTransitionHandler( } } + /** Calculate the bounds of a scaled task, then use those bounds to request split select. */ + private fun requestSplitFromScaledTask( + @SplitPosition splitPosition: Int, + wct: WindowContainerTransaction + ) { + val state = requireTransitionState() + val taskInfo = state.draggedTaskChange?.taskInfo + ?: error("Expected non-null taskInfo") + val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds) + val taskScale = state.dragAnimator.scale + val scaledWidth = taskBounds.width() * taskScale + val scaledHeight = taskBounds.height() * taskScale + val dragPosition = PointF(state.dragAnimator.position) + state.dragAnimator.cancelAnimator() + val animatedTaskBounds = Rect( + dragPosition.x.toInt(), + dragPosition.y.toInt(), + (dragPosition.x + scaledWidth).toInt(), + (dragPosition.y + scaledHeight).toInt() + ) + requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) + } + + private fun requestSplitSelect( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + @SplitPosition splitPosition: Int, + taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds) + ) { + // Prepare to exit split in order to enter split select. + if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + splitScreenController.prepareExitSplitScreen( + wct, + splitScreenController.getStageOfTask(taskInfo.taskId), + SplitScreenController.EXIT_REASON_DESKTOP_MODE + ) + splitScreenController.transitionHandler.onSplitToDesktop() + } + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi) + splitScreenController.requestEnterSplitSelect( + taskInfo, + wct, + splitPosition, + taskBounds + ) + } + override fun startAnimation( - transition: IBinder, - info: TransitionInfo, - startTransaction: SurfaceControl.Transaction, - finishTransaction: SurfaceControl.Transaction, - finishCallback: Transitions.TransitionFinishCallback + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback ): Boolean { val state = requireTransitionState() - val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && + val isStartDragToDesktop = + info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && transition == state.startTransitionToken if (!isStartDragToDesktop) { return false @@ -245,13 +331,16 @@ class DragToDesktopTransitionHandler( when (state) { is TransitionState.FromSplit -> { state.splitRootChange = change - val layer = if (!state.cancelled) { - // Normal case, split root goes to the bottom behind everything else. - appLayers - i - } else { - // Cancel-early case, pretend nothing happened so split root stays top. - dragLayer - } + val layer = + if (state.cancelState == CancelState.NO_CANCEL) { + // Normal case, split root goes to the bottom behind everything + // else. + appLayers - i + } else { + // Cancel-early case, pretend nothing happened so split root stays + // top. + dragLayer + } startTransaction.apply { setLayer(change.leash, layer) show(change.leash) @@ -293,10 +382,23 @@ class DragToDesktopTransitionHandler( // Do not do this in the cancel-early case though, since in that case nothing should // happen on screen so the layering will remain the same as if no transition // occurred. - if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) { + if ( + change.taskInfo?.taskId == state.draggedTaskId && + state.cancelState != CancelState.STANDARD_CANCEL + ) { + // We need access to the dragged task's change in both non-cancel and split + // cancel cases. state.draggedTaskChange = change + } + if ( + change.taskInfo?.taskId == state.draggedTaskId && + state.cancelState == CancelState.NO_CANCEL + ) { taskDisplayAreaOrganizer.reparentToDisplayArea( - change.endDisplayId, change.leash, startTransaction) + change.endDisplayId, + change.leash, + startTransaction + ) val bounds = change.endAbsBounds startTransaction.apply { setLayer(change.leash, dragLayer) @@ -310,11 +412,11 @@ class DragToDesktopTransitionHandler( state.startTransitionFinishTransaction = finishTransaction startTransaction.apply() - if (!state.cancelled) { + if (state.cancelState == CancelState.NO_CANCEL) { // Normal case, start animation to scale down the dragged task. It'll also be moved to // follow the finger and when released we'll start the next phase/transition. state.dragAnimator.startAnimation() - } else { + } else if (state.cancelState == CancelState.STANDARD_CANCEL) { // Cancel-early case, the state was flagged was cancelled already, which means the // gesture ended in the cancel region. This can happen even before the start transition // is ready/animate here when cancelling quickly like with a fling. There's no point @@ -322,30 +424,68 @@ class DragToDesktopTransitionHandler( // directly into starting the cancel transition to restore WM order. Surfaces should // not move as if no transition happened. startCancelDragToDesktopTransition() + } else if ( + state.cancelState == CancelState.CANCEL_SPLIT_LEFT || + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT + ){ + // Cancel-early case for split-cancel. The state was flagged already as a cancel for + // requesting split select. Similar to the above, this can happen due to quick fling + // gestures. We can simply request split here without needing to calculate animated + // task bounds as the task has not shrunk at all. + val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + val taskInfo = state.draggedTaskChange?.taskInfo + ?: error("Expected non-null task info.") + val wct = WindowContainerTransaction() + restoreWindowOrder(wct) + state.startTransitionFinishTransaction?.apply() + state.startTransitionFinishCb?.onTransitionFinished(null /* wct */) + requestSplitSelect(wct, taskInfo, splitPosition) } return true } override fun mergeAnimation( - transition: IBinder, - info: TransitionInfo, - t: SurfaceControl.Transaction, - mergeTarget: IBinder, - finishCallback: Transitions.TransitionFinishCallback + transition: IBinder, + info: TransitionInfo, + t: SurfaceControl.Transaction, + mergeTarget: IBinder, + finishCallback: Transitions.TransitionFinishCallback ) { val state = requireTransitionState() - val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && + // We don't want to merge the split select animation if that's what we requested. + if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT || + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) { + clearState() + return + } + val isCancelTransition = + info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && transition == state.cancelTransitionToken && mergeTarget == state.startTransitionToken - val isEndTransition = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && + val isEndTransition = + info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && mergeTarget == state.startTransitionToken - val startTransactionFinishT = state.startTransitionFinishTransaction + val startTransactionFinishT = + state.startTransitionFinishTransaction ?: error("Start transition expected to be waiting for merge but wasn't") - val startTransitionFinishCb = state.startTransitionFinishCb + val startTransitionFinishCb = + state.startTransitionFinishCb ?: 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) @@ -355,97 +495,83 @@ class DragToDesktopTransitionHandler( state.draggedTaskChange = change } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) { // Other freeform tasks that are being restored go behind the dragged task. - val draggedTaskLeash = state.draggedTaskChange?.leash + val draggedTaskLeash = + state.draggedTaskChange?.leash ?: error("Expected dragged leash to be non-null") t.setRelativeLayer(change.leash, draggedTaskLeash, -i) startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i) } } - val draggedTaskChange = state.draggedTaskChange + val draggedTaskChange = + state.draggedTaskChange ?: throw IllegalStateException("Expected non-null change of dragged task") val draggedTaskLeash = draggedTaskChange.leash 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) - .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) - .apply { - addUpdateListener { animator -> - val animBounds = animator.animatedValue as Rect - tx.apply { - setScale(draggedTaskLeash, 1f, 1f) - setPosition( - draggedTaskLeash, - animBounds.left.toFloat(), - animBounds.top.toFloat() - ) - setWindowCrop( - draggedTaskLeash, - animBounds.width(), - animBounds.height() - ) - } - onTaskResizeAnimationListener.onBoundsChange( - state.draggedTaskId, - tx, - animBounds + 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, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() ) + setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height()) } - addListener(object : AnimatorListenerAdapter() { + onTaskResizeAnimationListener.onBoundsChange( + state.draggedTaskId, + tx, + animBounds + ) + } + addListener( + object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) startTransitionFinishCb.onTransitionFinished(null /* null */) clearState() } - }) - start() - } + } + ) + start() + } } else if (isCancelTransition) { info.changes.forEach { change -> t.show(change.leash) @@ -459,8 +585,8 @@ class DragToDesktopTransitionHandler( } override fun handleRequest( - transition: IBinder, - request: TransitionRequestInfo + transition: IBinder, + request: TransitionRequestInfo ): WindowContainerTransaction? { // Only handle transitions started from shell. return null @@ -489,13 +615,11 @@ class DragToDesktopTransitionHandler( val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator - val draggedTaskChange = state.draggedTaskChange - ?: throw IllegalStateException("Expected non-null task change") + 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 @@ -505,61 +629,75 @@ class DragToDesktopTransitionHandler( val dy = targetY - y val tx: SurfaceControl.Transaction = transactionSupplier.get() ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) - .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) - .apply { - addUpdateListener { animator -> - val scale = animator.animatedValue as Float - val fraction = animator.animatedFraction - val animX = x + (dx * fraction) - val animY = y + (dy * fraction) - tx.apply { - setPosition(sc, animX, animY) - setScale(sc, scale, scale) - show(sc) - apply() - } + .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) + .apply { + addUpdateListener { animator -> + val scale = animator.animatedValue as Float + val fraction = animator.animatedFraction + val animX = x + (dx * fraction) + val animY = y + (dy * fraction) + tx.apply { + setPosition(sc, animX, animY) + setScale(sc, scale, scale) + show(sc) + apply() } - addListener(object : AnimatorListenerAdapter() { + } + addListener( + object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx) // Start the cancel transition to restore order. startCancelDragToDesktopTransition() } - }) - start() - } + } + ) + start() + } } private fun startCancelDragToDesktopTransition() { val state = requireTransitionState() val wct = WindowContainerTransaction() + restoreWindowOrder(wct, state) + state.cancelTransitionToken = + transitions.startTransition( + TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this + ) + } + + private fun restoreWindowOrder( + wct: WindowContainerTransaction, + state: TransitionState = requireTransitionState() + ) { when (state) { is TransitionState.FromFullscreen -> { // There may have been tasks sent behind home that are not the dragged task (like // when the dragged task is translucent and that makes the task behind it visible). // Restore the order of those first. - state.otherRootChanges.mapNotNull { it.container }.forEach { wc -> - // TODO(b/322852244): investigate why even though these "other" tasks are - // reordered in front of home and behind the translucent dragged task, its - // surface is not visible on screen. - wct.reorder(wc, true /* toTop */) - } - val wc = state.draggedTaskChange?.container + state.otherRootChanges + .mapNotNull { it.container } + .forEach { wc -> + // TODO(b/322852244): investigate why even though these "other" tasks are + // reordered in front of home and behind the translucent dragged task, its + // surface is not visible on screen. + wct.reorder(wc, true /* toTop */) + } + val wc = + state.draggedTaskChange?.container ?: error("Dragged task should be non-null before cancelling") // Then the dragged task a the very top. wct.reorder(wc, true /* toTop */) } is TransitionState.FromSplit -> { - val wc = state.splitRootChange?.container + val wc = + state.splitRootChange?.container ?: error("Split root should be non-null before cancelling") wct.reorder(wc, true /* toTop */) } } val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling") wct.restoreTransientOrder(homeWc) - - state.cancelTransitionToken = transitions.startTransition( - TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this) } private fun clearState() { @@ -567,7 +705,19 @@ 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 { @@ -576,6 +726,7 @@ class DragToDesktopTransitionHandler( interface DragToDesktopStateListener { fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) + fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) } @@ -588,37 +739,51 @@ class DragToDesktopTransitionHandler( abstract var cancelTransitionToken: IBinder? abstract var homeToken: WindowContainerToken? abstract var draggedTaskChange: Change? - abstract var cancelled: Boolean + abstract var cancelState: CancelState abstract var startAborted: Boolean data class FromFullscreen( - override val draggedTaskId: Int, - override val dragAnimator: MoveToDesktopAnimator, - override val startTransitionToken: IBinder, - override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, - override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, - override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, - override var draggedTaskChange: Change? = null, - override var cancelled: Boolean = false, - override var startAborted: Boolean = false, - var otherRootChanges: MutableList<Change> = mutableListOf() + override val draggedTaskId: Int, + override val dragAnimator: MoveToDesktopAnimator, + override val startTransitionToken: IBinder, + override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, + override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, + override var cancelTransitionToken: IBinder? = null, + override var homeToken: WindowContainerToken? = null, + override var draggedTaskChange: Change? = null, + override var cancelState: CancelState = CancelState.NO_CANCEL, + override var startAborted: Boolean = false, + var otherRootChanges: MutableList<Change> = mutableListOf() ) : TransitionState() + data class FromSplit( - override val draggedTaskId: Int, - override val dragAnimator: MoveToDesktopAnimator, - override val startTransitionToken: IBinder, - override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, - override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, - override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, - override var draggedTaskChange: Change? = null, - override var cancelled: Boolean = false, - override var startAborted: Boolean = false, - var splitRootChange: Change? = null, + override val draggedTaskId: Int, + override val dragAnimator: MoveToDesktopAnimator, + override val startTransitionToken: IBinder, + override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, + override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, + override var cancelTransitionToken: IBinder? = null, + override var homeToken: WindowContainerToken? = null, + override var draggedTaskChange: Change? = null, + override var cancelState: CancelState = CancelState.NO_CANCEL, + override var startAborted: Boolean = false, + var splitRootChange: Change? = null, + var otherSplitTask: Int ) : TransitionState() } + /** Enum to provide context on cancelling a drag to desktop event. */ + enum class CancelState { + /** No cancel case; this drag is not flagged for a cancel event. */ + NO_CANCEL, + /** A standard cancel event; should restore task to previous windowing mode. */ + STANDARD_CANCEL, + /** A cancel event where the task will request to enter split on the left side. */ + CANCEL_SPLIT_LEFT, + /** A cancel event where the task will request to enter split on the right side. */ + CANCEL_SPLIT_RIGHT + } + companion object { /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */ private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L 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..e5b624f91c54 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 @@ -18,7 +18,8 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_TO_DESKTOP; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isEnterDesktopModeTransition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -30,6 +31,7 @@ import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; import android.view.WindowManager; +import android.view.WindowManager.TransitionType; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -37,6 +39,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener; @@ -59,6 +62,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener; + public EnterDesktopTaskTransitionHandler( Transitions transitions) { this(transitions, SurfaceControl.Transaction::new); @@ -72,16 +76,23 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } void setOnTaskResizeAnimationListener(OnTaskResizeAnimationListener listener) { - mOnTaskResizeAnimationListener = listener; + mOnTaskResizeAnimationListener = listener; } /** * 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) { - final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); + public IBinder moveToDesktop( + @NonNull WindowContainerTransaction wct, + DesktopModeTransitionSource transitionSource + ) { + final IBinder token = mTransitions.startTransition(getEnterTransitionType(transitionSource), + wct, this); mPendingTransitionTokens.add(token); + return token; } @Override @@ -113,7 +124,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition private boolean startChangeTransition( @NonNull IBinder transition, - @WindowManager.TransitionType int type, + @TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @@ -123,7 +134,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (type == TRANSIT_MOVE_TO_DESKTOP + if (isEnterDesktopModeTransition(type) && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { return animateMoveToDesktop(change, startT, finishCallback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 7342bd1ae5de..891f75cfdbda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -18,6 +18,9 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -30,6 +33,7 @@ import android.os.IBinder; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.WindowManager; +import android.view.WindowManager.TransitionType; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -38,6 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -52,11 +57,12 @@ import java.util.function.Supplier; */ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { private static final int FULLSCREEN_ANIMATION_DURATION = 336; + private final Context mContext; private final Transitions mTransitions; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; - private Supplier<SurfaceControl.Transaction> mTransactionSupplier; + private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; private Point mPosition; public ExitDesktopTaskTransitionHandler( @@ -76,17 +82,19 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH /** * Starts Transition of a given type - * @param type Transition type - * @param wct WindowContainerTransaction for transition - * @param position Position of the task when transition is started + * + * @param transitionSource DesktopModeTransitionSource for transition + * @param wct WindowContainerTransaction for transition + * @param position Position of the task when transition is started * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@WindowManager.TransitionType int type, + public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { mPosition = position; mOnAnimationFinishedCallback = onAnimationEndCallback; - final IBinder token = mTransitions.startTransition(type, wct, this); + final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), + wct, this); mPendingTransitionTokens.add(token); } @@ -120,7 +128,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH @VisibleForTesting boolean startChangeTransition( @NonNull IBinder transition, - @WindowManager.TransitionType int type, + @TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @@ -129,7 +137,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH return false; } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE + if (isExitDesktopModeTransition(type) && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { // This Transition animates a task to fullscreen after being dragged to status bar final Resources resources = mContext.getResources(); 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..a7ec2037706d 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode; import android.app.ActivityManager.RunningTaskInfo; import android.window.RemoteTransition; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.IDesktopTaskListener; /** @@ -28,10 +29,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 +46,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, in DesktopModeTransitionSource transitionSource); }
\ 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/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index c469e652b117..88d0554669b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -86,9 +86,9 @@ class ToggleResizeDesktopTaskTransitionHandler( .setWindowCrop(leash, startBounds.width(), startBounds.height()) .show(leash) onTaskResizeAnimationListener.onAnimationStart( - taskId, - startTransaction, - startBounds + taskId, + startTransaction, + startBounds ) }, onEnd = { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index 9aa5f4ffcd78..0acc7df98d1c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -54,8 +54,8 @@ Specifically, to support calling into a controller from an external process (lik extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long references to the outer controller - Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of - the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity - and ensure the call happens on the main shell thread and not the binder thread + the `executeRemoteCallWithTaskPermission()` calls to verify the caller's identity and ensure the + call happens on the main shell thread and not the binder thread - Inject `ShellController` and add the instance of the implementation as external interface - In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then call the SystemUIProxy method as needed in that code 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..c374eb8e8f03 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 @@ -32,7 +32,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; import android.app.ActivityManager; @@ -67,8 +66,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; @@ -316,7 +315,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll } // TODO(b/290391688): Also update the session data with task stack changes pd.dragSession = new DragSession(ActivityTaskManager.getInstance(), - mDisplayController.getDisplayLayout(displayId), event.getClipData()); + mDisplayController.getDisplayLayout(displayId), event.getClipData(), + event.getDragFlags()); pd.dragSession.update(); pd.activeDragCount++; pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession)); 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..a42ca1905ee7 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,9 +16,9 @@ package com.android.wm.shell.draganddrop; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +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; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; @@ -46,7 +46,6 @@ import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; -import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; import android.content.Intent; @@ -264,13 +263,14 @@ public class DragAndDropPolicy { final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + // Put BAL flags to avoid activity start aborted. + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); } - // 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); final UserHandle user = session.appData.getParcelableExtra(EXTRA_USER); if (isTask) { @@ -301,16 +301,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/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 59d696918448..4bb10dfdf8c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -22,11 +22,11 @@ import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; 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.common.split.SplitScreenUtils.getResizingBackgroundColor; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; @@ -41,7 +41,6 @@ import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; @@ -278,7 +277,7 @@ public class DragLayout extends LinearLayout final int activityType = taskInfo1.getActivityType(); if (activityType == ACTIVITY_TYPE_STANDARD) { Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); - int bgColor1 = getResizingBackgroundColor(taskInfo1); + int bgColor1 = getResizingBackgroundColor(taskInfo1).toArgb(); mDropZoneView1.setAppInfo(bgColor1, icon1); mDropZoneView2.setAppInfo(bgColor1, icon1); updateDropZoneSizes(null, null); // passing null splits the views evenly @@ -298,10 +297,10 @@ public class DragLayout extends LinearLayout mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); if (topOrLeftTask != null && bottomOrRightTask != null) { Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo); - int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask); + int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask).toArgb(); Drawable bottomOrRightIcon = mIconProvider.getIcon( bottomOrRightTask.topActivityInfo); - int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask); + int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask).toArgb(); mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon); mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon); } @@ -556,11 +555,6 @@ public class DragLayout extends LinearLayout } } - private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { - final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); - return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); - } - /** * Dumps information about this drag layout. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index 8f1bc59af1ef..0addd432aff0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -40,6 +40,7 @@ import java.util.List; public class DragSession { private final ActivityTaskManager mActivityTaskManager; private final ClipData mInitialDragData; + private final int mInitialDragFlags; final DisplayLayout displayLayout; // The activity info associated with the activity in the appData or the launchableIntent @@ -62,9 +63,10 @@ public class DragSession { boolean dragItemSupportsSplitscreen; DragSession(ActivityTaskManager activityTaskManager, - DisplayLayout dispLayout, ClipData data) { + DisplayLayout dispLayout, ClipData data, int dragFlags) { mActivityTaskManager = activityTaskManager; mInitialDragData = data; + mInitialDragFlags = dragFlags; displayLayout = dispLayout; } @@ -94,6 +96,6 @@ public class DragSession { dragItemSupportsSplitscreen = activityInfo == null || ActivityInfo.isResizeableMode(activityInfo.resizeMode); appData = mInitialDragData.getItemAt(0).getIntent(); - launchableIntent = DragUtils.getLaunchIntent(mInitialDragData); + launchableIntent = DragUtils.getLaunchIntent(mInitialDragData, mInitialDragFlags); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index 24f8e186bf76..e215870f1894 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -24,6 +24,7 @@ import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.view.DragEvent; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -67,14 +68,18 @@ public class DragUtils { */ @Nullable public static PendingIntent getLaunchIntent(@NonNull DragEvent dragEvent) { - return getLaunchIntent(dragEvent.getClipData()); + return getLaunchIntent(dragEvent.getClipData(), dragEvent.getDragFlags()); } /** * Returns a launchable intent in the given `ClipData` or `null` if there is none. */ @Nullable - public static PendingIntent getLaunchIntent(@NonNull ClipData data) { + public static PendingIntent getLaunchIntent(@NonNull ClipData data, int dragFlags) { + if ((dragFlags & View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) == 0) { + // Disallow launching the intent if the app does not want to delegate it to the system + return null; + } for (int i = 0; i < data.getItemCount(); i++) { final ClipData.Item item = data.getItemAt(i); if (item.getIntentSender() != null) { 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..7d2aa275a684 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.addOrMoveFreeformTaskToTop(taskInfo.displayId, 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.removeFreeformTask(taskInfo.displayId, 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.addOrMoveFreeformTaskToTop(taskInfo.displayId, 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..cd478e5bd567 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); + } } /** @@ -128,6 +143,10 @@ public class KeyguardTransitionHandler } public static boolean handles(TransitionInfo info) { + // There is no animation for screen-wake unless we are immediately unlocking. + if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) { + return false; + } return (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0; } @@ -142,6 +161,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 +176,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 +297,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 +367,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..962309f7c534 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 @@ -19,7 +19,6 @@ package com.android.wm.shell.onehanded; import static android.os.UserHandle.myUserId; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; @@ -55,7 +54,7 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index a9aa6badcfe2..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..0a3c15b6057f 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 @@ -40,6 +40,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; @@ -583,7 +584,7 @@ public class PipAnimationController { } static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash, - Rect baseValue, Rect startValue, Rect endValue, Rect sourceHintRect, + Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint, @PipAnimationController.TransitionDirection int direction, float startingAngle, @Surface.Rotation int rotationDelta) { final boolean isOutPipDirection = isOutPipDirection(direction); @@ -613,14 +614,25 @@ public class PipAnimationController { initialContainerRect = initialSourceValue; } - final Rect sourceHintRectInsets; - if (sourceHintRect == null) { - sourceHintRectInsets = null; + final Rect adjustedSourceRectHint = new Rect(); + if (sourceRectHint == null || sourceRectHint.isEmpty()) { + // Crop a Rect matches the aspect ratio and pivots at the center point. + // This is done for entering case only. + if (isInPipDirection(direction)) { + final float aspectRatio = endValue.width() / (float) endValue.height(); + adjustedSourceRectHint.set(PipUtils.getEnterPipWithOverlaySrcRectHint( + startValue, aspectRatio)); + } } else { - sourceHintRectInsets = new Rect(sourceHintRect.left - initialContainerRect.left, - sourceHintRect.top - initialContainerRect.top, - initialContainerRect.right - sourceHintRect.right, - initialContainerRect.bottom - sourceHintRect.bottom); + adjustedSourceRectHint.set(sourceRectHint); + } + final Rect sourceHintRectInsets = new Rect(); + if (!adjustedSourceRectHint.isEmpty()) { + sourceHintRectInsets.set( + adjustedSourceRectHint.left - initialContainerRect.left, + adjustedSourceRectHint.top - initialContainerRect.top, + initialContainerRect.right - adjustedSourceRectHint.right, + initialContainerRect.bottom - adjustedSourceRectHint.bottom); } final Rect zeroInsets = new Rect(0, 0, 0, 0); @@ -648,7 +660,7 @@ public class PipAnimationController { } float angle = (1.0f - fraction) * startingAngle; setCurrentValue(bounds); - if (inScaleTransition() || sourceHintRect == null) { + if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) { if (isOutPipDirection) { getSurfaceTransactionHelper().crop(tx, leash, end) .scale(tx, leash, end, bounds); @@ -661,7 +673,7 @@ public class PipAnimationController { } else { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, - sourceHintRect, initialSourceValue, bounds, insets, + adjustedSourceRectHint, initialSourceValue, bounds, insets, isInPipDirection, fraction); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); @@ -729,9 +741,6 @@ public class PipAnimationController { } private Rect computeInsets(float fraction) { - if (sourceHintRectInsets == null) { - return zeroInsets; - } final Rect startRect = isOutPipDirection ? sourceHintRectInsets : zeroInsets; final Rect endRect = isOutPipDirection ? zeroInsets : sourceHintRectInsets; return mInsetsEvaluator.evaluate(fraction, startRect, endRect); @@ -743,11 +752,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/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index e11e8596a7fe..ff2d46e11107 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -226,11 +226,10 @@ public abstract class PipContentOverlay { appBoundsCenterX - mOverlayHalfSize, appBoundsCenterY - mOverlayHalfSize); // Scale back the bitmap with the pivot point at center. - mTmpTransform.postScale( + final float scale = Math.min( (float) mAppBounds.width() / currentBounds.width(), - (float) mAppBounds.height() / currentBounds.height(), - appBoundsCenterX, - appBoundsCenterY); + (float) mAppBounds.height() / currentBounds.height()); + mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY); atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index a58d94ecd19b..202f60dad842 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -137,7 +137,7 @@ public class PipSurfaceTransactionHelper { mTmpDestinationRect.inset(insets); // Scale to the bounds no smaller than the destination and offset such that the top/left // of the scaled inset source rect aligns with the top/left of the destination bounds - final float scale; + final float scale, left, top; if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. @@ -148,12 +148,15 @@ public class PipSurfaceTransactionHelper { ? (float) destinationBounds.width() / sourceBounds.width() : (float) destinationBounds.height() / sourceBounds.height(); scale = (1 - fraction) * startScale + fraction * endScale; + left = destinationBounds.left - insets.left * scale; + top = destinationBounds.top - insets.top * scale; } else { scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), (float) destinationBounds.height() / sourceBounds.height()); + // Work around the rounding error by fix the position at very beginning. + left = scale == 1 ? 0 : destinationBounds.left - insets.left * scale; + top = scale == 1 ? 0 : destinationBounds.top - insets.top * scale; } - final float left = destinationBounds.left - insets.left * scale; - final float top = destinationBounds.top - insets.top * scale; mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) .setCrop(leash, mTmpDestinationRect) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bd186ba22588..e4420d73886e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -82,7 +82,6 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; @@ -92,10 +91,12 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -372,6 +373,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull final Rect mAppBounds = new Rect(); + /** The source rect hint from stopSwipePipToHome(). */ + @Nullable + private Rect mSwipeSourceRectHint; + public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @@ -503,7 +508,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. */ public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay, Rect appBounds) { + SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState); // do nothing if there is no startSwipePipToHome being called before @@ -512,6 +517,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } mPipBoundsState.setBounds(destinationBounds); setContentOverlay(overlay, appBounds); + mSwipeSourceRectHint = sourceRectHint; if (ENABLE_SHELL_TRANSITIONS && overlay != null) { // With Shell transition, the overlay was attached to the remote transition leash, which // will be removed when the current transition is finished, so we need to reparent it @@ -522,7 +528,39 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); t.setLayer(overlay, Integer.MAX_VALUE); t.apply(); + // This serves as a last resort in case the Shell Transition is not handled properly. + // We want to make sure the overlay passed from Launcher gets removed eventually. + mayRemoveContentOverlay(overlay); + } + } + + /** + * Returns non-null Rect if the pip is entering from swipe-to-home with a specified source hint. + * This also consumes the rect hint. + */ + @Nullable + Rect takeSwipeSourceRectHint() { + final Rect sourceRectHint = mSwipeSourceRectHint; + if (sourceRectHint == null || sourceRectHint.isEmpty()) { + return null; } + mSwipeSourceRectHint = null; + return mPipTransitionState.getInSwipePipToHomeTransition() ? sourceRectHint : null; + } + + private void mayRemoveContentOverlay(SurfaceControl overlay) { + final WeakReference<SurfaceControl> overlayRef = new WeakReference<>(overlay); + final long timeoutDuration = (mEnterAnimationDuration + + CONTENT_OVERLAY_FADE_OUT_DELAY_MS + + EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS) * 2L; + mMainExecutor.executeDelayed(() -> { + final SurfaceControl overlayLeash = overlayRef.get(); + if (overlayLeash != null && overlayLeash.isValid() && overlayLeash == mPipOverlay) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Cleanup the overlay(%s) as a last resort.", overlayLeash); + removeContentOverlay(overlayLeash, null /* callback */); + } + }, timeoutDuration); } /** @@ -578,6 +616,19 @@ 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(() -> { + // TODO(341627042): force set to entered state to avoid potential stack overflow. + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); + exitPip(animationDurationMs, requestEnterSplit); + }); + return; + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -824,7 +875,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(); @@ -947,7 +999,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void onEndOfSwipePipToHomeTransition() { if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mPipTransitionController.setEnterAnimationType(ANIM_TYPE_BOUNDS); return; } @@ -1957,6 +2008,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface); return; } if (surface == null || !surface.isValid()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 6a1a62ea30a1..3cae72d89ecc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -27,6 +27,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; @@ -42,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; @@ -75,6 +75,7 @@ import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.CounterRotatorHelper; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; @@ -107,6 +108,7 @@ public class PipTransition extends PipTransitionController { private final PipDisplayLayoutState mPipDisplayLayoutState; private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private final HomeTransitionObserver mHomeTransitionObserver; private final Optional<SplitScreenController> mSplitScreenOptional; private final PipAnimationController mPipAnimationController; private @PipAnimationController.AnimationType int mEnterAnimationType = ANIM_TYPE_BOUNDS; @@ -164,6 +166,7 @@ public class PipTransition extends PipTransitionController { PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + HomeTransitionObserver homeTransitionObserver, Optional<SplitScreenController> splitScreenOptional) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -174,6 +177,7 @@ public class PipTransition extends PipTransitionController { mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = pipSurfaceTransactionHelper; + mHomeTransitionObserver = homeTransitionObserver; mSplitScreenOptional = splitScreenOptional; } @@ -279,6 +283,12 @@ public class PipTransition extends PipTransitionController { // Entering PIP. if (isEnteringPip(info)) { + if (handleEnteringPipWithDisplayChange(transition, info, startTransaction, + finishTransaction, finishCallback)) { + // The destination position is applied directly and let default transition handler + // run the display change animation. + return true; + } startEnterAnimation(info, startTransaction, finishTransaction, finishCallback); return true; } @@ -293,6 +303,25 @@ public class PipTransition extends PipTransitionController { return false; } + private boolean handleEnteringPipWithDisplayChange(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mFixedRotationState != FIXED_ROTATION_UNDEFINED + || !TransitionUtil.hasDisplayChange(info)) { + return false; + } + final TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange == null) { + return false; + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: handle entering PiP with display change", TAG); + mMixedHandler.animateEnteringPipWithDisplayChange(transition, info, pipChange, + startT, finishT, finishCallback); + return true; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -347,9 +376,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(); } } @@ -450,6 +486,14 @@ public class PipTransition extends PipTransitionController { // activity windowing mode, and set the task bounds to the final bounds wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); wct.setBounds(taskInfo.token, destinationBounds); + // If the animation is only used to apply destination bounds immediately and + // invisibly, then reshow it until the pip is drawn with the bounds. + final PipAnimationController.PipTransitionAnimator<?> animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.getEndValue().equals(0f)) { + tx.addTransactionCommittedListener(mTransitions.getMainExecutor(), + () -> fadeExistingPip(true /* show */)); + } } else { wct.setBounds(taskInfo.token, null /* bounds */); } @@ -612,6 +656,9 @@ public class PipTransition extends PipTransitionController { startTransaction.remove(mPipOrganizer.mPipOverlay); mPipOrganizer.clearContentOverlay(); } + if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) { + mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */); + } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: No window of exiting PIP is found. Can't play expand animation", TAG); @@ -636,6 +683,7 @@ public class PipTransition extends PipTransitionController { .setContainerLayer() .setHidden(false) .setParent(root.getLeash()) + .setCallsite("PipTransition.startExitAnimation") .build(); startTransaction.reparent(activitySurface, pipLeash); // Put the activity at local position with offset in case it is letterboxed. @@ -817,8 +865,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); } @@ -840,8 +891,11 @@ public class PipTransition extends PipTransitionController { && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED && !change.getContainer().equals(mCurrentPipTaskToken)) { // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps - // that enter PiP instantly on opening, mostly from CTS/Flicker tests) - if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN) { + // that enter PiP instantly on opening, mostly from CTS/Flicker tests). + // TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also + // be allowed to animate if the task in question is pinned already - see b/308054074. + if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN + || transitType == TRANSIT_TO_FRONT) { return true; } // This can happen if the request to enter PIP happens when we are collecting for @@ -862,19 +916,24 @@ public class PipTransition extends PipTransitionController { mEnterAnimationType = type; } - private void startEnterAnimation(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Search for an Enter PiP transition - TransitionInfo.Change enterPip = null; + @Nullable + private static TransitionInfo.Change getPipChange(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.getTaskInfo() != null && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { - enterPip = change; + return change; } } + return null; + } + + private void startEnterAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // Search for an Enter PiP transition + final TransitionInfo.Change enterPip = getPipChange(info); if (enterPip == null) { throw new IllegalStateException("Trying to start PiP animation without a pip" + "participant"); @@ -945,11 +1004,14 @@ public class PipTransition extends PipTransitionController { final Rect currentBounds = pipChange.getStartAbsBounds(); int rotationDelta = deltaRotation(startRotation, endRotation); - Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( - taskInfo.pictureInPictureParams, currentBounds, destinationBounds); + Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint(); + if (sourceHintRect == null) { + sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( + taskInfo.pictureInPictureParams, currentBounds, destinationBounds); + } if (rotationDelta != Surface.ROTATION_0 - && mFixedRotationState == FIXED_ROTATION_TRANSITION) { - // Need to get the bounds of new rotation in old rotation for fixed rotation, + && endRotation != mPipDisplayLayoutState.getRotation()) { + // Computes the destination bounds in new rotation. computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo, destinationBounds, sourceHintRect); } @@ -975,6 +1037,7 @@ public class PipTransition extends PipTransitionController { } startTransaction.apply(); + int animationDuration = mEnterExitAnimationDuration; PipAnimationController.PipTransitionAnimator animator; if (enterAnimationType == ANIM_TYPE_BOUNDS) { animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, @@ -1006,18 +1069,29 @@ public class PipTransition extends PipTransitionController { } } } else if (enterAnimationType == ANIM_TYPE_ALPHA) { + // In case augmentRequest() is unable to apply the entering bounds (e.g. the request + // info only contains display change), keep the animation invisible (alpha 0) and + // duration 0 to apply the destination bounds. The actual fade-in animation will be + // done in onFinishResize() after the bounds are applied. + final boolean fadeInAfterOnFinishResize = rotationDelta != Surface.ROTATION_0 + && mFixedRotationState == FIXED_ROTATION_CALLBACK; animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, - 0f, 1f); + 0f, fadeInAfterOnFinishResize ? 0f : 1f); + if (fadeInAfterOnFinishResize) { + animationDuration = 0; + } mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); + // Always reset to bounds animation type afterwards. + setEnterAnimationType(ANIM_TYPE_BOUNDS); } else { throw new RuntimeException("Unrecognized animation type: " + enterAnimationType); } mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(mEnterExitAnimationDuration); + .setDuration(animationDuration); if (rotationDelta != Surface.ROTATION_0 && mFixedRotationState == FIXED_ROTATION_TRANSITION) { // For fixed rotation, the animation destination bounds is in old rotation coordinates. @@ -1041,8 +1115,11 @@ public class PipTransition extends PipTransitionController { final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); - // Transform the destination bounds to current display coordinates. - rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation); + if (mFixedRotationState == FIXED_ROTATION_TRANSITION) { + // Transform the destination bounds to current display coordinates. + // With fixed rotation, the bounds of new rotation shows in old rotation. + rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation); + } // When entering PiP (from button navigation mode), adjust the source rect hint by // display cutout if applicable. if (outSourceHintRect != null && taskInfo.displayCutoutInsets != 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 32442f740a52..6eefdcfc4d93 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 @@ -49,12 +49,12 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.DefaultMixedHandler; 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. @@ -68,6 +68,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final Transitions mTransitions; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); protected PipTaskOrganizer mPipOrganizer; + protected DefaultMixedHandler mMixedHandler; protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { @@ -125,12 +126,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. } @@ -173,6 +170,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH mPipOrganizer = pto; } + public void setMixedHandler(DefaultMixedHandler mixedHandler) { + mMixedHandler = mixedHandler; + } + + public void applyTransaction(WindowContainerTransaction wct) { + mShellTaskOrganizer.applyTransaction(wct); + } + /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ @@ -266,9 +271,9 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** Whether a particular package is same as current pip package. */ - public boolean isInPipPackage(String packageName) { + public boolean isPackageActiveInPip(String packageName) { final TaskInfo inPipTask = mPipOrganizer.getTaskInfo(); - return packageName != null && inPipTask != null + return packageName != null && inPipTask != null && mPipOrganizer.isInPip() && packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent)); } @@ -305,6 +310,22 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void end() { } + /** + * Finish the current transition if possible. + * + * @param tx transaction to be applied with a potentially new draw after finishing. + */ + public void finishTransition(@Nullable SurfaceControl.Transaction tx) { + } + + /** + * 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..8c4bf7620068 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 @@ -22,7 +22,6 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; @@ -122,6 +121,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 +153,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 +846,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } - private void onSystemUiStateChanged(boolean isValidState, int flag) { + private void onSystemUiStateChanged(boolean isValidState, long flag) { mTouchHandler.onSystemUiStateChanged(isValidState); } @@ -998,9 +1001,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay, Rect appBounds) { + SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) { mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay, - appBounds); + appBounds, sourceRectHint); } private void abortSwipePipToHome(int taskId, ComponentName componentName) { @@ -1043,6 +1046,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 +1077,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 +1194,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); }); @@ -1287,13 +1291,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { if (overlay != null) { overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> controller.stopSwipePipToHome( - taskId, componentName, destinationBounds, overlay, appBounds)); + taskId, componentName, destinationBounds, overlay, appBounds, + sourceRectHint)); } @Override 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/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index c2f4d72a1ddf..ca0d61f8fc9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -233,6 +233,7 @@ public class TvPipTransition extends PipTransitionController { .setContainerLayer() .setHidden(false) .setParent(root.getLeash()) + .setCallsite("TvPipTransition.startAnimation") .build(); startTransaction.reparent(activitySurface, pipLeash); // Put the activity at local position with offset in case it is letterboxed. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java new file mode 100644 index 000000000000..895c2aeba9ef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java @@ -0,0 +1,117 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Animator that handles the alpha animation for entering PIP + */ +public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + @IntDef(prefix = {"FADE_"}, value = { + FADE_IN, + FADE_OUT + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface Fade {} + + public static final int FADE_IN = 0; + public static final int FADE_OUT = 1; + + private final int mEnterAnimationDuration; + private final SurfaceControl mLeash; + private final SurfaceControl.Transaction mStartTransaction; + + // optional callbacks for tracking animation start and end + @Nullable private Runnable mAnimationStartCallback; + @Nullable private Runnable mAnimationEndCallback; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + public PipAlphaAnimator(Context context, + SurfaceControl leash, + SurfaceControl.Transaction tx, + @Fade int direction) { + mLeash = leash; + mStartTransaction = tx; + if (direction == FADE_IN) { + setFloatValues(0f, 1f); + } else { // direction == FADE_OUT + setFloatValues(1f, 0f); + } + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mEnterAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipEnterAnimationDuration); + setDuration(mEnterAnimationDuration); + addListener(this); + addUpdateListener(this); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final float alpha = (Float) animation.getAnimatedValue(); + mSurfaceControlTransactionFactory.getTransaction().setAlpha(mLeash, alpha).apply(); + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java new file mode 100644 index 000000000000..5c561fed89c7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -0,0 +1,154 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +/** + * Animator that handles any resize related animation for PIP. + */ +public class PipResizeAnimator extends ValueAnimator + implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{ + @NonNull + private final Context mContext; + @NonNull + private final SurfaceControl mLeash; + @Nullable + private SurfaceControl.Transaction mStartTx; + @Nullable + private SurfaceControl.Transaction mFinishTx; + @Nullable + private Runnable mAnimationStartCallback; + @Nullable + private Runnable mAnimationEndCallback; + private RectEvaluator mRectEvaluator; + private final Rect mBaseBounds = new Rect(); + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + private final Rect mAnimatedRect = new Rect(); + private final float mDelta; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + public PipResizeAnimator(@NonNull Context context, + @NonNull SurfaceControl leash, + @Nullable SurfaceControl.Transaction startTransaction, + @Nullable SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + int duration, + float delta) { + mContext = context; + mLeash = leash; + mStartTx = startTransaction; + mFinishTx = finishTransaction; + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + + mBaseBounds.set(baseBounds); + mStartBounds.set(startBounds); + mAnimatedRect.set(startBounds); + mEndBounds.set(endBounds); + mDelta = delta; + + mRectEvaluator = new RectEvaluator(mAnimatedRect); + + setObjectValues(startBounds, endBounds); + addListener(this); + addUpdateListener(this); + setEvaluator(mRectEvaluator); + // TODO: change this + setDuration(duration); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTx != null) { + setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); + mStartTx.apply(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + final float degrees = (1.0f - fraction) * mDelta; + setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); + tx.apply(); + } + + /** + * Set a proper transform matrix for a leash to move it to given bounds with a certain rotation. + * + * @param baseBounds crop/buffer size relative to which we are scaling the leash. + * @param targetBounds bounds to which we are scaling the leash. + * @param degrees degrees of rotation - counter-clockwise is positive by convention. + */ + public static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect baseBounds, Rect targetBounds, float degrees) { + Matrix transformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; + final float scale = (float) targetBounds.width() / baseBounds.width(); + + transformTensor.setScale(scale, scale); + transformTensor.postTranslate(targetBounds.left, targetBounds.top); + transformTensor.postRotate(degrees, targetBounds.centerX(), targetBounds.centerY()); + + tx.setMatrix(leash, transformTensor, mMatrixTmp); + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTx != null) { + setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} 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..fc0d36d13b2e 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,39 @@ 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; @@ -47,28 +57,64 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; 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; +import java.io.PrintWriter; + /** * 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 ShellCommandHandler mShellCommandHandler; + 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, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, @@ -76,8 +122,12 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { mContext = context; + mShellCommandHandler = shellCommandHandler; mShellController = shellController; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; @@ -85,6 +135,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,24 +146,42 @@ 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, + ShellCommandHandler shellCommandHandler, + 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, shellCommandHandler, shellController, + displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, + pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, + pipTransitionState, mainExecutor); } private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); // Ensure that we have the display info in case we get calls to update the bounds before the // listener calls back mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); 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 +195,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 +270,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) { @@ -193,11 +285,73 @@ public class PipController implements ConfigurationChangeListener, } private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { 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)); + } + } + + private void dump(PrintWriter pw, String prefix) { + final String innerPrefix = " "; + pw.println(TAG); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mPipBoundsState.dump(pw, innerPrefix); + mPipDisplayLayoutState.dump(pw, innerPrefix); } /** @@ -206,9 +360,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 +391,8 @@ public class PipController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); } @Override @@ -234,13 +410,15 @@ public class PipController implements ConfigurationChangeListener, @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { if (overlay != null) { overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> controller.onSwipePipToHomeAnimationStart( - taskId, componentName, destinationBounds, overlay, appBounds)); + taskId, componentName, destinationBounds, overlay, appBounds, + sourceRectHint)); } @Override @@ -257,7 +435,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..495cd0075494 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -0,0 +1,790 @@ +/* + * 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 (mPipBoundsState.getBounds().equals( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion())) { + // Avoid scheduling transitions for bounds that don't change, such transition is + // a no-op and would be aborted. + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + cleanUpHighPerfSessionMaybe(); + // SCHEDULED_BOUNDS_CHANGE can have multiple active listeners making + // actual changes (e.g. PipTouchHandler). So post state update onto handler, + // to run after synchronous dispatch is complete. + mPipTransitionState.postState(PipTransitionState.CHANGED_PIP_BOUNDS); + 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(); + + // Signal that the transition is done - should update transition state by default. + mPipScheduler.scheduleFinishResizePip(false /* configAtEnd */); + 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..33e80bd80988 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -0,0 +1,598 @@ +/* + * 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.internal.util.Preconditions; +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 com.android.wm.shell.pip2.animation.PipResizeAnimator; + +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 Rect mStartBoundsAfterRelease = 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; + } + + // Cache initial bounds after release for animation before mLastResizeBounds are modified. + mStartBoundsAfterRelease.set(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; + + if (mPipBoundsState.getBounds().equals(mLastResizeBounds)) { + // If the bounds are invariant move the destination bounds by a single pixel + // to top/bottom to avoid a no-op transition. This trick helps keep the + // animation a part of the transition. + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsState.getBounds()); + + // Move to the top if closer to the bottom edge and vice versa. + boolean inTopHalf = snapFraction < 1.5 || snapFraction > 3.5; + int offsetY = inTopHalf ? 1 : -1; + mLastResizeBounds.offset(0 /* dx */, offsetY); + } + mWaitingForBoundsChangeTransition = true; + + // Schedule PiP resize transition, but delay any config updates until very end. + mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds, true /* configAtEnd */); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + if (!mWaitingForBoundsChangeTransition) break; + // If resize transition was scheduled from this component, handle leash updates. + mWaitingForBoundsChangeTransition = false; + + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkState(pipLeash != null, + "No leash cached by mPipTransitionState=" + mPipTransitionState); + + SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishTx = extra.getParcelable( + PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), + mPipBoundsState.getBounds().height()); + + PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, + startTx, finishTx, mPipBoundsState.getBounds(), mStartBoundsAfterRelease, + mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle); + animator.setAnimationEndCallback(() -> { + // All motion operations have actually finished, so make bounds cache updates. + mUpdateResizeBoundsCallback.accept(mLastResizeBounds); + cleanUpHighPerfSessionMaybe(); + + // Signal that we are done with resize transition + mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); + }); + animator.start(); + 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..9c1e321a1273 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,78 @@ 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) { + scheduleAnimateResizePip(toBounds, false /* configAtEnd */); + } + + /** + * Animates resizing of the pinned stack given the duration. + * + * @param configAtEnd true if we are delaying config updates until the transition ends. + */ + public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) { + if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTaskToken, toBounds); - mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); + wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + if (configAtEnd) { + wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken); + } + mPipTransitionController.startResizeTransition(wct); } - void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { - mInSwipePipToHomeTransition = true; + /** + * Signals to Core to finish the PiP resize transition. + * Note that we do not allow any actual WM Core changes at this point. + * + * @param configAtEnd true if we are waiting for config updates at the end of the transition. + */ + public void scheduleFinishResizePip(boolean configAtEnd) { + SurfaceControl.Transaction tx = null; + if (configAtEnd) { + tx = new SurfaceControl.Transaction(); + tx.addTransactionCommittedListener(mMainExecutor, () -> { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + }); + } else { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + } + mPipTransitionController.finishTransition(tx); } - boolean isInSwipePipToHomeTransition() { - return mInSwipePipToHomeTransition; + /** + * 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 */); } - void onExitPip() { - mPipTaskToken = null; - mPinnedTaskLeash = null; + /** + * 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()); + + 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..56a465a4889a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -0,0 +1,1128 @@ +/* + * 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.ShellCommandHandler; +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 ShellCommandHandler mShellCommandHandler; + 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, + ShellCommandHandler shellCommandHandler, + 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; + mShellCommandHandler = shellCommandHandler; + 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(); + + mShellCommandHandler.addDumpCallback(this::dump, this); + 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..57dc5f92b2b6 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,52 @@ 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.pip2.animation.PipAlphaAnimator; 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 +97,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 +116,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 +135,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition collection stage lifecycle hooks + // + @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { @@ -106,13 +152,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 +179,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 +198,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 +228,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 +254,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 +286,80 @@ 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; + } + + SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); + PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; + + Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds(); + Rect destinationBounds = pipChange.getEndAbsBounds(); + + float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat(); + + // We fake the source rect hint when the one prvided by the app is invalid for + // the animation with an app icon overlay. + Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint() + : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio); + + WindowContainerTransaction finishWct = new WindowContainerTransaction(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + final float scale = (float) destinationBounds.width() / animationSrcRectHint.width(); + startTransaction.setWindowCrop(pipLeash, animationSrcRectHint); + startTransaction.setPosition(pipLeash, + destinationBounds.left - animationSrcRectHint.left * scale, + destinationBounds.top - animationSrcRectHint.top * scale); + startTransaction.setScale(pipLeash, scale, scale); + + if (overlayLeash != null) { + final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); + + // 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 +368,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,26 +385,53 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + Rect destinationBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkNotNull(pipLeash, "Leash is null for alpha transition."); + + // Start transition with 0 alpha at the entry bounds. + startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) + .setWindowCrop(pipLeash, destinationBounds.width(), destinationBounds.height()) + .setAlpha(pipLeash, 0f); + + PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipLeash, startTransaction, + PipAlphaAnimator.FADE_IN); + animator.setAnimationEndCallback(() -> { + finishCallback.onTransitionFinished(null); + // This should update the pip transition state accordingly after we stop playing. + onClientDrawAtTransitionEnd(); + }); + + animator.start(); + return true; + } + private boolean startExpandAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @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); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); return true; } - private boolean startExpandAnimation(@NonNull TransitionInfo info, + private boolean removePipImmediately(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { startTransaction.apply(); finishCallback.onTransitionFinished(null); - onExitPip(); + 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 +457,7 @@ public class PipTransition extends PipTransitionController { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); + wct.deferConfigToTransitionEnd(pipTask.token); return wct; } @@ -328,20 +483,82 @@ public class PipTransition extends PipTransitionController { private boolean isLegacyEnter(@NonNull TransitionInfo info) { TransitionInfo.Change pipChange = getPipChange(info); - // If the only change in the changes list is a TO_FRONT mode PiP task, + // If the only change in the changes list is a opening type PiP task, // then this is legacy-enter PiP. - return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT - && info.getChanges().size() == 1; + return pipChange != null && info.getChanges().size() == 1 + && (pipChange.getMode() == TRANSIT_TO_FRONT || pipChange.getMode() == TRANSIT_OPEN); } - /** - * 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; + } - private void onExitPip() { - mPipTaskToken = null; - mPipScheduler.onExitPip(); + 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); + } + } + + @Override + public void finishTransition(@Nullable SurfaceControl.Transaction tx) { + WindowContainerTransaction wct = null; + if (tx != null && mPipTransitionState.mPipTaskToken != null) { + // Outside callers can only provide a transaction to be applied with the final draw. + // So no actual WM changes can be applied for this transition after this point. + wct = new WindowContainerTransaction(); + wct.setBoundsChangeTransaction(mPipTransitionState.mPipTaskToken, tx); + } + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(wct); + } + } + + @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; + } } } 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..9d599caf13dd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -0,0 +1,322 @@ +/* + * 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.os.Handler; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.shared.annotations.ShellMainThread; + +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; + + // + // Dependencies + // + + @ShellMainThread + private final Handler mMainHandler; + + // + // 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<>(); + + public PipTransitionState(@ShellMainThread Handler handler) { + mMainHandler = handler; + } + + /** + * @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; + } + } + + /** + * Posts the state update for PiP in the context of transitions onto the main handler. + * + * <p>This is done to guarantee that any callback dispatches for the present state are + * complete. This is relevant for states that have multiple listeners, such as + * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with + * the actual transition scheduling.</p> + */ + public void postState(@TransitionState int state) { + postState(state, null /* extra */); + } + + /** + * Posts the state update for PiP in the context of transitions onto the main handler. + * + * <p>This is done to guarantee that any callback dispatches for the present state are + * complete. This is relevant for states that have multiple listeners, such as + * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with + * the actual transition scheduling.</p> + * + * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. + */ + public void postState(@TransitionState int state, @Nullable Bundle extra) { + mMainHandler.post(() -> setState(state, extra)); + } + + 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..497c3f704c82 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 @@ -18,6 +18,8 @@ package com.android.wm.shell.protolog; import com.android.internal.protolog.common.IProtoLogGroup; +import java.util.UUID; + /** * Defines logging groups for ProtoLog. * @@ -52,7 +54,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, @@ -116,12 +118,22 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { this.mLogToLogcat = logToLogcat; } + @Override + public int getId() { + return Consts.START_ID + this.ordinal(); + } + private static class Consts { 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; + + private static final int START_ID = (int) ( + UUID.nameUUIDFromBytes(ShellProtoLogGroup.class.getName().getBytes()) + .getMostSignificantBits() % Integer.MAX_VALUE); } } 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..245829ecafb3 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,12 @@ 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); + + /** A task has moved to front. */ + oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS new file mode 100644 index 000000000000..452644b05a2a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS @@ -0,0 +1,6 @@ +# WM shell sub-module task stack owners +uysalorhan@google.com +samcackett@google.com +alexchau@google.com +silvajordan@google.com +uwaisashraf@google.com
\ No newline at end of file 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..03c8cf8cc795 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,17 +19,18 @@ 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.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; +import static com.android.window.flags.Flags.enableTaskStackObserverInShell; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; 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,14 +50,15 @@ 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; +import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; @@ -73,7 +75,8 @@ import java.util.function.Consumer; * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener, + TaskStackTransitionObserver.TaskStackTransitionObserverListener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; @@ -84,9 +87,10 @@ public class RecentTasksController implements TaskStackListenerCallback, private final TaskStackListenerImpl mTaskStackListener; private final RecentTasksImpl mImpl = new RecentTasksImpl(); private final ActivityTaskManager mActivityTaskManager; + private final TaskStackTransitionObserver mTaskStackTransitionObserver; 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) @@ -112,13 +116,15 @@ public class RecentTasksController implements TaskStackListenerCallback, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor ) { if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { return null; } return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, - taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor); + taskStackListener, activityTaskManager, desktopModeTaskRepository, + taskStackTransitionObserver, mainExecutor); } RecentTasksController(Context context, @@ -128,14 +134,16 @@ public class RecentTasksController implements TaskStackListenerCallback, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; mShellCommandHandler = shellCommandHandler; mActivityTaskManager = activityTaskManager; - mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); + mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; mDesktopModeTaskRepository = desktopModeTaskRepository; + mTaskStackTransitionObserver = taskStackTransitionObserver; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @@ -154,6 +162,10 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, + mMainExecutor); + } } void setTransitionHandler(RecentsTransitionHandler handler) { @@ -252,8 +264,14 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRunningTaskVanished(taskInfo); } - public void onTaskWindowingModeChanged(TaskInfo taskInfo) { + /** + * Notify listeners that the running infos related to recent tasks was updated. + * + * This currently includes windowing mode and visibility. + */ + public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { notifyRecentTasksChanged(); + notifyRunningTaskChanged(taskInfo); } @Override @@ -261,6 +279,12 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRecentTasksChanged(); } + @Override + public void onTaskMovedToFrontThroughTransition( + ActivityManager.RunningTaskInfo runningTaskInfo) { + notifyTaskMovedToFront(runningTaskInfo); + } + @VisibleForTesting void notifyRecentTasksChanged() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); @@ -278,7 +302,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 +318,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 +330,41 @@ 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 void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null + || !enableTaskStackObserverInShell() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onTaskMovedToFront(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onTaskMovedToFront", e); + } + } + + private boolean shouldEnableRunningTasksForDesktopMode() { + return mPcFeatureEnabled + || (DesktopModeStatus.canEnterDesktopMode(mContext) + && enableDesktopWindowingTaskbarRunningApps()); + } + @VisibleForTesting void registerRecentTasksListener(IRecentTasksListener listener) { mListener = listener; @@ -332,6 +395,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 +406,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 +435,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]))); } @@ -403,6 +476,26 @@ public class RecentTasksController implements TaskStackListenerCallback, return null; } + /** + * Find the background task that match the given taskId. + */ + @Nullable + public ActivityManager.RecentTaskInfo findTaskInBackground(int taskId) { + List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, + ActivityManager.getCurrentUser()); + for (int i = 0; i < tasks.size(); i++) { + final ActivityManager.RecentTaskInfo task = tasks.get(i); + if (task.isVisible) { + continue; + } + if (taskId == task.taskId) { + return task; + } + } + return null; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); @@ -444,6 +537,16 @@ public class RecentTasksController implements TaskStackListenerCallback, }); }); } + + @Override + public void setTransitionBackgroundColor(@Nullable Color color) { + mMainExecutor.execute(() -> { + if (mTransitionHandler == null) { + return; + } + mTransitionHandler.setTransitionBackgroundColor(color); + }); + } } @@ -471,6 +574,16 @@ 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)); + } + + @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onTaskMovedToFront(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..3a266d9bb3ef 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; @@ -384,6 +402,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + /** + * Cleans up the recents transition. This should generally not be called directly + * to cancel a transition after it has started, instead callers should call one of + * the cancel() methods to ensure that Launcher is notified. + */ void cleanUp() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.cleanup", mInstanceId); @@ -418,7 +441,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.start", mInstanceId); if (mListener == null || mTransition == null) { - cleanUp(); + Slog.e(TAG, "Missing listener or transition, hasListener=" + (mListener != null) + + " hasTransition=" + (mTransition != null)); + cancel("No listener (" + (mListener == null) + + ") or no transition (" + (mTransition == null) + ")"); return false; } // First see if this is a valid recents transition. @@ -442,7 +468,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { if (mRecentsTask == null && !hasPausingTasks) { // Recents is already running apparently, so this is a no-op. Slog.e(TAG, "Tried to start recents while it is already running."); - cleanUp(); + cancel("No recents task and no pausing tasks"); return false; } @@ -465,6 +491,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 +567,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 +610,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 +1118,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 +1146,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 +1189,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 +1200,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/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt new file mode 100644 index 000000000000..7c5f10a5bcca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -0,0 +1,143 @@ +/* + * 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.recents + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.ArrayMap +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import com.android.window.flags.Flags.enableTaskStackObserverInShell +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import dagger.Lazy +import java.util.concurrent.Executor + +/** + * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners + * about task stack changes. + * + * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it + */ +class TaskStackTransitionObserver( + private val transitions: Lazy<Transitions>, + shellInit: ShellInit +) : Transitions.TransitionObserver { + + private val transitionToTransitionChanges: MutableMap<IBinder, TransitionChanges> = + mutableMapOf() + private val taskStackTransitionObserverListeners = + ArrayMap<TaskStackTransitionObserverListener, Executor>() + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + shellInit.addInitCallback(::onInit, this) + } + } + + fun onInit() { + transitions.get().registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + if (enableTaskStackObserverInShell()) { + val taskInfoList = mutableListOf<RunningTaskInfo>() + val transitionTypeList = mutableListOf<Int>() + + for (change in info.changes) { + if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { + continue + } + + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (change.mode == WindowManager.TRANSIT_OPEN) { + change.taskInfo?.let { taskInfoList.add(it) } + transitionTypeList.add(change.mode) + } + } + transitionToTransitionChanges.put( + transition, + TransitionChanges(taskInfoList, transitionTypeList) + ) + } + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + val taskInfoList = + transitionToTransitionChanges.getOrDefault(transition, TransitionChanges()).taskInfoList + val typeList = + transitionToTransitionChanges + .getOrDefault(transition, TransitionChanges()) + .transitionTypeList + transitionToTransitionChanges.remove(transition) + + for ((index, taskInfo) in taskInfoList.withIndex()) { + if ( + TransitionUtil.isOpeningType(typeList[index]) && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + ) { + notifyTaskStackTransitionObserverListeners(taskInfo) + } + } + } + + fun addTaskStackTransitionObserverListener( + taskStackTransitionObserverListener: TaskStackTransitionObserverListener, + executor: Executor + ) { + taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor + } + + fun removeTaskStackTransitionObserverListener( + taskStackTransitionObserverListener: TaskStackTransitionObserverListener + ) { + taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener) + } + + private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) { + taskStackTransitionObserverListeners.forEach { (listener, executor) -> + executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) } + } + } + + /** Listener to use to get updates regarding task stack from this observer */ + interface TaskStackTransitionObserverListener { + /** Called when a task is moved to front. */ + fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {} + } + + private data class TransitionChanges( + val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(), + val transitionTypeList: MutableList<Int> = ArrayList() + ) +} 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..dd219d32bbaa 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 @@ -24,7 +24,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.common.MultiInstanceHelper.getComponent; import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent; import static com.android.wm.shell.common.MultiInstanceHelper.samePackage; @@ -90,7 +89,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 +97,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 +137,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 +151,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 +437,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 +486,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 +505,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 +1064,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 +1128,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 +1165,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 +1179,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)); + } } /** @@ -1197,8 +1240,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void invalidate() { mController = null; - // Unregister the listener to ensure any registered binder death recipients are unlinked + // Unregister the listeners to ensure any binder death recipients are unlinked mListener.unregister(); + mSelectListener.unregister(); } @Override 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..0541a0287179 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); @@ -493,10 +501,11 @@ class SplitScreenTransitions { mAnimatingTransition = null; mOnFinish.run(); - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(wct /* wct */); - mFinishCallback = null; - } + if (mFinishCallback != null) { + Transitions.TransitionFinishCallback currentFinishCallback = mFinishCallback; + mFinishCallback = null; + currentFinishCallback.onTransitionFinished(wct /* wct */); + } } private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) { @@ -529,6 +538,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 ec907fd9bd12..b6a18e537600 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 @@ -16,10 +16,8 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; 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; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -43,6 +41,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString; +import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; @@ -59,6 +58,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 +66,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 +157,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 @@ -200,7 +202,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; private final TransactionPool mTransactionPool; - private final SplitScreenTransitions mSplitTransitions; + private SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; // Cache live tile tasks while entering recents, evict them from stages in finish transaction @@ -236,6 +238,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 +251,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; @@ -398,6 +411,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSplitTransitions; } + @VisibleForTesting + void setSplitTransitions(SplitScreenTransitions splitScreenTransitions) { + mSplitTransitions = splitScreenTransitions; + } + public boolean isSplitScreenVisible() { return mSideStageListener.mVisible && mMainStageListener.mVisible; } @@ -529,7 +547,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) { @@ -582,7 +600,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); wct.startTask(taskId, options); // If this should be mixed, send the task to avoid split handle transition directly. - if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(taskId, mTaskOrganizer)) { + if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) { mTaskOrganizer.applyTransaction(wct); return; } @@ -621,7 +639,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.sendPendingIntent(intent, fillInIntent, options); // If this should be mixed, just send the intent to avoid split handle transition directly. - if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(intent)) { + if (mMixedHandler != null && mMixedHandler.isIntentInPip(intent)) { mTaskOrganizer.applyTransaction(wct); return; } @@ -660,7 +678,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) { @@ -710,16 +728,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, taskId1, taskId2, splitPosition, snapPosition); final WindowContainerTransaction wct = new WindowContainerTransaction(); if (taskId2 == INVALID_TASK_ID) { - if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) { - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); - } - if (mRecentTasks.isPresent()) { - mRecentTasks.get().removeSplitPair(taskId1); - } - options1 = options1 != null ? options1 : new Bundle(); - addActivityOptions(options1, null); - wct.startTask(taskId1, options1); - mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + startSingleTask(taskId1, options1, wct, remoteTransition); return; } @@ -740,11 +749,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, "startIntentAndTask: intent=%s task1=%d position=%d snapPosition=%d", pendingIntent.getIntent(), taskId, splitPosition, snapPosition); final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (taskId == INVALID_TASK_ID) { - options1 = options1 != null ? options1 : new Bundle(); - addActivityOptions(options1, null); - wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent); + boolean secondTaskPipped = mMixedHandler.isTaskInPip(taskId, mTaskOrganizer); + if (taskId == INVALID_TASK_ID || secondTaskPipped) { + startSingleIntent(pendingIntent, fillInIntent, options1, wct, remoteTransition); + return; + } + + if (firstIntentPipped) { + startSingleTask(taskId, options2, wct, remoteTransition); return; } @@ -756,6 +769,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); } + /** + * @param taskId Starts this task in fullscreen, removing it from existing pairs if it was part + * of one. + */ + private void startSingleTask(int taskId, Bundle options, WindowContainerTransaction wct, + RemoteTransition remoteTransition) { + if (mMainStage.containsTask(taskId) || mSideStage.containsTask(taskId)) { + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + if (mRecentTasks.isPresent()) { + mRecentTasks.get().removeSplitPair(taskId); + } + options = options != null ? options : new Bundle(); + addActivityOptions(options, null); + wct.startTask(taskId, options); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } + /** Starts a shortcut and a task to a split pair in one transition. */ void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @@ -843,6 +874,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } + boolean handledForPipSplitLaunch = handlePippedSplitIntentsLaunch( + pendingIntent1, + pendingIntent2, + options1, + options2, + shortcutInfo1, + shortcutInfo2, + wct, + fillInIntent1, + fillInIntent2, + remoteTransition); + if (handledForPipSplitLaunch) { + return; + } + if (!mMainStage.isActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. @@ -877,6 +923,46 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setEnterInstanceId(instanceId); } + /** + * Checks if either of the apps in the desired split launch is currently in Pip. If so, it will + * launch the non-pipped app as a fullscreen app, otherwise no-op. + */ + private boolean handlePippedSplitIntentsLaunch(PendingIntent pendingIntent1, + PendingIntent pendingIntent2, Bundle options1, Bundle options2, + ShortcutInfo shortcutInfo1, ShortcutInfo shortcutInfo2, WindowContainerTransaction wct, + Intent fillInIntent1, Intent fillInIntent2, RemoteTransition remoteTransition) { + // If one of the split apps to start is in Pip, only launch the non-pip app in fullscreen + boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent1); + boolean secondIntentPipped = mMixedHandler.isIntentInPip(pendingIntent2); + if (firstIntentPipped || secondIntentPipped) { + Bundle options = secondIntentPipped ? options1 : options2; + options = options == null ? new Bundle() : options; + addActivityOptions(options, null); + if (shortcutInfo1 != null || shortcutInfo2 != null) { + ShortcutInfo infoToLaunch = secondIntentPipped ? shortcutInfo1 : shortcutInfo2; + wct.startShortcut(mContext.getPackageName(), infoToLaunch, options); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } else { + PendingIntent intentToLaunch = secondIntentPipped ? pendingIntent1 : pendingIntent2; + Intent fillInIntentToLaunch = secondIntentPipped ? fillInIntent1 : fillInIntent2; + startSingleIntent(intentToLaunch, fillInIntentToLaunch, options, wct, + remoteTransition); + } + return true; + } + return false; + } + + /** @param pendingIntent Starts this intent in fullscreen */ + private void startSingleIntent(PendingIntent pendingIntent, Intent fillInIntent, Bundle options, + WindowContainerTransaction wct, + RemoteTransition remoteTransition) { + Bundle optionsToLaunch = options != null ? options : new Bundle(); + addActivityOptions(optionsToLaunch, null); + wct.sendPendingIntent(pendingIntent, fillInIntent, optionsToLaunch); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } + /** Starts a pair of tasks using legacy transition. */ void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, @@ -1213,7 +1299,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 -> { @@ -1234,7 +1320,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; } @@ -1438,6 +1524,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); mSplitTransitions.startDismissTransition(wct, this, mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); + setSplitsVisible(false); } else { exitSplitScreen( mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, @@ -1451,6 +1538,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()); @@ -1470,6 +1558,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", @@ -1547,6 +1636,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. */ @@ -1582,6 +1679,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 @@ -1612,6 +1719,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; @@ -1738,11 +1847,9 @@ 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()); + mSplitLayout.update(null, true /* resetImePosition */); + 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. @@ -1779,13 +1886,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + ActivityOptions options = ActivityOptions.fromBundle(opts); if (launchTarget != null) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token); + options.setLaunchRootTask(launchTarget.mRootTaskInfo.token); } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); + options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + + // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; + // this might have to be changed as more split-to-pip cujs are defined. + options.setDisallowEnterPictureInPictureWhileLaunching(true); + opts.putAll(options.toBundle()); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { @@ -2021,7 +2134,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSkipEvictingMainStageChildren = false; } else { mShowDecorImmediately = true; - mSplitLayout.flingDividerToCenter(); + mSplitLayout.flingDividerToCenter(/*finishCallback*/ null); } }); } @@ -2221,7 +2334,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSkipEvictingMainStageChildren = false; } else { mShowDecorImmediately = true; - mSplitLayout.flingDividerToCenter(); + mSplitLayout.flingDividerToCenter(/*finishCallback*/ null); } }); } @@ -2280,14 +2393,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) { + public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, + boolean shouldUseParallaxEffect) { final SurfaceControl.Transaction t = mTransactionPool.acquire(); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - updateSurfaceBounds(layout, t, true /* applyResizingOffset */); + updateSurfaceBounds(layout, t, shouldUseParallaxEffect); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); - mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); + // TODO (b/307490004): "commonColor" below is a temporary fix to ensure the colors on both + // sides match. When b/307490004 is fixed, this code can be reverted. + float[] commonColor = getResizingBackgroundColor(mSideStage.mRootTaskInfo).getComponents(); + mMainStage.onResizing( + mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately, commonColor); + mSideStage.onResizing( + mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately, commonColor); t.apply(); mTransactionPool.release(t); } @@ -2593,6 +2712,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(); @@ -2636,6 +2762,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // cases above and it is not already visible return null; } else { + if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId + || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " + + "restoring to split", request.getDebugId()); + out = new WindowContainerTransaction(); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); + } if (isOpening && getStageOfTask(triggerTask) != null) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split", request.getDebugId()); @@ -2723,7 +2857,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 @@ -2734,7 +2868,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (mMixedHandler.isEnteringPip(change, transitType)) { - hasEnteringPip = true; + pipChange = change; } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); @@ -2786,9 +2920,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; } @@ -2823,6 +2968,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 @@ -2836,6 +2982,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.update(startTransaction, true /* resetImePosition */); startTransaction.apply(); } + notifySplitAnimationFinished(); return true; } } @@ -2969,7 +3116,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Includes TRANSIT_CHANGE to cover reparenting top-most task to split. mainChild = change; } else if (sideChild == null && stageType == STAGE_TYPE_SIDE - && isOpeningType(change.getMode())) { + && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) { sideChild = change; } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) { // Collect all to back task's and evict them when transition finished. @@ -2980,7 +3127,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, SplitScreenTransitions.EnterSession pendingEnter = mSplitTransitions.mPendingEnter; if (pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { - // Open to side should only be used when split already active and foregorund. + // Open to side should only be used when split already active and foregorund or when + // app is restoring to split from fullscreen. if (mainChild == null && sideChild == null) { Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "Launched a task in split, but didn't receive any task in transition.")); @@ -3009,7 +3157,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pendingEnter.mRemoteHandler.onTransitionConsumed(transition, false /*aborted*/, finishT); } - mSplitUnsupportedToast.show(); + handleUnsupportedSplitStart(); return true; } } @@ -3038,6 +3186,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final TransitionInfo.Change finalMainChild = mainChild; final TransitionInfo.Change finalSideChild = sideChild; enterTransition.setFinishedCallback((callbackWct, callbackT) -> { + if (!enterTransition.mResizeAnim) { + // If resizing, we'll call notify at the end of the resizing animation (below) + notifySplitAnimationFinished(); + } if (finalMainChild != null) { if (!mainNotContainOpenTask) { mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId); @@ -3057,12 +3209,28 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (enterTransition.mResizeAnim) { mShowDecorImmediately = true; - mSplitLayout.flingDividerToCenter(); + mSplitLayout.flingDividerToCenter(this::notifySplitAnimationFinished); } callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false); mPausingTasks.clear(); }); + if (info.getType() == TRANSIT_CHANGE && !isSplitActive() + && pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { + if (finalMainChild != null && finalSideChild == null) { + requestEnterSplitSelect(finalMainChild.getTaskInfo(), + new WindowContainerTransaction(), + getMainStagePosition(), finalMainChild.getStartAbsBounds()); + } else if (finalSideChild != null && finalMainChild == null) { + requestEnterSplitSelect(finalSideChild.getTaskInfo(), + new WindowContainerTransaction(), + getSideStagePosition(), finalSideChild.getStartAbsBounds()); + } else { + throw new IllegalStateException( + "Attempting to restore to split but reparenting change not found"); + } + } + finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); return true; @@ -3323,6 +3491,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"); @@ -3454,6 +3627,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. @@ -3516,7 +3702,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; } @@ -3536,7 +3722,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 f33ab33dafcc..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; @@ -310,10 +314,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, - int offsetY, boolean immediately) { + int offsetY, boolean immediately, float[] veilColor) { if (mSplitDecorManager != null && mRootTaskInfo != null) { mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, - offsetY, immediately); + offsetY, immediately, veilColor); } } 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/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index bec4ba3bf0d1..fa084c585a59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -23,7 +23,6 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.ActivityManager.RunningTaskInfo; 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..a126cbe41b00 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; @@ -29,6 +30,7 @@ import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.graphics.Rect; +import android.gui.TrustedOverlay; import android.os.Binder; import android.util.CloseGuard; import android.util.Slog; @@ -48,6 +50,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 +174,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 +202,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 +227,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 +390,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(); } @@ -402,6 +449,14 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mSurfaceCreated = true; mIsInitialized = true; mSurfaceControl = surfaceControl; + // SurfaceControl is expected to be null only in the case of unit tests. Guard against it + // to avoid runtime exception in SurfaceControl.Transaction. + if (surfaceControl != null) { + // TaskView is meant to contain app activities which shouldn't have trusted overlays + // flag set even when itself reparented in a window which is trusted. + mTransaction.setTrustedOverlay(surfaceControl, TrustedOverlay.DISABLED) + .apply(); + } notifyInitialized(); mShellExecutor.execute(() -> { if (mTaskToken == null) { @@ -597,6 +652,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 +672,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 +698,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 8746b8c8d55c..8ee1efa90a30 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,12 @@ 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 display changes when pip is entering. */ + static final int TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE = 11; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; @@ -116,7 +124,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 +150,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; @@ -228,6 +236,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, // Add after dependencies because it is higher priority shellInit.addInitCallback(() -> { mPipHandler = pipTransitionController; + pipTransitionController.setMixedHandler(this); mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler(); mPlayer.addHandler(this); if (mSplitHandler != null) { @@ -425,7 +434,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Converting mixed transition into a keyguard transition"); // Consume the original mixed transition - onTransitionConsumed(transition, false, null); + mActiveTransitions.remove(mixed); + mixed.onTransitionConsumed(transition, false, null); return true; } else { // Keyguard handler cannot handle it, process through original mixed @@ -482,9 +492,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); @@ -542,6 +554,47 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return true; } + /** + * For example: pip is entering in rotation 0, and then the display changes to rotation 90 + * before the pip transition is ready. So the info contains both the entering pip and display + * change. In this case, the pip can go to the end state in new rotation directly, and let the + * display level animation cover all changed participates. + */ + public void animateEnteringPipWithDisplayChange(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change pipChange, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // In order to play display level animation, force the type to CHANGE (it could be PIP). + final TransitionInfo changeInfo = info.getType() != TRANSIT_CHANGE + ? subCopy(info, TRANSIT_CHANGE, true /* withChanges */) : info; + final MixedTransition mixed = createDefaultMixedTransition( + MixedTransition.TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE, transition); + mActiveTransitions.add(mixed); + mixed.mInFlightSubAnimations = 2; + final Transitions.TransitionFinishCallback finishCB = wct -> { + --mixed.mInFlightSubAnimations; + mixed.joinFinishArgs(wct); + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + finishCallback.onTransitionFinished(mixed.mFinishWCT); + }; + // Perform the display animation first. + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, changeInfo, + startT, finishT, finishCB, mPipHandler); + // Use a standalone finish transaction for pip because it will apply immediately. + final SurfaceControl.Transaction pipFinishT = new SurfaceControl.Transaction(); + mPipHandler.startEnterAnimation(pipChange, startT, pipFinishT, wct -> { + // Apply immediately to avoid potential flickering by bounds change at the end of + // display animation. + mPipHandler.applyTransaction(wct); + finishCB.onTransitionFinished(null /* wct */); + }); + // Jump to the pip end state directly and make sure the real finishT have the latest state. + mPipHandler.end(); + mPipHandler.syncPipSurfaceState(info, startT, finishT); + } + private static boolean animateKeyguard(@NonNull final MixedTransition mixed, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @@ -562,22 +615,23 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, /** Use to when split use intent to enter, check if this enter transition should be mixed or * not.*/ - public boolean shouldSplitEnterMixed(PendingIntent intent) { + public boolean isIntentInPip(PendingIntent intent) { // Check if this intent package is same as pip one or not, if true we want let the pip // task enter split. if (mPipHandler != null) { - return mPipHandler.isInPipPackage(SplitScreenUtils.getPackageName(intent.getIntent())); + return mPipHandler + .isPackageActiveInPip(SplitScreenUtils.getPackageName(intent.getIntent())); } return false; } /** Use to when split use taskId to enter, check if this enter transition should be mixed or * not.*/ - public boolean shouldSplitEnterMixed(int taskId, ShellTaskOrganizer shellTaskOrganizer) { + public boolean isTaskInPip(int taskId, ShellTaskOrganizer shellTaskOrganizer) { // Check if this intent package is same as pip one or not, if true we want let the pip // task enter split. if (mPipHandler != null) { - return mPipHandler.isInPipPackage( + return mPipHandler.isPackageActiveInPip( SplitScreenUtils.getPackageName(taskId, shellTaskOrganizer)); } return false; 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..c33fb80fdefc 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) { @@ -70,13 +70,18 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { return switch (mType) { - case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false; + case TYPE_DISPLAY_AND_SPLIT_CHANGE, TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE -> false; case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING -> animateEnterPipFromActivityEmbedding( 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); @@ -248,6 +253,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull Transitions.TransitionFinishCallback finishCallback) { switch (mType) { case TYPE_DISPLAY_AND_SPLIT_CHANGE: + case TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE: // queue since no actual animation. return; case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: 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 74e85f8dd468..018c9044e2f7 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 @@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; @@ -507,6 +508,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Point animRelOffset = new Point( change.getEndAbsBounds().left - animRoot.getOffset().x, change.getEndAbsBounds().top - animRoot.getOffset().y); + + if (change.getActivityComponent() != null) { + // For appcompat letterbox: we intentionally report the task-bounds so that we + // can animate as-if letterboxes are "part of" the activity. This means we can't + // always rely solely on endAbsBounds and need to also max with endRelOffset. + animRelOffset.x = Math.max(animRelOffset.x, change.getEndRelOffset().x); + animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y); + } + if (change.getActivityComponent() != null && !isActivityLevel) { // At this point, this is an independent activity change in a non-activity // transition. This means that an activity transition got erroneously combined @@ -534,7 +544,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, clipRect); - if (info.getAnimationOptions() != null) { + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = info.getAnimationOptions(); + } else { + options = change.getAnimationOptions(); + } + if (options != null) { attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), cornerRadius); } @@ -585,7 +601,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 @@ -596,8 +611,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); @@ -715,7 +732,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final boolean isOpeningType = TransitionUtil.isOpeningType(type); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } final int overrideType = options != null ? options.getType() : ANIM_NONE; final Rect endBounds = TransitionUtil.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) 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..9b27e413b5e4 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) { @@ -130,5 +133,9 @@ public class HomeTransitionObserver implements TransitionObserver, */ public void invalidate(Transitions transitions) { transitions.unregisterObserver(this); + if (mListener != null) { + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); + } } } 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 4ea71490798c..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); } } @@ -212,6 +213,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { switch (mType) { case TYPE_RECENTS_DURING_DESKTOP: case TYPE_RECENTS_DURING_SPLIT: + case TYPE_RECENTS_DURING_KEYGUARD: mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); break; default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/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/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 9ce22094d56b..e196254628d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -167,7 +167,7 @@ class ScreenRotationAnimation { t.show(mScreenshotLayer); if (!isCustomRotate()) { mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, - screenshotBuffer.getColorSpace()); + screenshotBuffer.getColorSpace(), mSurfaceControl); } hardwareBuffer.close(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 1be85d05c16e..2047b5a88604 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -55,6 +55,7 @@ import android.window.TransitionInfo; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; @@ -71,7 +72,12 @@ public class TransitionAnimationHelper { final int changeFlags = change.getFlags(); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } final int overrideType = options != null ? options.getType() : ANIM_NONE; int animAttr = 0; boolean translucent = false; @@ -246,7 +252,7 @@ public class TransitionAnimationHelper { if (!a.getShowBackdrop()) { return defaultColor; } - if (info.getAnimationOptions() != null + if (!Flags.moveAnimationOptionsToChange() && info.getAnimationOptions() != null && info.getAnimationOptions().getBackgroundColor() != 0) { // If available use the background color provided through AnimationOptions return info.getAnimationOptions().getBackgroundColor(); @@ -280,6 +286,7 @@ public class TransitionAnimationHelper { .setParent(rootLeash) .setColorLayer() .setOpaque(true) + .setCallsite("TransitionAnimationHelper.addBackgroundToTransition") .build(); startTransaction .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) 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 888105d4a20a..f257e207673d 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,11 +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.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; @@ -43,25 +42,30 @@ 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; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; +import android.util.ArrayMap; import android.util.Log; import android.util.Pair; 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); @@ -160,9 +166,6 @@ public class Transitions implements RemoteCallable<Transitions>, public static final int TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 11; - /** Transition type to fullscreen from desktop mode. */ - public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12; - /** Transition type to cancel the drag to desktop mode. */ public static final int TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 13; @@ -171,12 +174,20 @@ public class Transitions implements RemoteCallable<Transitions>, public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 14; - /** Transition to animate task to desktop. */ - public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15; - /** 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; + + /** Transition type for desktop mode transitions. */ + public static final int TRANSIT_DESKTOP_MODE_TYPES = + WindowManager.TRANSIT_FIRST_CUSTOM + 100; + private final ShellTaskOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; @@ -216,7 +227,8 @@ public class Transitions implements RemoteCallable<Transitions>, private boolean mDisableForceSync = false; private static final class ActiveTransition { - IBinder mToken; + final IBinder mToken; + TransitionHandler mHandler; boolean mAborted; TransitionInfo mInfo; @@ -226,6 +238,10 @@ public class Transitions implements RemoteCallable<Transitions>, /** Ordered list of transitions which have been merged into this one. */ private ArrayList<ActiveTransition> mMerged; + ActiveTransition(IBinder token) { + mToken = token; + } + boolean isSync() { return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0; } @@ -255,6 +271,9 @@ public class Transitions implements RemoteCallable<Transitions>, } } + /** All transitions that we have created, but not yet finished. */ + private final ArrayMap<IBinder, ActiveTransition> mKnownTransitions = new ArrayMap<>(); + /** Keeps track of transitions which have been started, but aren't ready yet. */ private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>(); @@ -416,12 +435,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 +526,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 +538,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 +572,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) { @@ -629,8 +663,10 @@ public class Transitions implements RemoteCallable<Transitions>, } if (change.hasFlags(FLAG_NO_ANIMATION)) { hasNoAnimation = true; - } else { - // at-least one relevant participant *is* animated, so we need to animate. + } else if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) { + // Ignore the order only or occluded changes since they shouldn't be visible during + // animation. For anything else, we need to animate if at-least one relevant + // participant *is* animated, return false; } } @@ -662,7 +698,7 @@ public class Transitions implements RemoteCallable<Transitions>, info.getDebugId(), transitionToken, info); int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { - final ActiveTransition existing = getKnownTransition(transitionToken); + final ActiveTransition existing = mKnownTransitions.get(transitionToken); if (existing != null) { Log.e(TAG, "Got duplicate transitionReady for " + transitionToken); // The transition is already somewhere else in the pipeline, so just return here. @@ -677,8 +713,8 @@ public class Transitions implements RemoteCallable<Transitions>, + transitionToken + ". expecting one of " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); - final ActiveTransition fallback = new ActiveTransition(); - fallback.mToken = transitionToken; + final ActiveTransition fallback = new ActiveTransition(transitionToken); + mKnownTransitions.put(transitionToken, fallback); mPendingTransitions.add(fallback); activeIdx = mPendingTransitions.size() - 1; } @@ -718,7 +754,7 @@ public class Transitions implements RemoteCallable<Transitions>, // Sleep starts a process of forcing all prior transitions to finish immediately ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sync track %d", i); - finishForSync(active, i, null /* forceFinish */); + finishForSync(active.mToken, i, null /* forceFinish */); } if (hadPreceding) { return false; @@ -836,6 +872,7 @@ public class Transitions implements RemoteCallable<Transitions>, } else if (mPendingTransitions.isEmpty()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + "animations finished"); + mKnownTransitions.clear(); // Run all runnables from the run-when-idle queue. for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { mRunWhenIdleQueue.get(i).run(); @@ -856,7 +893,7 @@ public class Transitions implements RemoteCallable<Transitions>, ready.mStartT.apply(); } // finish now since there's nothing to animate. Calls back into processReadyQueue - onFinish(ready, null); + onFinish(ready.mToken, null); return; } playTransition(ready); @@ -915,8 +952,10 @@ public class Transitions implements RemoteCallable<Transitions>, private void playTransition(@NonNull ActiveTransition active) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active); + final var token = active.mToken; + for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionStarting(active.mToken); + mObservers.get(i).onTransitionStarting(token); } setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT); @@ -925,8 +964,8 @@ public class Transitions implements RemoteCallable<Transitions>, if (active.mHandler != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s", active.mHandler); - boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo, - active.mStartT, active.mFinishT, (wct) -> onFinish(active, wct)); + boolean consumed = active.mHandler.startAnimation(token, active.mInfo, + active.mStartT, active.mFinishT, (wct) -> onFinish(token, wct)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); @@ -934,8 +973,8 @@ public class Transitions implements RemoteCallable<Transitions>, } } // Otherwise give every other handler a chance - active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT, - active.mFinishT, (wct) -> onFinish(active, wct), active.mHandler); + active.mHandler = dispatchTransition(token, active.mInfo, active.mStartT, + active.mFinishT, (wct) -> onFinish(token, wct), active.mHandler); } /** @@ -1011,10 +1050,15 @@ public class Transitions implements RemoteCallable<Transitions>, info.releaseAnimSurfaces(); } - private void onFinish(ActiveTransition active, + private void onFinish(IBinder token, @Nullable WindowContainerTransaction wct) { + final ActiveTransition active = mKnownTransitions.get(token); + if (active == null) { + Log.e(TAG, "Trying to finish a non-existent transition: " + token); + return; + } final Track track = mTracks.get(active.getTrack()); - if (track.mActiveTransition != active) { + if (track == null || track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " + " a handler didn't properly deal with a merge. " + active, new RuntimeException()); @@ -1067,54 +1111,25 @@ public class Transitions implements RemoteCallable<Transitions>, ActiveTransition merged = active.mMerged.get(iM); mOrganizer.finishTransition(merged.mToken, null /* wct */); releaseSurfaces(merged.mInfo); + mKnownTransitions.remove(merged.mToken); } active.mMerged.clear(); } + mKnownTransitions.remove(token); // Now that this is done, check the ready queue for more work. processReadyQueue(track); } - /** - * Checks to see if the transition specified by `token` is already known. If so, it will be - * returned. - */ - @Nullable - private ActiveTransition getKnownTransition(IBinder token) { - for (int i = 0; i < mPendingTransitions.size(); ++i) { - final ActiveTransition active = mPendingTransitions.get(i); - if (active.mToken == token) return active; - } - for (int i = 0; i < mReadyDuringSync.size(); ++i) { - final ActiveTransition active = mReadyDuringSync.get(i); - if (active.mToken == token) return active; - } - for (int t = 0; t < mTracks.size(); ++t) { - final Track tr = mTracks.get(t); - for (int i = 0; i < tr.mReadyTransitions.size(); ++i) { - final ActiveTransition active = tr.mReadyTransitions.get(i); - if (active.mToken == token) return active; - } - final ActiveTransition active = tr.mActiveTransition; - if (active == null) continue; - if (active.mToken == token) return active; - if (active.mMerged == null) continue; - for (int m = 0; m < active.mMerged.size(); ++m) { - final ActiveTransition merged = active.mMerged.get(m); - if (merged.mToken == token) return merged; - } - } - return null; - } - void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s", request.getDebugId(), transitionToken, request); - if (getKnownTransition(transitionToken) != null) { + if (mKnownTransitions.containsKey(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } - final ActiveTransition active = new ActiveTransition(); + final ActiveTransition active = new ActiveTransition(transitionToken); + mKnownTransitions.put(transitionToken, active); WindowContainerTransaction wct = null; // If we have sleep, we use a special handler and we try to finish everything ASAP. @@ -1154,7 +1169,6 @@ public class Transitions implements RemoteCallable<Transitions>, wct.setBounds(request.getTriggerTask().token, null); } mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); - active.mToken = transitionToken; // Currently, WMCore only does one transition at a time. If it makes a requestStart, it // is already collecting that transition on core-side, so it will be the next one to // become ready. There may already be pending transitions added as part of direct @@ -1173,14 +1187,38 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition " + "type=%d wct=%s handler=%s", type, wct, handler); - final ActiveTransition active = new ActiveTransition(); + final ActiveTransition active = + new ActiveTransition(mOrganizer.startNewTransition(type, wct)); active.mHandler = handler; - active.mToken = mOrganizer.startNewTransition(type, wct); + mKnownTransitions.put(active.mToken, active); mPendingTransitions.add(active); return active.mToken; } /** + * 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). @@ -1192,14 +1230,14 @@ public class Transitions implements RemoteCallable<Transitions>, * * This is then repeated until there are no more pending sleep transitions. * - * @param reason The SLEEP transition that triggered this round of finishes. We will continue - * looping round finishing transitions as long as this is still waiting. + * @param reason The token for the SLEEP transition that triggered this round of finishes. + * We will continue looping round finishing transitions until this is ready. * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge * signal to -- so it will be force-finished if it's still running. */ - private void finishForSync(ActiveTransition reason, + private void finishForSync(IBinder reason, int trackIdx, @Nullable ActiveTransition forceFinish) { - if (getKnownTransition(reason.mToken) == null) { + if (!mKnownTransitions.containsKey(reason)) { Log.d(TAG, "finishForSleep: already played sync transition " + reason); return; } @@ -1219,7 +1257,7 @@ public class Transitions implements RemoteCallable<Transitions>, forceFinish.mHandler.onTransitionConsumed( forceFinish.mToken, true /* aborted */, null /* finishTransaction */); } - onFinish(forceFinish, null); + onFinish(forceFinish.mToken, null); } } if (track.isIdle() || mReadyDuringSync.isEmpty()) { @@ -1322,6 +1360,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 +1486,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 +1507,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 +1550,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 +1651,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 +1705,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 ed4ae05cb2c9..456658c54fd0 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,6 +16,8 @@ package com.android.wm.shell.transition.tracing; +import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP; + import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping; import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings; import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition; @@ -47,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_DROP) + .build(); + mDataSource.register(params); } /** @@ -211,8 +218,6 @@ public class PerfettoTransitionTracer implements TransitionTracer { } os.end(mappingsToken); - - ctx.flush(); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index c26604a84a61..7c2ba455c0c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -293,7 +293,13 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @Override public void onFoldStateChanged(boolean isFolded) { if (isFolded) { + // Reset unfold animation finished flag on folding, so it could be used next time + // when we unfold the device as an indication that animation hasn't finished yet mAnimationFinished = false; + + // If we are currently animating unfold animation we should finish it because + // the animation might not start and finish as the device was folded + finishTransitionIfNeeded(); } } 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..95e0d79c212e 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); @@ -205,6 +255,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final WindowContainerToken mTaskToken; private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; + private final int mDisplayId; private int mDragPointerId = -1; private boolean mIsDragging; @@ -216,6 +267,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; mDragDetector = new DragDetector(this); + mDisplayId = taskInfo.displayId; } @Override @@ -224,12 +276,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (id == R.id.close_window) { mTaskOperations.closeTask(mTaskToken); } else if (id == R.id.back_button) { - mTaskOperations.injectBackKey(); + mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.minimize_window) { 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 +341,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 +358,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..d0ca5b0fdce6 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; @@ -67,9 +74,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, - taskInfo.getConfiguration()); - + super(context, displayController, taskOrganizer, taskInfo, taskSurface); mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; @@ -87,13 +92,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 +198,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 +225,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mHandler, mChoreographer, mDisplay.getDisplayId(), - 0 /* taskCornerRadius */, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -230,12 +236,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..21b6db29143a 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; @@ -79,11 +83,14 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; 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; @@ -96,6 +103,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionReg import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import java.io.PrintWriter; +import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -119,9 +127,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 +136,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; @@ -147,6 +153,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DisplayInsetsController mDisplayInsetsController; private final Region mExclusionRegion = Region.obtain(); private boolean mInImmersiveMode; + private final String mSysUIPackageName; private final ISystemGestureExclusionListener mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { @@ -197,7 +204,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + new SparseArray<>()); } @VisibleForTesting @@ -219,7 +227,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 +240,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 +248,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTransactionFactory = transactionFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mInputManager = mContext.getSystemService(InputManager.class); + mWindowDecorByTaskId = windowDecorByTaskId; + mSysUIPackageName = mContext.getResources().getString( + com.android.internal.R.string.config_systemUi); shellInit.addInitCallback(this::onInit, this); } @@ -248,8 +260,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,11 +281,10 @@ 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)) { + mDesktopTasksController.moveToSplit(decor.mTaskInfo); } } } @@ -305,6 +316,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 +367,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 +375,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; @@ -361,6 +391,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; private final GestureDetector mGestureDetector; + private final int mDisplayId; /** * Whether to pilfer the next motion event to send cancellations to the windows below. @@ -382,6 +413,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPositioningCallback = dragPositioningCallback; mDragDetector = new DragDetector(this); mGestureDetector = new GestureDetector(mContext, this); + mDisplayId = taskInfo.displayId; mCloseMaximizeWindowRunnable = () -> { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); if (decoration == null) return; @@ -402,25 +434,26 @@ 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(); + mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(); + decoration.createHandleMenu(mSplitScreenController); } else { 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, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); decoration.closeHandleMenu(); } else if (id == R.id.fullscreen_button) { decoration.closeHandleMenu(); @@ -428,42 +461,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(mTaskId, SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { - mDesktopTasksController.ifPresent(c -> - c.moveToFullscreen(mTaskId)); + mDesktopTasksController.moveToFullscreen(mTaskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); } } 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 +518,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 +531,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,17 +582,28 @@ 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); + mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } else if (ev.getAction() == ACTION_HOVER_EXIT) { if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window || id == R.id.maximize_menu) { + } else if (id == R.id.maximize_window + || MaximizeMenu.Companion.isMaximizeMenuView(id)) { // Close menu if not hovering over maximize menu or maximize button after a // delay to give user a chance to re-enter view or to move from one maximize // menu view to another. mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); + if (id != R.id.maximize_window) { + decoration.onMaximizeMenuHoverExit(id, ev); + } } return true; } @@ -572,7 +612,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 +624,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 +646,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 +656,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 +681,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 +711,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 +801,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 +810,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 +830,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 +847,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 +944,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 +1039,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && taskInfo.isFocused) { return false; } - return DesktopModeStatus.isEnabled() + // TODO(b/347289970): Consider replacing with API + if (Flags.enableDesktopWindowingModalsPolicy() + && isSingleTopActivityTranslucent(taskInfo)) { + return false; + } + if (isSystemUIApplication(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 +1076,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); } @@ -1122,10 +1117,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mSplitScreenController.isTaskInSplitScreen(taskId); } + // TODO(b/347289970): Consider replacing with API + private boolean isSystemUIApplication(RunningTaskInfo taskInfo) { + if (taskInfo.baseActivity != null) { + return (Objects.equals(taskInfo.baseActivity.getPackageName(), mSysUIPackageName)); + } + return false; + } + 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 +1185,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 +1229,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..4d597cac889e 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; @@ -48,18 +60,19 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; 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.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.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; +import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; +import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder; import kotlin.Unit; @@ -78,7 +91,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; - private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder; + private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; private View.OnTouchListener mOnCaptionTouchListener; private View.OnLongClickListener mOnCaptionLongClickListener; @@ -86,10 +99,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; - + private Runnable mCurrentViewHostRunnable = null; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); + private final Runnable mViewHostRunnable = + () -> updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mResult); private final Point mPositionInParent = new Point(); private HandleMenu mHandleMenu; @@ -97,9 +112,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; @@ -112,12 +127,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowDecorConfig, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - this (context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + this (context, displayController, taskOrganizer, taskInfo, taskSurface, handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, @@ -130,7 +144,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowDecorConfig, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -140,17 +153,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, SurfaceControlViewHostFactory surfaceControlViewHostFactory) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + super(context, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, surfaceControlViewHostFactory); - mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; - - loadAppInfo(); } void setCaptionListeners( @@ -186,16 +196,88 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // position and crop are set. final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled() && mTaskDragResizer.isResizingOrAnimating(); - // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is - // synced with the buffer transaction (that draws the View). Both will be shown on screen - // at the same, whereas applying them independently causes flickering. See b/270202228. - relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskPositionAndCrop); + // For headers only (i.e. in freeform): use |applyStartTransactionOnDraw| so that the + // transaction (that applies task crop) is synced with the buffer transaction (that draws + // the View). Both will be shown on screen at the same, whereas applying them independently + // causes flickering. See b/270202228. + final boolean applyTransactionOnDraw = + taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop); + if (!applyTransactionOnDraw) { + t.apply(); + } } void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + Trace.beginSection("DesktopModeWindowDecoration#relayout"); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + // The Task is in Freeform mode -> show its header in sync since it's an integral part + // of the window itself - a delayed header might cause bad UX. + relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + } else { + // The Task is outside Freeform mode -> allow the handle view to be delayed since the + // handle is just a small addition to the window. + relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + } + Trace.endSection(); + } + + /** Run the whole relayout phase immediately without delay. */ + private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + // Clear the current ViewHost runnable as we will update the ViewHost here + clearCurrentViewHostRunnable(); + updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + if (mResult.mRootView != null) { + updateViewHost(mRelayoutParams, startT, mResult); + } + } + + /** + * Clear the current ViewHost runnable - to ensure it doesn't run once relayout params have been + * updated. + */ + private void clearCurrentViewHostRunnable() { + if (mCurrentViewHostRunnable != null) { + mHandler.removeCallbacks(mCurrentViewHostRunnable); + mCurrentViewHostRunnable = null; + } + } + + /** + * Relayout the window decoration but repost some of the work, to unblock the current callstack. + */ + private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + if (applyStartTransactionOnDraw) { + throw new IllegalArgumentException( + "We cannot both sync viewhost ondraw and delay viewhost creation."); + } + // Clear the current ViewHost runnable as we will update the ViewHost here + clearCurrentViewHostRunnable(); + updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, + false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop); + 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. + return; + } + // Store the current runnable so it can be removed if we start a new relayout. + mCurrentViewHostRunnable = mViewHostRunnable; + mHandler.post(mCurrentViewHostRunnable); + } + + private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); if (isHandleMenuActive()) { mHandleMenu.relayout(startT); } @@ -207,54 +289,41 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + Trace.beginSection("DesktopModeWindowDecoration#relayout-updateViewsAndSurfaces"); + updateViewsAndSurfaces(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#updateRelayoutParamsAndSurfaces return; } + if (oldRootView != mResult.mRootView) { - if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) { - mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder( - mResult.mRootView, - mOnCaptionTouchListener, - mOnCaptionButtonClickListener - ); - } else if (mRelayoutParams.mLayoutResId - == R.layout.desktop_mode_app_controls_window_decor) { - mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( - mResult.mRootView, - mOnCaptionTouchListener, - mOnCaptionButtonClickListener, - mOnCaptionLongClickListener, - mOnCaptionGenericMotionListener, - mAppName, - mAppIconBitmap, - () -> { - if (!isMaximizeMenuActive()) { - createMaximizeMenu(); - } - return Unit.INSTANCE; - }); - } else { - throw new IllegalArgumentException("Unexpected layout resource id"); - } + mWindowDecorViewHolder = createViewHolder(); } + Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); mWindowDecorViewHolder.bindData(mTaskInfo); + Trace.endSection(); if (!mTaskInfo.isFocused) { closeHandleMenu(); closeMaximizeMenu(); } - final boolean isFreeform = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; - if (!isDragResizeable) { + updateDragResizeListener(oldDecorationSurface); + updateMaximizeMenu(startT); + Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces + } + + private void updateDragResizeListener(SurfaceControl oldDecorationSurface) { + if (!isDragResizable(mTaskInfo)) { if (!mTaskInfo.positionInParent.equals(mPositionInParent)) { // We still want to track caption bar's exclusion region on a non-resizeable task. updateExclusionRegion(); @@ -265,43 +334,79 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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(); } + } + + private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) { + final boolean isFreeform = + taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + return isFreeform && taskInfo.isResizeable; + } + + private void updateMaximizeMenu(SurfaceControl.Transaction startT) { + if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) { + return; + } + if (!mTaskInfo.isVisible()) { + closeMaximizeMenu(); + } else { + mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); + } + } - if (isMaximizeMenuActive()) { - if (!mTaskInfo.isVisible()) { - closeMaximizeMenu(); - } else { - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); - } + private WindowDecorationViewHolder createViewHolder() { + if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) { + return new AppHandleViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener + ); + } else if (mRelayoutParams.mLayoutResId + == R.layout.desktop_mode_app_header) { + loadAppInfoIfNeeded(); + return new AppHeaderViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener, + mOnCaptionLongClickListener, + mOnCaptionGenericMotionListener, + mAppName, + mAppIconBitmap, + () -> { + if (!isMaximizeMenuActive()) { + createMaximizeMenu(); + } + return Unit.INSTANCE; + }); } + throw new IllegalArgumentException("Unexpected layout resource id"); } @VisibleForTesting @@ -312,17 +417,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); + final boolean isAppHeader = + captionLayoutId == R.layout.desktop_mode_app_header; + final boolean isAppHandle = captionLayoutId == R.layout.desktop_mode_app_handle; relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = captionLayoutId; 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 (isAppHeader) { + 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 +447,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 (isAppHandle) { + // 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 @@ -345,19 +460,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop; - // The configuration used to lay out the window decoration. The system context's config is - // used when the task density has been overridden to a custom density so that the resources - // and views of the decoration aren't affected and match the rest of the System UI, if not - // then just use the task's configuration. A copy is made instead of using the original - // reference so that the configuration isn't mutated on config changes and diff checks can - // be made in WindowDecoration#relayout using the pre/post-relayout configuration. - // See b/301119301. + + // The configuration used to layout the window decoration. A copy is made instead of using + // the original reference so that the configuration isn't mutated on config changes and + // diff checks can be made in WindowDecoration#relayout using the pre/post-relayout + // configuration. See b/301119301. // TODO(b/301119301): consider moving the config data needed for diffs to relayout params // instead of using a whole Configuration as a parameter. final Configuration windowDecorConfig = new Configuration(); - windowDecorConfig.setTo(DesktopTasksController.isDesktopDensityOverrideSet() - ? context.getResources().getConfiguration() // Use system context. - : taskInfo.configuration); // Use task configuration. + if (Flags.enableAppHeaderWithTaskDensity() && isAppHeader) { + // Should match the density of the task. The task may have had its density overridden + // to be different that SysUI's. + windowDecorConfig.setTo(taskInfo.configuration); + } else if (DesktopModeStatus.useDesktopOverrideDensity()) { + // The task has had its density overridden, but keep using the system's density to + // layout the header. + windowDecorConfig.setTo(context.getResources().getConfiguration()); + } else { + windowDecorConfig.setTo(taskInfo.configuration); + } relayoutParams.mWindowDecorConfig = windowDecorConfig; if (DesktopModeStatus.useRoundedCorners()) { @@ -371,7 +492,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * resource. Otherwise, return ID_NULL and caption width be set to task width. */ private static int getCaptionWidthId(int layoutResId) { - if (layoutResId == R.layout.desktop_mode_focused_window_decor) { + if (layoutResId == R.layout.desktop_mode_app_handle) { return R.dimen.desktop_mode_fullscreen_decor_caption_width; } return Resources.ID_NULL; @@ -399,7 +520,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 +541,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,23 +599,27 @@ 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, + mTaskSurface, mSurfaceControlTransactionSupplier, mTaskInfo); } /** * Show the resize veil. */ public void showResizeVeil(Rect taskBounds) { - mResizeVeil.showVeil(mTaskSurface, taskBounds); + createResizeVeilIfNeeded(); + mResizeVeil.showVeil(mTaskSurface, taskBounds, mTaskInfo); } /** * Show the resize veil. */ public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) { - mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */); + createResizeVeilIfNeeded(); + mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, mTaskInfo, false /* fadeIn */); } /** @@ -498,8 +653,9 @@ 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) + final int appTextWidth = ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppNameTextWidth(); final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(), R.dimen.desktop_mode_app_details_width_minus_text) + appTextWidth; @@ -581,15 +737,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** * Create and display handle menu window. */ - void createHandleMenu() { + void createHandleMenu(SplitScreenController splitScreenController) { + 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) + .setDisplayController(mDisplayController) + .setSplitScreenController(splitScreenController) .build(); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); @@ -606,10 +765,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } @Override - void releaseViews() { + void releaseViews(WindowContainerTransaction wct) { closeHandleMenu(); closeMaximizeMenu(); - super.releaseViews(); + super.releaseViews(wct); } /** @@ -677,7 +836,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { if (isHandleMenuActive() || !(mWindowDecorViewHolder - instanceof DesktopModeFocusedWindowDecorationViewHolder)) { + instanceof AppHandleViewHolder)) { return false; } @@ -706,27 +865,54 @@ 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() && !isHandleMenuAboveStatusBar()) { + mHandleMenu.checkMotionEvent(ev); + } + } + + private boolean isHandleMenuAboveStatusBar() { + return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform(); + } + private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -738,13 +924,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); + clearCurrentViewHostRunnable(); super.close(); } private static int getDesktopModeWindowDecorLayoutId(@WindowingMode int windowingMode) { return windowingMode == WINDOWING_MODE_FREEFORM - ? R.layout.desktop_mode_app_controls_window_decor - : R.layout.desktop_mode_focused_window_decor; + ? R.layout.desktop_mode_app_header + : R.layout.desktop_mode_app_handle; } private void updatePositionInParent() { @@ -764,7 +951,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(); @@ -775,6 +962,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return exclusionRegion; } + int getCaptionX() { + return mResult.mCaptionX; + } + @Override int getCaptionHeightId(@WindowingMode int windowingMode) { return getCaptionHeightIdStatic(windowingMode); @@ -796,21 +987,39 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } void setAnimatingTaskResize(boolean animatingTaskResize) { - if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) return; - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return; + ((AppHeaderViewHolder) mWindowDecorViewHolder) .setAnimatingTaskResize(animatingTaskResize); } + /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeWindowHoverExit() { - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverExit(); } + /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ void onMaximizeWindowHoverEnter() { - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + ((AppHeaderViewHolder) 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 "{" @@ -833,17 +1042,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - final Configuration windowDecorConfig = - DesktopTasksController.isDesktopDensityOverrideSet() - ? context.getResources().getConfiguration() // Use system context - : taskInfo.configuration; // Use task configuration return new DesktopModeWindowDecoration( context, displayController, taskOrganizer, taskInfo, taskSurface, - windowDecorConfig, handler, choreographer, syncQueue, 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..fe1c9c3cce66 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 @@ -22,12 +22,16 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; +import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.SurfaceControl; +import com.android.window.flags.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.DesktopModeStatus; /** * Utility class that contains logic common to classes implementing {@link DragPositioningCallback} @@ -35,11 +39,11 @@ import com.android.wm.shell.common.DisplayController; * and applying that change to the task bounds when applicable. */ public class DragPositioningCallbackUtility { - /** * Determine the delta between input's current point and the input start point. - * @param inputX current input x coordinate - * @param inputY current input y coordinate + * + * @param inputX current input x coordinate + * @param inputY current input y coordinate * @param repositionStartPoint initial input coordinate * @return delta between these two points */ @@ -52,13 +56,14 @@ public class DragPositioningCallbackUtility { /** * Based on type of resize and delta provided, calculate the new bounds to display for this * task. - * @param ctrlType type of drag being performed - * @param repositionTaskBounds the bounds the task is being repositioned to + * + * @param ctrlType type of drag being performed + * @param repositionTaskBounds the bounds the task is being repositioned to * @param taskBoundsAtDragStart the bounds of the task on the first drag input event - * @param stableBounds bounds that represent the resize limit of this task - * @param delta difference between start input and current input in x/y coordinates - * @param displayController task's display controller - * @param windowDecoration window decoration of the task being dragged + * @param stableBounds bounds that represent the resize limit of this task + * @param delta difference between start input and current input in x/y + * coordinates + * @param windowDecoration window decoration of the task being dragged * @return whether this method changed repositionTaskBounds */ static boolean changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, @@ -131,7 +136,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,65 +145,69 @@ public class DragPositioningCallbackUtility { } /** - * Calculates the new position of the top edge of the task and returns true if it 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 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 + * @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, WindowDecoration windowDecoration) { - return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController, + return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController, windowDecoration) : windowDecoration.mTaskInfo.minWidth; } private static float getMinHeight(DisplayController displayController, WindowDecoration windowDecoration) { - return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize(displayController, + return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinHeight(displayController, windowDecoration) : windowDecoration.mTaskInfo.minHeight; } + private static float getDefaultMinWidth(DisplayController displayController, + WindowDecoration windowDecoration) { + if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { + return WindowDecoration.loadDimensionPixelSize( + windowDecoration.mDecorWindowContext.getResources(), + R.dimen.desktop_mode_minimum_window_width); + } + return getDefaultMinSize(displayController, windowDecoration); + } + + private static float getDefaultMinHeight(DisplayController displayController, + WindowDecoration windowDecoration) { + if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { + return WindowDecoration.loadDimensionPixelSize( + windowDecoration.mDecorWindowContext.getResources(), + R.dimen.desktop_mode_minimum_window_height); + } + return getDefaultMinSize(displayController, windowDecoration); + } + private static float getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration) { float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) @@ -206,9 +215,15 @@ public class DragPositioningCallbackUtility { return windowDecoration.mTaskInfo.defaultMinSize * density; } + private static boolean isSizeConstraintForDesktopModeEnabled(Context context) { + return DesktopModeStatus.canEnterDesktopMode(context) + && Flags.enableDesktopWindowingSizeConstraints(); + } + interface DragStartListener { /** * Inform the implementing class that a drag resize has started + * * @param taskId id of this positioner's {@link WindowDecoration} */ void onDragStart(int taskId); 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..d902444d4b15 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 @@ -16,7 +16,6 @@ package com.android.wm.shell.windowdecor; -import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; @@ -24,13 +23,17 @@ 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 static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen; +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 +41,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 +55,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 +71,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 +92,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,23 +115,25 @@ 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) .setContainerLayer() .setParent(mDecorationSurface) + .setCallsite("DragResizeInputListener.constructor") .build(); mSurfaceControlTransactionSupplier.get() .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) @@ -170,7 +152,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 +163,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 +197,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 +212,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 +239,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 +268,40 @@ 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) { + // The id of the particular pointer in a MotionEvent that we are listening to for drag + // resize events. For example, if multiple fingers are touching the screen, then each one + // has a separate pointer id, but we only accept drag input from one. + 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 +314,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 +393,31 @@ 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; 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, + new Point() /* offset */); if (mShouldHandleEvents) { + // Save the id of the pointer for this drag interaction; we will use the + // same pointer for all subsequent MotionEvents in this interaction. 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); + final int ctrlType = mDragResizeWindowGeometry.calculateCtrlType( + isEventFromTouchscreen(e), isEdgeResizePermitted(e), 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; } @@ -437,9 +426,16 @@ class DragResizeInputListener implements AutoCloseable { break; } mInputManager.pilferPointers(mInputChannel.getToken()); - int dragPointerIndex = e.findPointerIndex(mDragPointerId); - float rawX = e.getRawX(dragPointerIndex); - float rawY = e.getRawY(dragPointerIndex); + final int dragPointerIndex = e.findPointerIndex(mDragPointerId); + if (dragPointerIndex < 0) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action move, but ignore event due to invalid " + + "pointer index", + TAG); + break; + } + final float rawX = e.getRawX(dragPointerIndex); + final float rawY = e.getRawY(dragPointerIndex); final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY); updateInputSinkRegionForDrag(taskBounds); result = true; @@ -447,15 +443,21 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - mInputManager.pilferPointers(mInputChannel.getToken()); if (mShouldHandleEvents) { - int dragPointerIndex = e.findPointerIndex(mDragPointerId); + final int dragPointerIndex = e.findPointerIndex(mDragPointerId); + if (dragPointerIndex < 0) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action %d, but ignore event due to invalid " + + "pointer index", + TAG, e.getActionMasked()); + break; + } final Rect taskBounds = mCallback.onDragPositioningEnd( e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); // 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 +482,23 @@ 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); + // Since we are handling cursor, we know that this is not a touchscreen event, and + // that edge resizing should always be allowed. + @DragPositioningCallback.CtrlType int ctrlType = + mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -629,14 +529,16 @@ 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); + } } } 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..b5d1d4a76342 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -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.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) { + 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 = isEventFromTouchscreen(e) + ? isInCornerBounds(mLargeTaskCorners, x, y) + : isInCornerBounds(mFineTaskCorners, x, y); + // Check if touch falls within the edge resize handle. Limit edge resizing to stylus and + // mouse input. + if (!result && isEdgeResizePermitted(e)) { + result = isInEdgeResizeBounds(x, y); + } + return result; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isEventFromTouchscreen(e) + ? isInCornerBounds(mFineTaskCorners, x, y) + : isInEdgeResizeBounds(x, y); + } + } + + static boolean isEventFromTouchscreen(@NonNull MotionEvent e) { + return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + } + + static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { + if (enableWindowingEdgeDragResize()) { + return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS + || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + } else { + return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + } + } + + 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. + * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events + * (finger & stylus) are eligible for a larger area than cursor events + * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge + * resize region. + */ + @DragPositioningCallback.CtrlType + int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, 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 = isTouchscreen + ? 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 && isEdgeResizePermitted) { + ctrlType = calculateEdgeResizeCtrlType(x, y); + } + return ctrlType; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isTouchscreen + ? 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..df0836c1121d 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,11 @@ 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 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 android.annotation.NonNull; import android.annotation.Nullable; @@ -31,17 +36,26 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Point; import android.graphics.PointF; +import android.graphics.Rect; 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; import android.window.SurfaceSyncGroup; +import androidx.annotation.VisibleForTesting; + +import com.android.window.flags.Flags; import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; /** * Handle menu opened when the appropriate button is clicked on. @@ -53,16 +67,27 @@ 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; - private final PointF mHandleMenuPosition = new PointF(); + private final DesktopModeWindowDecoration mParentDecor; + @VisibleForTesting + AdditionalViewContainer mHandleMenuViewContainer; + // Position of the handle menu used for laying out the handle view. + @VisibleForTesting + final PointF mHandleMenuPosition = new PointF(); + // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} + // may be in a different coordinate space than the input coordinates. Therefore, we still care + // about the menu's coordinates relative to the display as a whole, so we need to maintain + // those as well. + final Point mGlobalMenuPosition = new Point(); private final boolean mShouldShowWindowingPill; private final Bitmap mAppIconBitmap; private final CharSequence mAppName; private final View.OnClickListener mOnClickListener; private final View.OnTouchListener mOnTouchListener; private final RunningTaskInfo mTaskInfo; + private final DisplayController mDisplayController; + private final SplitScreenController mSplitScreenController; private final int mLayoutResId; private int mMarginMenuTop; private int mMarginMenuStart; @@ -72,12 +97,16 @@ class HandleMenu { private HandleMenuAnimator mHandleMenuAnimator; - HandleMenu(WindowDecoration parentDecor, int layoutResId, View.OnClickListener onClickListener, - View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName, - boolean shouldShowWindowingPill, int captionHeight) { + HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId, + View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, + Bitmap appIcon, CharSequence appName, DisplayController displayController, + SplitScreenController splitScreenController, boolean shouldShowWindowingPill, + int captionHeight) { mParentDecor = parentDecor; mContext = mParentDecor.mDecorWindowContext; mTaskInfo = mParentDecor.mTaskInfo; + mDisplayController = displayController; + mSplitScreenController = splitScreenController; mLayoutResId = layoutResId; mOnClickListener = onClickListener; mOnTouchListener = onTouchListener; @@ -93,20 +122,27 @@ class HandleMenu { final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - createHandleMenuWindow(t, ssg); + createHandleMenuViewContainer(t, ssg); ssg.addTransaction(t); ssg.markSyncReady(); setupHandleMenu(); animateHandleMenu(); } - private void createHandleMenuWindow(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { + private void createHandleMenuViewContainer(SurfaceControl.Transaction t, + SurfaceSyncGroup ssg) { final int x = (int) mHandleMenuPosition.x; final int y = (int) mHandleMenuPosition.y; - mHandleMenuWindow = mParentDecor.addWindow( - R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", - t, ssg, x, y, mMenuWidth, mMenuHeight); - final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView(); + if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) { + mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext, + R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId, + x, y, mMenuWidth, mMenuHeight); + } else { + mHandleMenuViewContainer = mParentDecor.addWindow( + R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", + t, ssg, x, y, mMenuWidth, mMenuHeight); + } + final View handleMenuView = mHandleMenuViewContainer.getView(); mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight); } @@ -127,7 +163,7 @@ class HandleMenu { * pill. */ private void setupHandleMenu() { - final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView(); + final View handleMenu = mHandleMenuViewContainer.getView(); handleMenu.setOnTouchListener(mOnTouchListener); setupAppInfoPill(handleMenu); if (mShouldShowWindowingPill) { @@ -140,10 +176,12 @@ 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); + collapseBtn.setTaskInfo(mTaskInfo); appIcon.setImageBitmap(mAppIconBitmap); appName.setText(mAppName); } @@ -172,9 +210,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 +223,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); + } } /** @@ -214,55 +250,105 @@ class HandleMenu { * Updates handle menu's position variables to reflect its next position. */ private void updateHandleMenuPillPositions() { - final int menuX, menuY; - final int captionWidth = mTaskInfo.getConfiguration() - .windowConfiguration.getBounds().width(); - if (mLayoutResId - == R.layout.desktop_mode_app_controls_window_decor) { - // Align the handle menu to the left of the caption. + int menuX; + final int menuY; + final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); + updateGlobalMenuPosition(taskBounds); + if (mLayoutResId == R.layout.desktop_mode_app_header) { + // Align the handle menu to the left side of the caption. menuX = mMarginMenuStart; menuY = mMarginMenuTop; } else { - // Position the handle menu at the center of the caption. - menuX = (captionWidth / 2) - (mMenuWidth / 2); - menuY = mMarginMenuStart; + if (Flags.enableAdditionalWindowsAboveStatusBar()) { + // In a focused decor, we use global coordinates for handle menu. Therefore we + // need to account for other factors like split stage and menu/handle width to + // center the menu. + final DisplayLayout layout = mDisplayController + .getDisplayLayout(mTaskInfo.displayId); + menuX = mGlobalMenuPosition.x + ((mMenuWidth - layout.width()) / 2); + menuY = mGlobalMenuPosition.y + ((mMenuHeight - layout.height()) / 2); + } else { + menuX = (taskBounds.width() / 2) - (mMenuWidth / 2); + menuY = mMarginMenuTop; + } } - // Handle Menu position setup. mHandleMenuPosition.set(menuX, menuY); + } + private void updateGlobalMenuPosition(Rect taskBounds) { + if (mTaskInfo.isFreeform()) { + mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart, + taskBounds.top + mMarginMenuTop); + } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + mGlobalMenuPosition.set( + (taskBounds.width() / 2) - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop + ); + } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { + final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId); + final Rect leftOrTopStageBounds = new Rect(); + final Rect rightOrBottomStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftOrTopStageBounds, + rightOrBottomStageBounds); + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + mGlobalMenuPosition.set(leftOrTopStageBounds.width() + + (rightOrBottomStageBounds.width() / 2) + - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop); + } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2) + - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop); + } + } } /** * Update pill layout, in case task changes have caused positioning to change. */ void relayout(SurfaceControl.Transaction t) { - if (mHandleMenuWindow != null) { + if (mHandleMenuViewContainer != null) { updateHandleMenuPillPositions(); - t.setPosition(mHandleMenuWindow.mWindowSurface, - mHandleMenuPosition.x, mHandleMenuPosition.y); + mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y); } } /** - * 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) { - 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); + void checkMotionEvent(MotionEvent ev) { + // If the menu view is above status bar, we can let the views handle input directly. + if (isViewAboveStatusBar()) return; + final View handleMenu = mHandleMenuViewContainer.getView(); + 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(); } } + private boolean isViewAboveStatusBar() { + return Flags.enableAdditionalWindowsAboveStatusBar() + && !mTaskInfo.isFreeform(); + } + + // 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. @@ -272,10 +358,33 @@ class HandleMenu { */ boolean isValidMenuInput(PointF inputPoint) { if (!viewsLaidOut()) return true; - return pointInView( - mHandleMenuWindow.mWindowViewHost.getView(), - inputPoint.x - mHandleMenuPosition.x, - inputPoint.y - mHandleMenuPosition.y); + if (!isViewAboveStatusBar()) { + return pointInView( + mHandleMenuViewContainer.getView(), + inputPoint.x - mHandleMenuPosition.x, + inputPoint.y - mHandleMenuPosition.y); + } else { + // Handle menu exists in a different coordinate space when added to WindowManager. + // Therefore we must compare the provided input coordinates to global menu coordinates. + // This includes factoring for split stage as input coordinates are relative to split + // stage position, not relative to the display as a whole. + PointF inputRelativeToMenu = new PointF( + inputPoint.x - mGlobalMenuPosition.x, + inputPoint.y - mGlobalMenuPosition.y + ); + if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) + == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + // TODO(b/343561161): This also needs to be calculated differently if + // the task is in top/bottom split. + Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + inputRelativeToMenu.x += leftStageBounds.width(); + } + return pointInView( + mHandleMenuViewContainer.getView(), + inputRelativeToMenu.x, + inputRelativeToMenu.y); + } } private boolean pointInView(View v, float x, float y) { @@ -287,7 +396,7 @@ class HandleMenu { * Check if the views for handle menu can be seen. */ private boolean viewsLaidOut() { - return mHandleMenuWindow.mWindowViewHost.getView().isLaidOut(); + return mHandleMenuViewContainer.getView().isLaidOut(); } private void loadHandleMenuDimensions() { @@ -305,12 +414,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; } @@ -323,8 +435,8 @@ class HandleMenu { void close() { final Runnable after = () -> { - mHandleMenuWindow.releaseView(); - mHandleMenuWindow = null; + mHandleMenuViewContainer.releaseView(); + mHandleMenuViewContainer = null; }; if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { @@ -335,7 +447,7 @@ class HandleMenu { } static final class Builder { - private final WindowDecoration mParent; + private final DesktopModeWindowDecoration mParent; private CharSequence mName; private Bitmap mAppIcon; private View.OnClickListener mOnClickListener; @@ -343,9 +455,10 @@ class HandleMenu { private int mLayoutId; private boolean mShowWindowingPill; private int mCaptionHeight; + private DisplayController mDisplayController; + private SplitScreenController mSplitScreenController; - - Builder(@NonNull WindowDecoration parent) { + Builder(@NonNull DesktopModeWindowDecoration parent) { mParent = parent; } @@ -384,9 +497,20 @@ class HandleMenu { return this; } + Builder setDisplayController(DisplayController displayController) { + mDisplayController = displayController; + return this; + } + + Builder setSplitScreenController(SplitScreenController splitScreenController) { + mSplitScreenController = splitScreenController; + return this; + } + HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener, - mAppIcon, mName, mShowWindowingPill, mCaptionHeight); + return new HandleMenu(mParent, mLayoutId, mOnClickListener, + mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, + mShowWindowingPill, mCaptionHeight); } } } 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..18757ef6ff40 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -0,0 +1,48 @@ +/* + * 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.app.ActivityManager.RunningTaskInfo +import com.android.window.flags.Flags +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer + +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. + * TODO(b/342229481): Remove this and all uses of it once [AdditionalSystemViewContainer] is no longer + * guarded by a flag. + */ +class HandleMenuImageButton( + context: Context?, + attrs: AttributeSet? +) : ImageButton(context, attrs) { + lateinit var taskInfo: RunningTaskInfo + + override fun onHoverEvent(motionEvent: MotionEvent): Boolean { + if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) { + return super.onHoverEvent(motionEvent) + } else { + return false + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index b2f8cfdbfb7a..4f049015af99 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -20,6 +20,8 @@ import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.content.Context import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.RippleDrawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -29,6 +31,7 @@ import android.widget.ProgressBar import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat +import com.android.window.flags.Flags import com.android.wm.shell.R private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350 @@ -85,22 +88,51 @@ class MaximizeButtonView( } fun cancelHoverAnimation() { - hoverProgressAnimatorSet.removeAllListeners() + hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() } hoverProgressAnimatorSet.cancel() progressBar.visibility = View.INVISIBLE } - fun setAnimationTints(darkMode: Boolean) { - if (darkMode) { - progressBar.progressTintList = ColorStateList.valueOf( + /** + * Set the color tints of the maximize button views. + * + * @param darkMode whether the app's theme is in dark mode. + * @param iconForegroundColor the color tint to use for the maximize icon to match the rest of + * the App Header icons + * @param baseForegroundColor the base foreground color tint used by the App Header, used to style + * views within this button using the same base color but with different opacities. + */ + fun setAnimationTints( + darkMode: Boolean, + iconForegroundColor: ColorStateList? = null, + baseForegroundColor: Int? = null, + rippleDrawable: RippleDrawable? = null + ) { + if (Flags.enableThemedAppHeaders()) { + requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" } + requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" } + requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" } + maximizeWindow.imageTintList = iconForegroundColor + maximizeWindow.background = rippleDrawable + progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor) + .withAlpha(OPACITY_15) + progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) + } else { + if (darkMode) { + progressBar.progressTintList = ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark)) - maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, + maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, R.color.desktop_mode_caption_button_color_selector_dark)) - } else { - progressBar.progressTintList = ColorStateList.valueOf( + } else { + progressBar.progressTintList = ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_light)) - maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, + maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, R.color.desktop_mode_caption_button_color_selector_light)) + } } } + + companion object { + private const val OPACITY_15 = 38 + } } 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..0470367015ea 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,29 +16,55 @@ package com.android.wm.shell.windowdecor +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.annotation.ColorInt import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.content.res.ColorStateList import android.content.res.Resources +import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PointF +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.StateListDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.SurfaceControlViewHost +import android.view.View 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.TextView import android.window.TaskConstants +import androidx.compose.material3.ColorScheme +import androidx.compose.ui.graphics.toArgb +import androidx.core.animation.addListener 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 +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.OPACITY_12 +import com.android.wm.shell.windowdecor.common.OPACITY_40 +import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier @@ -58,17 +84,16 @@ class MaximizeMenu( private val menuPosition: PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { - private var maximizeMenu: AdditionalWindow? = null + private var maximizeMenu: AdditionalViewHostViewContainer? = null + private var maximizeMenuView: MaximizeMenuView? = 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 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) /** Position the menu relative to the caption's position. */ fun positionMenu(position: PointF, t: Transaction) { @@ -80,22 +105,20 @@ class MaximizeMenu( fun show() { if (maximizeMenu != null) return createMaximizeMenu() - setupMaximizeMenu() + maximizeMenuView?.animateOpenMenu() } /** Closes the maximize window and releases its view. */ fun close() { + maximizeMenuView?.cancelAnimation() maximizeMenu?.releaseView() maximizeMenu = null + maximizeMenuView = null } /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu() { val t = transactionSupplier.get() - val v = LayoutInflater.from(decorWindowContext).inflate( - R.layout.desktop_mode_window_decor_maximize_menu, - null // Root - ) val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder) leash = builder @@ -119,16 +142,25 @@ class MaximizeMenu( viewHost = SurfaceControlViewHost(decorWindowContext, displayController.getDisplay(taskInfo.displayId), windowManager, "MaximizeMenu") - viewHost.setView(v, lp) + maximizeMenuView = MaximizeMenuView( + context = decorWindowContext, + menuHeight = menuHeight, + menuPadding = menuPadding, + onClickListener = onClickListener, + onTouchListener = onTouchListener, + onGenericMotionListener = onGenericMotionListener, + ).also { menuView -> + menuView.bind(taskInfo) + viewHost.setView(menuView.rootView, lp) + } // 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) + maximizeMenu = + AdditionalViewHostViewContainer(leash, viewHost, transactionSupplier) syncQueue.runInSync { transaction -> transaction.merge(t) @@ -144,31 +176,6 @@ class MaximizeMenu( } } - private fun setupMaximizeMenu() { - val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return - - maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener) - maximizeMenuView.setOnTouchListener(onTouchListener) - - val maximizeButton = maximizeMenuView.requireViewById<Button>( - 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.setOnClickListener(onClickListener) - snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - - val snapLeftButton = maximizeMenuView.requireViewById<Button>( - R.id.maximize_menu_snap_left_button - ) - snapLeftButton.setOnClickListener(onClickListener) - snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) - } - /** * A valid menu input is one of the following: * An input that happens in the menu views. @@ -187,14 +194,457 @@ class MaximizeMenu( * Check if the views for maximize menu can be seen. */ private fun viewsLaidOut(): Boolean { - return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false + return maximizeMenu?.view?.isLaidOut ?: false + } + + /** + * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views. + * + * TODO(b/346440693): this is only needed for the left/right snap options that don't support + * selector states to manage its hover state. Look into whether that can be added to avoid + * manually tracking hover enter/exit motion events. Also because those button colors/states + * aren't updating correctly for pressed, focused and selected states. + * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit]. + */ + fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { + setSnapButtonsColorOnHover(viewId, ev) + } + + /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */ + fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { + setSnapButtonsColorOnHover(viewId, ev) + } + + /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */ + fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { + val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return + val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return + val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth && + ev.y >= 0 && ev.y <= snapOptionsHeight + + 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. + maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE) + } + } + + private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { + val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return + val snapMenuCenter = snapOptionsWidth / 2 + when { + viewId == R.id.maximize_menu_snap_left_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> { + maximizeMenuView + ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT) + } + viewId == R.id.maximize_menu_snap_right_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> { + maximizeMenuView + ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT) + } + } + } + + /** + * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for + * resizing a Task. + */ + class MaximizeMenuView( + context: Context, + private val menuHeight: Int, + private val menuPadding: Int, + onClickListener: OnClickListener, + onTouchListener: OnTouchListener, + onGenericMotionListener: OnGenericMotionListener, + ) { + val rootView: View = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) + private val maximizeText = + requireViewById(R.id.maximize_menu_maximize_window_text) as TextView + private val maximizeButton = + requireViewById(R.id.maximize_menu_maximize_button) as Button + private val snapWindowText = + requireViewById(R.id.maximize_menu_snap_window_text) as TextView + private val snapRightButton = + requireViewById(R.id.maximize_menu_snap_right_button) as Button + private val snapLeftButton = + requireViewById(R.id.maximize_menu_snap_left_button) as Button + private val snapButtonsLayout = + requireViewById(R.id.maximize_menu_snap_menu_layout) + + private val decorThemeUtil = DecorThemeUtil(context) + + private val outlineRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius) + private val outlineStroke = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke) + private val fillPadding = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding) + private val fillRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) + + private val openMenuAnimatorSet = AnimatorSet() + private lateinit var taskInfo: RunningTaskInfo + private lateinit var style: MenuStyle + + /** The width of the snap menu option view, including both left and right snaps. */ + val snapOptionsWidth: Int + get() = snapButtonsLayout.width + /** The height of the snap menu option view, including both left and right snaps .*/ + val snapOptionsHeight: Int + get() = snapButtonsLayout.height + + init { + // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and + // expose only what is actually relevant to outside classes so that specific checks + // against resource IDs aren't needed outside this class. + rootView.setOnGenericMotionListener(onGenericMotionListener) + rootView.setOnTouchListener(onTouchListener) + maximizeButton.setOnClickListener(onClickListener) + maximizeButton.setOnGenericMotionListener(onGenericMotionListener) + snapRightButton.setOnClickListener(onClickListener) + snapRightButton.setOnGenericMotionListener(onGenericMotionListener) + snapLeftButton.setOnClickListener(onClickListener) + snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) + snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) + + // To prevent aliasing. + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + + /** Bind the menu views to the new [RunningTaskInfo] data. */ + fun bind(taskInfo: RunningTaskInfo) { + this.taskInfo = taskInfo + this.style = calculateMenuStyle(taskInfo) + + rootView.background.setTint(style.backgroundColor) + + // Maximize option. + maximizeButton.background = style.maximizeOption.drawable + maximizeText.setTextColor(style.textColor) + + // Snap options. + snapWindowText.setTextColor(style.textColor) + updateSplitSnapSelection(SnapToHalfSelection.NONE) + } + + /** Animate the opening of the menu */ + fun animateOpenMenu() { + maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + openMenuAnimatorSet.playTogether( + ObjectAnimator.ofFloat(rootView, 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() + rootView.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 + maximizeButton.scaleY = value + snapButtonsLayout.scaleY = value + maximizeText.scaleY = value + snapWindowText.scaleY = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, + (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + }, + ObjectAnimator.ofInt(rootView.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 + maximizeButton.alpha = value + snapButtonsLayout.alpha = value + maximizeText.alpha = value + snapWindowText.alpha = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION) + .apply { + duration = ELEVATION_ANIMATION_DURATION_MS + startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + } + ) + openMenuAnimatorSet.addListener( + onEnd = { + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + ) + openMenuAnimatorSet.start() + } + + /** Cancel the open menu animation. */ + fun cancelAnimation() { + openMenuAnimatorSet.cancel() + } + + /** Update the view state to a new snap to half selection. */ + fun updateSplitSnapSelection(selection: SnapToHalfSelection) { + when (selection) { + SnapToHalfSelection.NONE -> deactivateSnapOptions() + SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true) + SnapToHalfSelection.RIGHT -> activateSnapOption(activateLeft = false) + } + } + + private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle { + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + val menuBackgroundColor = colorScheme.surfaceContainerLow.toArgb() + return MenuStyle( + backgroundColor = menuBackgroundColor, + textColor = colorScheme.onSurface.toArgb(), + maximizeOption = MenuStyle.MaximizeOption( + drawable = createMaximizeDrawable(menuBackgroundColor, colorScheme) + ), + snapOptions = MenuStyle.SnapOptions( + inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(), + semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40), + activeSnapSideColor = colorScheme.primary.toArgb(), + inactiveStrokeColor = colorScheme.outlineVariant.toArgb(), + activeStrokeColor = colorScheme.primary.toArgb(), + inactiveBackgroundColor = menuBackgroundColor, + activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12) + ), + ) + } + + private fun deactivateSnapOptions() { + // TODO(b/346440693): the background/colorStateList set on these buttons is overridden + // to a static resource & color on manually tracked hover events, which defeats the + // point of state lists and selector states. Look into whether changing that is + // possible, similar to the maximize option. Also to include support for the + // semi-active state (when the "other" snap option is selected). + val snapSideColorList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_focused), + intArrayOf(android.R.attr.state_selected), + intArrayOf(), + ), + intArrayOf( + style.snapOptions.activeSnapSideColor, + style.snapOptions.activeSnapSideColor, + style.snapOptions.activeSnapSideColor, + style.snapOptions.inactiveSnapSideColor + ) + ) + snapLeftButton.background?.setTintList(snapSideColorList) + snapRightButton.background?.setTintList(snapSideColorList) + with (snapButtonsLayout) { + setBackgroundResource(R.drawable.desktop_mode_maximize_menu_layout_background) + (background as GradientDrawable).apply { + setColor(style.snapOptions.inactiveBackgroundColor) + setStroke(outlineStroke, style.snapOptions.inactiveStrokeColor) + } + } + } + + private fun activateSnapOption(activateLeft: Boolean) { + // Regardless of which side is active, the background of the snap options layout (that + // includes both sides) is considered "active". + with (snapButtonsLayout) { + setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) + (background as GradientDrawable).apply { + setColor(style.snapOptions.activeBackgroundColor) + setStroke(outlineStroke, style.snapOptions.activeStrokeColor) + } + } + if (activateLeft) { + // Highlight snap left button, partially highlight the other side. + snapLeftButton.background.setTint(style.snapOptions.activeSnapSideColor) + snapRightButton.background.setTint(style.snapOptions.semiActiveSnapSideColor) + } else { + // Highlight snap right button, partially highlight the other side. + snapRightButton.background.setTint(style.snapOptions.activeSnapSideColor) + snapLeftButton.background.setTint(style.snapOptions.semiActiveSnapSideColor) + } + } + + private fun createMaximizeDrawable( + @ColorInt menuBackgroundColor: Int, + colorScheme: ColorScheme + ): StateListDrawable { + val activeStrokeAndFill = colorScheme.primary.toArgb() + val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12) + val activeDrawable = createMaximizeButtonDrawable( + strokeAndFillColor = activeStrokeAndFill, + backgroundColor = activeBackground, + // Add a mask with the menu background's color because the active background color is + // semi transparent, otherwise the transparency will reveal the stroke/fill color + // behind it. + backgroundMask = menuBackgroundColor + ) + return StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_pressed), activeDrawable) + addState(intArrayOf(android.R.attr.state_focused), activeDrawable) + addState(intArrayOf(android.R.attr.state_selected), activeDrawable) + addState(intArrayOf(android.R.attr.state_hovered), activeDrawable) + // Inactive drawable. + addState( + StateSet.WILD_CARD, + createMaximizeButtonDrawable( + strokeAndFillColor = colorScheme.outlineVariant.toArgb(), + backgroundColor = colorScheme.surfaceContainerLow.toArgb(), + backgroundMask = null // not needed because the bg color is fully opaque + ) + ) + } + } + + private fun createMaximizeButtonDrawable( + @ColorInt strokeAndFillColor: Int, + @ColorInt backgroundColor: Int, + @ColorInt backgroundMask: Int? + ): LayerDrawable { + val layers = mutableListOf<Drawable>() + // First (bottom) layer, effectively the button's border ring once its inner shape is + // covered by the next layers. + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = strokeAndFillColor + paint.style = Paint.Style.FILL + }) + // Second layer, a mask for the next (background) layer if needed because of + // transparency. + backgroundMask?.let { color -> + layers.add( + ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = color + paint.style = Paint.Style.FILL + } + ) + } + // Third layer, the "background" padding between the border and the fill. + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = backgroundColor + paint.style = Paint.Style.FILL + }) + // Final layer, the inner most rounded-rect "fill". + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { fillRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = strokeAndFillColor + paint.style = Paint.Style.FILL + }) + return LayerDrawable(layers.toTypedArray()).apply { + when (numberOfLayers) { + 3 -> { + setLayerInset(1, outlineStroke) + setLayerInset(2, fillPadding) + } + 4 -> { + setLayerInset(intArrayOf(1, 2), outlineStroke) + setLayerInset(3, fillPadding) + } + else -> error("Unexpected number of layers: $numberOfLayers") + } + } + } + + private fun LayerDrawable.setLayerInset(index: IntArray, inset: Int) { + for (i in index) { + setLayerInset(i, inset, inset, inset, inset) + } + } + + private fun LayerDrawable.setLayerInset(index: Int, inset: Int) { + setLayerInset(index, inset, inset, inset, inset) + } + + private fun requireViewById(id: Int) = rootView.requireViewById<View>(id) + + /** The style to apply to the menu. */ + data class MenuStyle( + @ColorInt val backgroundColor: Int, + @ColorInt val textColor: Int, + val maximizeOption: MaximizeOption, + val snapOptions: SnapOptions, + ) { + data class MaximizeOption( + val drawable: StateListDrawable, + ) + data class SnapOptions( + @ColorInt val inactiveSnapSideColor: Int, + @ColorInt val semiActiveSnapSideColor: Int, + @ColorInt val activeSnapSideColor: Int, + @ColorInt val inactiveStrokeColor: Int, + @ColorInt val activeStrokeColor: Int, + @ColorInt val inactiveBackgroundColor: Int, + @ColorInt val activeBackgroundColor: Int, + ) + } + + /** The possible selection states of the half-snap menu option. */ + enum class SnapToHalfSelection { + NONE, LEFT, RIGHT + } } 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_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..974166700203 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 @@ -7,7 +7,7 @@ import android.graphics.PointF import android.graphics.Rect import android.view.MotionEvent import android.view.SurfaceControl -import com.android.internal.policy.ScreenDecorationsUtils +import com.android.wm.shell.R /** * Creates an animator to shrink and position task after a user drags a fullscreen task from @@ -31,17 +31,24 @@ 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() + val cornerRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat() + 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 +84,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 deleted file mode 100644 index b0d3b5090ef0..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ /dev/null @@ -1,270 +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.windowdecor; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.ColorRes; -import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; -import android.widget.ImageView; -import android.window.TaskConstants; - -import com.android.wm.shell.R; - -import java.util.function.Supplier; - -/** - * Creates and updates a veil that covers task contents on resize. - */ -public class ResizeVeil { - private static final int RESIZE_ALPHA_DURATION = 100; - private final Context mContext; - private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; - private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; - private final Drawable mAppIcon; - private ImageView mIconView; - private SurfaceControl mParentSurface; - private SurfaceControl mVeilSurface; - private final RunningTaskInfo mTaskInfo; - private SurfaceControlViewHost mViewHost; - private final Display mDisplay; - private ValueAnimator mVeilAnimator; - - public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo, - Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display, - Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { - mContext = context; - mAppIcon = appIcon; - mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; - mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; - mTaskInfo = taskInfo; - mDisplay = display; - 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) - .setContainerLayer() - .build(); - View v = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null); - - t.setPosition(mVeilSurface, 0, 0) - .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) - .apply(); - Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); - final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(taskBounds.width(), - taskBounds.height(), - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT); - lp.setTitle("Resize veil 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); - } - - /** - * Shows the veil surface/view. - * - * @param t the transaction to apply in sync with the veil draw - * @param parentSurface the surface that the veil should be a child of - * @param taskBounds the bounds of the task that owns the veil - * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown - * immediately - */ - public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface, - Rect taskBounds, boolean fadeIn) { - // 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)); - - relayout(taskBounds, t); - if (fadeIn) { - cancelAnimation(); - mVeilAnimator = new ValueAnimator(); - mVeilAnimator.setFloatValues(0f, 1f); - mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); - mVeilAnimator.addUpdateListener(animation -> { - t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction()); - t.apply(); - }); - mVeilAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - t.setAlpha(mVeilSurface, 1); - t.apply(); - } - }); - - final ValueAnimator iconAnimator = new ValueAnimator(); - iconAnimator.setFloatValues(0f, 1f); - iconAnimator.setDuration(RESIZE_ALPHA_DURATION); - iconAnimator.addUpdateListener(animation -> { - mIconView.setAlpha(animation.getAnimatedFraction()); - }); - - t.show(mVeilSurface) - .addTransactionCommittedListener( - mContext.getMainExecutor(), () -> { - mVeilAnimator.start(); - iconAnimator.start(); - }) - .setAlpha(mVeilSurface, 0); - } else { - // Show the veil immediately at full opacity. - t.show(mVeilSurface).setAlpha(mVeilSurface, 1); - } - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); - } - - /** - * Animate veil's alpha to 1, fading it in. - */ - public void showVeil(SurfaceControl parentSurface, Rect taskBounds) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - showVeil(t, parentSurface, taskBounds, true /* fadeIn */); - } - - /** - * Update veil bounds to match bounds changes. - * @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()); - t.setPosition(mParentSurface, newBounds.left, newBounds.top); - t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); - } - - /** - * Calls relayout to update task and veil bounds. - * @param newBounds bounds to update veil to. - */ - public void updateResizeVeil(Rect newBounds) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - updateResizeVeil(t, newBounds); - } - - /** - * Calls relayout to update task and veil bounds. - * Finishes veil fade in if animation is currently running; this is to prevent empty space - * being visible behind the transparent veil during a fast resize. - * - * @param t a transaction to be applied in sync with the veil draw. - * @param newBounds bounds to update veil to. - */ - public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) { - if (mVeilAnimator != null && mVeilAnimator.isStarted()) { - mVeilAnimator.removeAllUpdateListeners(); - mVeilAnimator.end(); - } - relayout(newBounds, t); - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); - } - - /** - * Animate veil's alpha to 0, fading it out. - */ - public void hideVeil() { - 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.apply(); - }); - mVeilAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.hide(mVeilSurface); - t.apply(); - } - }); - mVeilAnimator.start(); - } - - @ColorRes - private int getBackgroundColorId() { - Configuration configuration = mContext.getResources().getConfiguration(); - if ((configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) - == Configuration.UI_MODE_NIGHT_YES) { - return R.color.desktop_mode_resize_veil_dark; - } else { - return R.color.desktop_mode_resize_veil_light; - } - } - - private void cancelAnimation() { - if (mVeilAnimator != null) { - mVeilAnimator.removeAllUpdateListeners(); - mVeilAnimator.cancel(); - } - } - - /** - * Dispose of veil when it is no longer needed, likely on close of its container decor. - */ - void dispose() { - cancelAnimation(); - mVeilAnimator = null; - - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - if (mVeilSurface != null) { - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.remove(mVeilSurface); - mVeilSurface = null; - t.apply(); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt new file mode 100644 index 000000000000..cd2dac806a7f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -0,0 +1,417 @@ +/* + * 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.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.PointF +import android.graphics.Rect +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.WindowManager +import android.view.WindowlessWindowManager +import android.widget.ImageView +import android.window.TaskConstants +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.Theme +import java.util.function.Supplier + +/** + * Creates and updates a veil that covers task contents on resize. + */ +class ResizeVeil @JvmOverloads constructor( + private val context: Context, + private val displayController: DisplayController, + private val appIcon: Bitmap, + private var parentSurface: SurfaceControl, + private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, + private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = + object : SurfaceControlBuilderFactory {}, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = + object : SurfaceControlViewHostFactory {}, + taskInfo: RunningTaskInfo, +) { + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + private val surfaceSession = SurfaceSession() + private lateinit var iconView: ImageView + private var iconSize = 0 + + /** A container surface to host the veil background and icon child surfaces. */ + private var veilSurface: SurfaceControl? = null + /** A color surface for the veil background. */ + private var backgroundSurface: SurfaceControl? = null + /** A surface that hosts a windowless window with the app icon. */ + private var iconSurface: SurfaceControl? = null + private var viewHost: SurfaceControlViewHost? = null + private var display: Display? = null + private var veilAnimator: ValueAnimator? = null + + /** + * Whether the resize veil is currently visible. + * + * Note: when animating a [ResizeVeil.hideVeil], the veil is considered visible as soon + * as the animation starts. + */ + private var isVisible = false + + private val onDisplaysChangedListener: OnDisplaysChangedListener = + object : OnDisplaysChangedListener { + override fun onDisplayAdded(displayId: Int) { + if (taskInfo.displayId != displayId) { + return + } + displayController.removeDisplayWindowListener(this) + setupResizeVeil(taskInfo) + } + } + + /** + * Whether the resize veil is ready to be shown. + */ + private val isReady: Boolean + get() = viewHost != null + + init { + setupResizeVeil(taskInfo) + } + + /** + * Create the veil in its default invisible state. + */ + private fun setupResizeVeil(taskInfo: RunningTaskInfo) { + if (!obtainDisplayOrRegisterListener(taskInfo.displayId)) { + // Display may not be available yet, skip this until then. + return + } + Trace.beginSection("ResizeVeil#setupResizeVeil") + veilSurface = surfaceControlBuilderFactory + .create("Resize veil of Task=" + taskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(parentSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + backgroundSurface = surfaceControlBuilderFactory + .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession) + .setColorLayer() + .setHidden(true) + .setParent(veilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + iconSurface = surfaceControlBuilderFactory + .create("Resize veil icon of Task=" + taskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(veilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + iconSize = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size) + val root = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_resize_veil, null /* root */) + iconView = root.requireViewById(R.id.veil_application_icon) + iconView.setImageBitmap(appIcon) + val lp = WindowManager.LayoutParams( + iconSize, + iconSize, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT) + lp.title = "Resize veil icon window of Task=" + taskInfo.taskId + lp.setTrustedOverlay() + val wwm = WindowlessWindowManager(taskInfo.configuration, + iconSurface, null /* hostInputToken */) + viewHost = surfaceControlViewHostFactory.create(context, display, wwm, "ResizeVeil") + viewHost?.setView(root, lp) + Trace.endSection() + } + + private fun obtainDisplayOrRegisterListener(displayId: Int): Boolean { + display = displayController.getDisplay(displayId) + if (display == null) { + displayController.addDisplayWindowListener(onDisplaysChangedListener) + return false + } + return true + } + + /** + * Shows the veil surface/view. + * + * @param t the transaction to apply in sync with the veil draw + * @param parent the surface that the veil should be a child of + * @param taskBounds the bounds of the task that owns the veil + * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown + * immediately + */ + fun showVeil( + t: SurfaceControl.Transaction, + parent: SurfaceControl, + taskBounds: Rect, + taskInfo: RunningTaskInfo, + fadeIn: Boolean, + ) { + if (!isReady || isVisible) { + t.apply() + return + } + isVisible = true + val background = backgroundSurface + val icon = iconSurface + val veil = veilSurface + if (background == null || icon == null || veil == null) return + + // Parent surface can change, ensure it is up to date. + if (parent != parentSurface) { + t.reparent(veil, parent) + parentSurface = parent + } + + val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) { + Theme.LIGHT -> lightColors.surfaceContainer + Theme.DARK -> darkColors.surfaceContainer + } + t.show(veil) + .setLayer(veil, VEIL_CONTAINER_LAYER) + .setLayer(icon, VEIL_ICON_LAYER) + .setLayer(background, VEIL_BACKGROUND_LAYER) + .setColor(background, Color.valueOf(backgroundColor.toArgb()).components) + relayout(taskBounds, t) + if (fadeIn) { + cancelAnimation() + val veilAnimT = surfaceControlTransactionSupplier.get() + val iconAnimT = surfaceControlTransactionSupplier.get() + veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + veilAnimT.setAlpha(background, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + veilAnimT.show(background) + .setAlpha(background, 0f) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + veilAnimT.setAlpha(background, 1f).apply() + } + }) + } + val iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + iconAnimT.setAlpha(icon, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + iconAnimT.show(icon) + .setAlpha(icon, 0f) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + iconAnimT.setAlpha(icon, 1f).apply() + } + }) + } + + // Let the animators show it with the correct alpha value once the animation starts. + t.hide(icon) + .hide(background) + .apply() + veilAnimator?.start() + iconAnimator.start() + } else { + // Show the veil immediately. + t.show(icon) + .show(background) + .setAlpha(icon, 1f) + .setAlpha(background, 1f) + .apply() + } + } + + /** + * Animate veil's alpha to 1, fading it in. + */ + fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect, taskInfo: RunningTaskInfo) { + if (!isReady || isVisible) { + return + } + val t = surfaceControlTransactionSupplier.get() + showVeil(t, parentSurface, taskBounds, taskInfo, true /* fadeIn */) + } + + /** + * Update veil bounds to match bounds changes. + * @param newBounds bounds to update veil to. + */ + private fun relayout(newBounds: Rect, t: SurfaceControl.Transaction) { + val iconPosition = calculateAppIconPosition(newBounds) + val veil = veilSurface + val icon = iconSurface + if (veil == null || icon == null) return + t.setWindowCrop(veil, newBounds.width(), newBounds.height()) + .setPosition(icon, iconPosition.x, iconPosition.y) + .setPosition(parentSurface, newBounds.left.toFloat(), newBounds.top.toFloat()) + .setWindowCrop(parentSurface, newBounds.width(), newBounds.height()) + } + + /** + * Calls relayout to update task and veil bounds. + * @param newBounds bounds to update veil to. + */ + fun updateResizeVeil(newBounds: Rect) { + if (!isVisible) { + return + } + val t = surfaceControlTransactionSupplier.get() + updateResizeVeil(t, newBounds) + } + + /** + * Calls relayout to update task and veil bounds. + * Finishes veil fade in if animation is currently running; this is to prevent empty space + * being visible behind the transparent veil during a fast resize. + * + * @param t a transaction to be applied in sync with the veil draw. + * @param newBounds bounds to update veil to. + */ + fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) { + if (!isVisible) { + t.apply() + return + } + veilAnimator?.let { animator -> + if (animator.isStarted) { + animator.removeAllUpdateListeners() + animator.end() + } + } + relayout(newBounds, t) + t.apply() + } + + /** + * Animate veil's alpha to 0, fading it out. + */ + fun hideVeil() { + if (!isVisible) { + return + } + cancelAnimation() + val background = backgroundSurface + val icon = iconSurface + if (background == null || icon == null) return + + veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + surfaceControlTransactionSupplier.get() + .setAlpha(background, animatedValue as Float) + .setAlpha(icon, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + surfaceControlTransactionSupplier.get() + .hide(background) + .hide(icon) + .apply() + } + }) + } + veilAnimator?.start() + isVisible = false + } + + private fun calculateAppIconPosition(parentBounds: Rect): PointF { + return PointF(parentBounds.width().toFloat() / 2 - iconSize.toFloat() / 2, + parentBounds.height().toFloat() / 2 - iconSize.toFloat() / 2) + } + + private fun cancelAnimation() { + veilAnimator?.removeAllUpdateListeners() + veilAnimator?.cancel() + } + + /** + * Dispose of veil when it is no longer needed, likely on close of its container decor. + */ + fun dispose() { + cancelAnimation() + veilAnimator = null + isVisible = false + + viewHost?.release() + viewHost = null + + val t: SurfaceControl.Transaction = surfaceControlTransactionSupplier.get() + backgroundSurface?.let { background -> t.remove(background) } + backgroundSurface = null + iconSurface?.let { icon -> t.remove(icon) } + iconSurface = null + veilSurface?.let { veil -> t.remove(veil) } + veilSurface = null + t.apply() + displayController.removeDisplayWindowListener(onDisplaysChangedListener) + } + + interface SurfaceControlBuilderFactory { + fun create(name: String): SurfaceControl.Builder { + return SurfaceControl.Builder().setName(name) + } + + fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder { + return SurfaceControl.Builder(surfaceSession).setName(name) + } + } + + companion object { + private const val TAG = "ResizeVeil" + private const val RESIZE_ALPHA_DURATION = 100L + private const val 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 const val VEIL_BACKGROUND_LAYER = 0 + + /** The icon is a child of the veil container layer and goes in front of the background. */ + private const val VEIL_ICON_LAYER = 1 + } +} 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..ad238c35dd83 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 @@ -52,19 +52,19 @@ class TaskOperations { mSyncQueue = syncQueue; } - void injectBackKey() { - sendBackEvent(KeyEvent.ACTION_DOWN); - sendBackEvent(KeyEvent.ACTION_UP); + void injectBackKey(int displayId) { + sendBackEvent(KeyEvent.ACTION_DOWN, displayId); + sendBackEvent(KeyEvent.ACTION_UP, displayId); } - private void sendBackEvent(int action) { + private void sendBackEvent(int action, int displayId) { final long when = SystemClock.uptimeMillis(); final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); - ev.setDisplayId(mContext.getDisplay().getDisplayId()); + ev.setDisplayId(displayId); if (!mContext.getSystemService(InputManager.class) .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { Log.e(TAG, "Inject input event fail"); @@ -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..216990c35247 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,7 +18,11 @@ 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.content.res.Configuration.DENSITY_DPI_UNDEFINED; +import static android.view.WindowInsets.Type.captionBar; +import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.annotation.NonNull; import android.annotation.Nullable; @@ -33,6 +37,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 +45,24 @@ 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.internal.annotations.VisibleForTesting; 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 com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Supplier; /** @@ -131,8 +139,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( @@ -140,9 +149,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, - SurfaceControl taskSurface, - Configuration windowDecorConfig) { - this(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, + SurfaceControl taskSurface) { + this(context, displayController, taskOrganizer, taskInfo, taskSurface, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, new SurfaceControlViewHostFactory() {}); @@ -154,7 +162,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, @NonNull SurfaceControl taskSurface, - Configuration windowDecorConfig, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, @@ -171,8 +178,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); - mWindowDecorConfig = windowDecorConfig; - mDecorWindowContext = mContext.createConfigurationContext(mWindowDecorConfig); } /** @@ -194,8 +199,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> void relayout(RelayoutParams params, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { - outResult.reset(); + updateViewsAndSurfaces(params, startT, finishT, wct, rootView, outResult); + if (outResult.mRootView != null) { + updateViewHost(params, startT, outResult); + } + } + protected void updateViewsAndSurfaces(RelayoutParams params, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { + outResult.reset(); if (params.mRunningTaskInfo != null) { mTaskInfo = params.mRunningTaskInfo; } @@ -203,20 +216,48 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mLayoutResId = params.mLayoutResId; if (!mTaskInfo.isVisible) { - releaseViews(); + releaseViews(wct); finishT.hide(mTaskSurface); return; } + inflateIfNeeded(params, wct, rootView, oldLayoutResId, outResult); + if (outResult.mRootView == null) { + // Didn't manage to create a root view, early out. + return; + } + rootView = null; // Clear it just in case we use it accidentally + + updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); + + final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); + outResult.mWidth = taskBounds.width(); + outResult.mHeight = taskBounds.height(); + outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); + final Resources resources = mDecorWindowContext.getResources(); + outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL + ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); + outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2; + + updateDecorationContainerSurface(startT, outResult); + updateCaptionContainerSurface(startT, outResult); + updateCaptionInsets(params, wct, outResult, taskBounds); + updateTaskSurface(params, startT, finishT, outResult); + } + + private void inflateIfNeeded(RelayoutParams params, WindowContainerTransaction wct, + T rootView, int oldLayoutResId, RelayoutResult<T> outResult) { if (rootView == null && params.mLayoutResId == 0) { throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); } outResult.mRootView = rootView; - rootView = null; // Clear it just in case we use it accidentally - - final int oldDensityDpi = mWindowDecorConfig.densityDpi; - final int oldNightMode = mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + final int oldDensityDpi = mWindowDecorConfig != null + ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; + final int oldNightMode = mWindowDecorConfig != null + ? (mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) + : Configuration.UI_MODE_NIGHT_UNDEFINED; mWindowDecorConfig = params.mWindowDecorConfig != null ? params.mWindowDecorConfig : mTaskInfo.getConfiguration(); final int newDensityDpi = mWindowDecorConfig.densityDpi; @@ -225,8 +266,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay == null || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId - || oldNightMode != newNightMode) { - releaseViews(); + || oldNightMode != newNightMode + || mDecorWindowContext == null) { + releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { outResult.mRootView = null; @@ -244,24 +286,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) .inflate(params.mLayoutResId, null); } + } - updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); - - final Resources resources = mDecorWindowContext.getResources(); - final Configuration taskConfig = mTaskInfo.getConfiguration(); - final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - final boolean isFullscreen = taskConfig.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FULLSCREEN; - outResult.mWidth = taskBounds.width(); - outResult.mHeight = taskBounds.height(); - - // DecorationContainerSurface + private void updateDecorationContainerSurface( + SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { if (mDecorationContainerSurface == null) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); mDecorationContainerSurface = builder .setName("Decor container of Task=" + mTaskInfo.taskId) .setContainerLayer() .setParent(mTaskSurface) + .setCallsite("WindowDecoration.updateDecorationContainerSurface") .build(); startT.setTrustedOverlay(mDecorationContainerSurface, true) @@ -271,103 +306,101 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) .show(mDecorationContainerSurface); + } - // CaptionContainerSurface, CaptionWindowManager + private void updateCaptionContainerSurface( + SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { if (mCaptionContainerSurface == null) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); mCaptionContainerSurface = builder .setName("Caption container of Task=" + mTaskInfo.taskId) .setContainerLayer() .setParent(mDecorationContainerSurface) + .setCallsite("WindowDecoration.updateCaptionContainerSurface") .build(); } - outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); - outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL - ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); - outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2; - startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth, outResult.mCaptionHeight) .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */) .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); - } - } - } - // 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()); + private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct, + RelayoutResult<T> outResult, Rect taskBounds) { + if (!mIsCaptionVisible) { + if (mWindowDecorationInsets != null) { + mWindowDecorationInsets.remove(wct); + mWindowDecorationInsets = null; } + return; + } + // 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 { - startT.hide(mCaptionContainerSurface); + // The customizable region can at most be equal to the caption bar. + if (params.hasInputFeatureSpy()) { + outResult.mCustomizableCaptionRegion.set(captionInsetsRect); + } + final Resources resources = mDecorWindowContext.getResources(); + 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); + } + } } - // Task surface itself - float shadowRadius; - final Point taskPosition = mTaskInfo.positionInParent; - if (isFullscreen) { - // Shadow is not needed for fullscreen tasks - shadowRadius = 0; - } else { - shadowRadius = loadDimension(resources, params.mShadowRadiusId); + 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); } + } + private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) { if (params.mSetTaskPositionAndCrop) { + final Point taskPosition = mTaskInfo.positionInParent; startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight) .setPosition(mTaskSurface, taskPosition.x, taskPosition.y); } - startT.setShadowRadius(mTaskSurface, shadowRadius) - .show(mTaskSurface); + float shadowRadius; + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + // Shadow is not needed for fullscreen tasks + shadowRadius = 0; + } else { + shadowRadius = + loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId); + } + startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface); finishT.setShadowRadius(mTaskSurface, shadowRadius); + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (!DesktopModeStatus.isVeiledResizeEnabled()) { // When fluid resize is enabled, add a background to freeform tasks @@ -382,7 +415,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } else if (!DesktopModeStatus.isVeiledResizeEnabled()) { startT.unsetColor(mTaskSurface); } + } + /** + * Updates a {@link SurfaceControlViewHost} to connect the window decoration surfaces with our + * View hierarchy. + * + * @param params parameters to use from the last relayout + * @param onDrawTransaction a transaction to apply in sync with #onDraw + * @param outResult results to use from the last relayout + * + */ + protected void updateViewHost(RelayoutParams params, + SurfaceControl.Transaction onDrawTransaction, RelayoutResult<T> outResult) { + 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. @@ -390,33 +436,38 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskInfo.getConfiguration(), mCaptionContainerSurface, null /* hostInputToken */); } - - // Caption view - mCaptionWindowManager.setConfiguration(taskConfig); + mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration()); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, + TYPE_APPLICATION, 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); + if (onDrawTransaction == null) { + throw new IllegalArgumentException("Trying to sync a null Transaction"); + } + mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } mViewHost.setView(outResult.mRootView, lp); + Trace.endSection(); } else { + Trace.beginSection("CaptionViewHostLayout-relayout"); if (params.mApplyStartTransactionOnDraw) { - mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT); + if (onDrawTransaction == null) { + throw new IllegalArgumentException("Trying to sync a null Transaction"); + } + mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } mViewHost.relayout(lp); + Trace.endSection(); } + Trace.endSection(); // CaptionViewHostLayout } private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, @@ -487,7 +538,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 +564,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) { @@ -559,15 +612,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * @param yPos y position of new window * @param width width of new window * @param height height of new window - * @return the {@link AdditionalWindow} that was added. + * @return the {@link AdditionalViewHostViewContainer} that was added. */ - AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t, - SurfaceSyncGroup ssg, int xPos, int yPos, int width, int height) { + AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix, + SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos, + int width, int height) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); SurfaceControl windowSurfaceControl = builder .setName(namePrefix + " of Task=" + mTaskInfo.taskId) .setContainerLayer() .setParent(mDecorationContainerSurface) + .setCallsite("WindowDecoration.addWindow") .build(); View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); @@ -575,9 +630,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setWindowCrop(windowSurfaceControl, width, height) .show(windowSurfaceControl); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(width, height, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + new WindowManager.LayoutParams(width, height, TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, @@ -585,7 +640,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory .create(mDecorWindowContext, mDisplay, windowManager); ssg.add(viewHost.getSurfacePackage(), () -> viewHost.setView(v, lp)); - return new AdditionalWindow(windowSurfaceControl, viewHost, + return new AdditionalViewHostViewContainer(windowSurfaceControl, viewHost, mSurfaceControlTransactionSupplier); } @@ -594,15 +649,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 +669,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 +684,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 +694,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. @@ -670,46 +732,55 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } - interface SurfaceControlViewHostFactory { + @VisibleForTesting + public interface SurfaceControlViewHostFactory { 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); + } } - /** - * Subclass for additional windows associated with this WindowDecoration - */ - static class AdditionalWindow { - SurfaceControl mWindowSurface; - SurfaceControlViewHost mWindowViewHost; - Supplier<SurfaceControl.Transaction> mTransactionSupplier; + 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); + } - AdditionalWindow(SurfaceControl surfaceControl, - SurfaceControlViewHost surfaceControlViewHost, - Supplier<SurfaceControl.Transaction> transactionSupplier) { - mWindowSurface = surfaceControl; - mWindowViewHost = surfaceControlViewHost; - mTransactionSupplier = transactionSupplier; + void remove(WindowContainerTransaction wct) { + wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar()); + wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures()); } - void releaseView() { - WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM(); + @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); + } - if (mWindowViewHost != null) { - mWindowViewHost.release(); - mWindowViewHost = null; - } - windowManager = null; - final SurfaceControl.Transaction t = mTransactionSupplier.get(); - boolean released = false; - if (mWindowSurface != null) { - t.remove(mWindowSurface); - mWindowSurface = null; - released = true; - } - if (released) { - t.apply(); - } + @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/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt new file mode 100644 index 000000000000..6c2c8fd46bc9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -0,0 +1,66 @@ +/* + * 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.additionalviewcontainer + +import android.content.Context +import android.graphics.PixelFormat +import android.view.LayoutInflater +import android.view.SurfaceControl +import android.view.View +import android.view.WindowManager + +/** + * An [AdditionalViewContainer] that uses the system [WindowManager] instance. Intended + * for view containers that should be above the status bar layer. + */ +class AdditionalSystemViewContainer( + private val context: Context, + layoutId: Int, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int +) : AdditionalViewContainer() { + override val view: View + + init { + view = LayoutInflater.from(context).inflate(layoutId, null) + val lp = WindowManager.LayoutParams( + width, height, x, y, + WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT + ) + lp.title = "Additional view container of Task=$taskId" + lp.setTrustedOverlay() + val wm: WindowManager? = context.getSystemService(WindowManager::class.java) + wm?.addView(view, lp) + } + + override fun releaseView() { + context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view) + } + + override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { + val lp = (view.layoutParams as WindowManager.LayoutParams).apply { + this.x = x.toInt() + this.y = y.toInt() + } + view.layoutParams = lp + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt new file mode 100644 index 000000000000..2650648a2cde --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt @@ -0,0 +1,35 @@ +/* + * 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.additionalviewcontainer + +import android.view.SurfaceControl +import android.view.View +import com.android.wm.shell.windowdecor.WindowDecoration + +/** + * Class for additional view containers associated with a [WindowDecoration]. + */ +abstract class AdditionalViewContainer internal constructor( +) { + abstract val view: View? + + /** Release the view associated with this container and perform needed cleanup. */ + abstract fun releaseView() + + /** Reposition the view container using provided coordinates. */ + abstract fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt new file mode 100644 index 000000000000..222761260289 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt @@ -0,0 +1,46 @@ +/* + * 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.additionalviewcontainer + +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import java.util.function.Supplier + +/** + * An [AdditionalViewContainer] that uses a [SurfaceControlViewHost] to show the window. + * Intended for view containers in freeform tasks that do not extend beyond task bounds. + */ +class AdditionalViewHostViewContainer( + private val windowSurface: SurfaceControl, + private val windowViewHost: SurfaceControlViewHost, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, +) : AdditionalViewContainer() { + + override val view + get() = windowViewHost.view + + override fun releaseView() { + windowViewHost.release() + val t = transactionSupplier.get() + t.remove(windowSurface) + t.apply() + } + + override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { + t.setPosition(windowSurface, x, y) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt new file mode 100644 index 000000000000..f7cfbfa88485 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt @@ -0,0 +1,94 @@ +/* + * 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.common + +import android.annotation.ColorInt +import android.annotation.IntRange +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.graphics.Color +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme + +/** The theme of a window decoration. */ +internal enum class Theme { LIGHT, DARK } + +/** Whether a [Theme] is light. */ +internal fun Theme.isLight(): Boolean = this == Theme.LIGHT + +/** Whether a [Theme] is dark. */ +internal fun Theme.isDark(): Boolean = this == Theme.DARK + +/** Returns a copy of the color with its [alpha] component replaced with the given value. */ +@ColorInt +internal fun @receiver:ColorInt Int.withAlpha(@IntRange(from = 0, to = 255) alpha: Int): Int = + Color.argb( + alpha, + Color.red(this), + Color.green(this), + Color.blue(this) + ) + +/** Common opacity values used in window decoration views. */ +const val OPACITY_100 = 255 +const val OPACITY_11 = 28 +const val OPACITY_12 = 31 +const val OPACITY_15 = 38 +const val OPACITY_40 = 102 +const val OPACITY_55 = 140 +const val OPACITY_65 = 166 + +/** + * Utility class for determining themes based on system settings and app's [RunningTaskInfo]. + */ +internal class DecorThemeUtil(private val context: Context) { + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + private val systemTheme: Theme + get() = if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES) { + Theme.DARK + } else { + Theme.LIGHT + } + + /** + * Returns the [Theme] used by the app with the given [RunningTaskInfo]. + */ + fun getAppTheme(task: RunningTaskInfo): Theme { + // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the + // TaskInfo/TaskDescription. + val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme + return if (Color.valueOf(backgroundColor).luminance() < 0.5) { + Theme.DARK + } else { + Theme.LIGHT + } + } + + /** + * Returns the [ColorScheme] to use to style window decorations based on the given + * [RunningTaskInfo]. + */ + fun getColorScheme(task: RunningTaskInfo): ColorScheme = when (getAppTheme(task)) { + Theme.LIGHT -> lightColors + Theme.DARK -> darkColors + } +} 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..7ade9876d28a 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?.topOpaqueSystemBarsAppearance ?: 0 return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0 } val TaskInfo.isLightCaptionBarAppearance: Boolean get() { - val appearance = taskDescription?.statusBarAppearance ?: 0 + val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 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/AppHandleViewHolder.kt index 6dcae2776847..8d822c252288 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/AppHandleViewHolder.kt @@ -1,3 +1,18 @@ +/* + * 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.viewholder import android.animation.ObjectAnimator @@ -12,14 +27,14 @@ import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators /** - * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It - * hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. + * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). + * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. */ -internal class DesktopModeFocusedWindowDecorationViewHolder( +internal class AppHandleViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: View.OnClickListener -) : DesktopModeWindowDecorationViewHolder(rootView) { +) : WindowDecorationViewHolder(rootView) { companion object { private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 @@ -65,7 +80,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/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt new file mode 100644 index 000000000000..46127b177bc3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -0,0 +1,500 @@ +/* + * 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.viewholder + +import android.annotation.ColorInt +import android.app.ActivityManager.RunningTaskInfo +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.view.View +import android.view.View.OnLongClickListener +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import androidx.core.content.withStyledAttributes +import androidx.core.view.isVisible +import com.android.internal.R.attr.materialColorOnSecondaryContainer +import com.android.internal.R.attr.materialColorOnSurface +import com.android.internal.R.attr.materialColorSecondaryContainer +import com.android.internal.R.attr.materialColorSurfaceContainerHigh +import com.android.internal.R.attr.materialColorSurfaceContainerLow +import com.android.internal.R.attr.materialColorSurfaceDim +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.windowdecor.MaximizeButtonView +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.OPACITY_100 +import com.android.wm.shell.windowdecor.common.OPACITY_11 +import com.android.wm.shell.windowdecor.common.OPACITY_15 +import com.android.wm.shell.windowdecor.common.OPACITY_55 +import com.android.wm.shell.windowdecor.common.OPACITY_65 +import com.android.wm.shell.windowdecor.common.Theme +import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance +import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance + +/** + * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts + * finer controls such as a close window button and an "app info" section to pull up additional + * controls. + */ +internal class AppHeaderViewHolder( + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: View.OnClickListener, + onLongClickListener: OnLongClickListener, + onCaptionGenericMotionListener: View.OnGenericMotionListener, + appName: CharSequence, + appIconBitmap: Bitmap, + onMaximizeHoverAnimationFinishedListener: () -> Unit +) : WindowDecorationViewHolder(rootView) { + + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + /** + * The corner radius to apply to the app chip, maximize and close button's background drawable. + **/ + private val headerButtonsRippleRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius) + + /** + * The app chip, maximize and close button's height extends to the top & bottom edges of the + * header, and their width may be larger than their height. This is by design to increase the + * clickable and hover-able bounds of the view as much as possible. However, to prevent the + * ripple drawable from being as large as the views (and asymmetrical), insets are applied to + * the background ripple drawable itself to give the appearance of a smaller button + * (with padding between itself and the header edges / sibling buttons) but without affecting + * its touchable region. + */ + private val appChipDrawableInsets = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_app_chip_ripple_inset_vertical) + ) + private val maximizeDrawableInsets = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_vertical), + horizontal = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_horizontal) + ) + private val closeDrawableInsets = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_vertical), + horizontal = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_horizontal) + ) + + private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) + private val captionHandle: View = rootView.requireViewById(R.id.caption_handle) + private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button) + private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window) + private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button) + private val maximizeButtonView: MaximizeButtonView = + rootView.requireViewById(R.id.maximize_button_view) + private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window) + private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name) + private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon) + val appNameTextWidth: Int + get() = appNameTextView.width + + init { + captionView.setOnTouchListener(onCaptionTouchListener) + captionHandle.setOnTouchListener(onCaptionTouchListener) + openMenuButton.setOnClickListener(onCaptionButtonClickListener) + openMenuButton.setOnTouchListener(onCaptionTouchListener) + closeWindowButton.setOnClickListener(onCaptionButtonClickListener) + maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener) + maximizeWindowButton.setOnTouchListener(onCaptionTouchListener) + maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener) + maximizeWindowButton.onLongClickListener = onLongClickListener + closeWindowButton.setOnTouchListener(onCaptionTouchListener) + appNameTextView.text = appName + appIconImageView.setImageBitmap(appIconBitmap) + maximizeButtonView.onHoverAnimationFinishedListener = + onMaximizeHoverAnimationFinishedListener + } + + override fun bindData(taskInfo: RunningTaskInfo) { + if (Flags.enableThemedAppHeaders()) { + bindDataWithThemedHeaders(taskInfo) + } else { + bindDataLegacy(taskInfo) + } + } + + private fun bindDataLegacy(taskInfo: RunningTaskInfo) { + captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo)) + val color = getAppNameAndButtonColor(taskInfo) + val alpha = Color.alpha(color) + closeWindowButton.imageTintList = ColorStateList.valueOf(color) + maximizeWindowButton.imageTintList = ColorStateList.valueOf(color) + expandMenuButton.imageTintList = ColorStateList.valueOf(color) + appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance + appNameTextView.setTextColor(color) + appIconImageView.imageAlpha = alpha + maximizeWindowButton.imageAlpha = alpha + closeWindowButton.imageAlpha = alpha + expandMenuButton.imageAlpha = alpha + context.withStyledAttributes( + set = null, + attrs = intArrayOf( + android.R.attr.selectableItemBackground, + android.R.attr.selectableItemBackgroundBorderless + ), + defStyleAttr = 0, + defStyleRes = 0 + ) { + openMenuButton.background = getDrawable(0) + maximizeWindowButton.background = getDrawable(1) + closeWindowButton.background = getDrawable(1) + } + maximizeButtonView.setAnimationTints(isDarkMode()) + } + + private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) { + val header = fillHeaderInfo(taskInfo) + val headerStyle = getHeaderStyle(header) + + // Caption Background + when (headerStyle.background) { + is HeaderStyle.Background.Opaque -> { + captionView.setBackgroundColor(headerStyle.background.color) + } + HeaderStyle.Background.Transparent -> { + captionView.setBackgroundColor(Color.TRANSPARENT) + } + } + + // Caption Foreground + val foregroundColor = headerStyle.foreground.color + val foregroundAlpha = headerStyle.foreground.opacity + val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha) + // App chip. + openMenuButton.apply { + background = createRippleDrawable( + color = foregroundColor, + cornerRadius = headerButtonsRippleRadius, + drawableInsets = appChipDrawableInsets, + ) + expandMenuButton.imageTintList = colorStateList + appNameTextView.apply { + isVisible = header.type == Header.Type.DEFAULT + setTextColor(colorStateList) + } + appIconImageView.imageAlpha = foregroundAlpha + } + // Maximize button. + maximizeButtonView.setAnimationTints( + darkMode = header.appTheme == Theme.DARK, + iconForegroundColor = colorStateList, + baseForegroundColor = foregroundColor, + rippleDrawable = createRippleDrawable( + color = foregroundColor, + cornerRadius = headerButtonsRippleRadius, + drawableInsets = maximizeDrawableInsets + ) + ) + // Close button. + closeWindowButton.apply { + imageTintList = colorStateList + background = createRippleDrawable( + color = foregroundColor, + cornerRadius = headerButtonsRippleRadius, + drawableInsets = closeDrawableInsets + ) + } + } + + override fun onHandleMenuOpened() {} + + override fun onHandleMenuClosed() {} + + fun setAnimatingTaskResize(animatingTaskResize: Boolean) { + // If animating a task resize, cancel any running hover animations + if (animatingTaskResize) { + maximizeButtonView.cancelHoverAnimation() + } + maximizeButtonView.hoverDisabled = animatingTaskResize + } + + fun onMaximizeWindowHoverExit() { + maximizeButtonView.cancelHoverAnimation() + } + + fun onMaximizeWindowHoverEnter() { + maximizeButtonView.startHoverAnimation() + } + + private fun getHeaderStyle(header: Header): HeaderStyle { + return HeaderStyle( + background = getHeaderBackground(header), + foreground = getHeaderForeground(header) + ) + } + + private fun getHeaderBackground(header: Header): HeaderStyle.Background { + return when (header.type) { + Header.Type.DEFAULT -> { + when (header.appTheme) { + Theme.LIGHT -> { + if (header.isFocused) { + HeaderStyle.Background.Opaque(lightColors.secondaryContainer.toArgb()) + } else { + HeaderStyle.Background.Opaque(lightColors.surfaceContainerLow.toArgb()) + } + } + Theme.DARK -> { + if (header.isFocused) { + HeaderStyle.Background.Opaque(darkColors.surfaceContainerHigh.toArgb()) + } else { + HeaderStyle.Background.Opaque(darkColors.surfaceDim.toArgb()) + } + } + } + } + Header.Type.CUSTOM -> HeaderStyle.Background.Transparent + } + } + + private fun getHeaderForeground(header: Header): HeaderStyle.Foreground { + return when (header.type) { + Header.Type.DEFAULT -> { + when (header.appTheme) { + Theme.LIGHT -> { + if (header.isFocused) { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_100 + ) + } else { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_65 + ) + } + } + Theme.DARK -> { + if (header.isFocused) { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_100 + ) + } else { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_55 + ) + } + } + } + } + Header.Type.CUSTOM -> when { + header.isAppearanceCaptionLight && header.isFocused -> { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_100 + ) + } + header.isAppearanceCaptionLight && !header.isFocused -> { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_65 + ) + } + !header.isAppearanceCaptionLight && header.isFocused -> { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_100 + ) + } + !header.isAppearanceCaptionLight && !header.isFocused -> { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_55 + ) + } + else -> error("No other combination expected header=$header") + } + } + } + + private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header { + return Header( + type = if (taskInfo.isTransparentCaptionBarAppearance) { + Header.Type.CUSTOM + } else { + Header.Type.DEFAULT + }, + appTheme = decorThemeUtil.getAppTheme(taskInfo), + isFocused = taskInfo.isFocused, + isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance + ) + } + + @ColorInt + private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { + return Color.argb( + alpha, + Color.red(color), + Color.green(color), + Color.blue(color) + ) + } + + private fun createRippleDrawable( + @ColorInt color: Int, + cornerRadius: Int, + drawableInsets: DrawableInsets, + ): RippleDrawable { + return RippleDrawable( + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_hovered), + intArrayOf(android.R.attr.state_pressed), + intArrayOf(), + ), + intArrayOf( + replaceColorAlpha(color, OPACITY_11), + replaceColorAlpha(color, OPACITY_15), + Color.TRANSPARENT + ) + ), + null /* content */, + LayerDrawable(arrayOf( + ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { cornerRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = Color.WHITE + } + )).apply { + require(numberOfLayers == 1) { "Must only contain one layer" } + setLayerInset(0 /* index */, + drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b) + } + ) + } + + private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) { + constructor(vertical: Int = 0, horizontal: Int = 0) : + this(horizontal, vertical, horizontal, vertical) + } + + private data class Header( + val type: Type, + val appTheme: Theme, + val isFocused: Boolean, + val isAppearanceCaptionLight: Boolean, + ) { + enum class Type { DEFAULT, CUSTOM } + } + + private data class HeaderStyle( + val background: Background, + val foreground: Foreground + ) { + data class Foreground( + @ColorInt val color: Int, + val opacity: Int + ) + + sealed class Background { + data object Transparent : Background() + data class Opaque(@ColorInt val color: Int) : Background() + } + } + + @ColorInt + private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { + if (taskInfo.isTransparentCaptionBarAppearance) { + return Color.TRANSPARENT + } + val materialColorAttr: Int = + if (isDarkMode()) { + if (!taskInfo.isFocused) { + materialColorSurfaceContainerHigh + } else { + materialColorSurfaceDim + } + } else { + if (!taskInfo.isFocused) { + materialColorSurfaceContainerLow + } else { + materialColorSecondaryContainer + } + } + context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { + return getColor(0, 0) + } + return 0 + } + + @ColorInt + private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int { + val materialColorAttr = when { + taskInfo.isTransparentCaptionBarAppearance && + taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer + taskInfo.isTransparentCaptionBarAppearance && + !taskInfo.isLightCaptionBarAppearance -> materialColorOnSurface + isDarkMode() -> materialColorOnSurface + else -> materialColorOnSecondaryContainer + } + val appDetailsOpacity = when { + isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY + !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY + else -> FOCUSED_OPACITY + } + context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { + val color = getColor(0, 0) + return if (appDetailsOpacity == FOCUSED_OPACITY) { + color + } else { + Color.argb( + appDetailsOpacity, + Color.red(color), + Color.green(color), + Color.blue(color) + ) + } + } + return 0 + } + + private fun isDarkMode(): Boolean { + return context.resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + } + + companion object { + private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" + + private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% + private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% + private const val FOCUSED_OPACITY = 255 + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt deleted file mode 100644 index 58bbb030da01..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.android.wm.shell.windowdecor.viewholder - -import android.annotation.ColorInt -import android.app.ActivityManager.RunningTaskInfo -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Color -import android.view.View -import android.view.View.OnLongClickListener -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView -import androidx.core.content.withStyledAttributes -import androidx.core.view.isVisible -import com.android.internal.R.attr.materialColorOnSecondaryContainer -import com.android.internal.R.attr.materialColorOnSurface -import com.android.internal.R.attr.materialColorSecondaryContainer -import com.android.internal.R.attr.materialColorSurfaceContainerHigh -import com.android.internal.R.attr.materialColorSurfaceContainerLow -import com.android.internal.R.attr.materialColorSurfaceDim -import com.android.wm.shell.R -import com.android.wm.shell.windowdecor.MaximizeButtonView -import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance -import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance - -/** - * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts - * finer controls such as a close window button and an "app info" section to pull up additional - * controls. - */ -internal class DesktopModeAppControlsWindowDecorationViewHolder( - rootView: View, - onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener, - onLongClickListener: OnLongClickListener, - onCaptionGenericMotionListener: View.OnGenericMotionListener, - appName: CharSequence, - appIconBitmap: Bitmap, - onMaximizeHoverAnimationFinishedListener: () -> Unit -) : DesktopModeWindowDecorationViewHolder(rootView) { - - private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) - private val captionHandle: View = rootView.requireViewById(R.id.caption_handle) - private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button) - private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window) - private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button) - private val maximizeButtonView: MaximizeButtonView = - rootView.requireViewById(R.id.maximize_button_view) - private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window) - private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name) - private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon) - val appNameTextWidth: Int - get() = appNameTextView.width - - init { - captionView.setOnTouchListener(onCaptionTouchListener) - captionHandle.setOnTouchListener(onCaptionTouchListener) - openMenuButton.setOnClickListener(onCaptionButtonClickListener) - openMenuButton.setOnTouchListener(onCaptionTouchListener) - closeWindowButton.setOnClickListener(onCaptionButtonClickListener) - maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener) - maximizeWindowButton.setOnTouchListener(onCaptionTouchListener) - maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener) - maximizeWindowButton.onLongClickListener = onLongClickListener - closeWindowButton.setOnTouchListener(onCaptionTouchListener) - appNameTextView.text = appName - appIconImageView.setImageBitmap(appIconBitmap) - maximizeButtonView.onHoverAnimationFinishedListener = - onMaximizeHoverAnimationFinishedListener - } - - override fun bindData(taskInfo: RunningTaskInfo) { - captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo)) - val color = getAppNameAndButtonColor(taskInfo) - val alpha = Color.alpha(color) - closeWindowButton.imageTintList = ColorStateList.valueOf(color) - maximizeWindowButton.imageTintList = ColorStateList.valueOf(color) - expandMenuButton.imageTintList = ColorStateList.valueOf(color) - appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance - appNameTextView.setTextColor(color) - appIconImageView.imageAlpha = alpha - maximizeWindowButton.imageAlpha = alpha - closeWindowButton.imageAlpha = alpha - expandMenuButton.imageAlpha = alpha - - maximizeButtonView.setAnimationTints(isDarkMode()) - } - - override fun onHandleMenuOpened() {} - - override fun onHandleMenuClosed() {} - - fun setAnimatingTaskResize(animatingTaskResize: Boolean) { - // If animating a task resize, cancel any running hover animations - if (animatingTaskResize) { - maximizeButtonView.cancelHoverAnimation() - } - maximizeButtonView.hoverDisabled = animatingTaskResize - } - - fun onMaximizeWindowHoverExit() { - maximizeButtonView.cancelHoverAnimation() - } - - fun onMaximizeWindowHoverEnter() { - maximizeButtonView.startHoverAnimation() - } - - @ColorInt - private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { - if (taskInfo.isTransparentCaptionBarAppearance) { - return Color.TRANSPARENT - } - val materialColorAttr: Int = - if (isDarkMode()) { - if (!taskInfo.isFocused) { - materialColorSurfaceContainerHigh - } else { - materialColorSurfaceDim - } - } else { - if (!taskInfo.isFocused) { - materialColorSurfaceContainerLow - } else { - materialColorSecondaryContainer - } - } - context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { - return getColor(0, 0) - } - return 0 - } - - @ColorInt - private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int { - val materialColorAttr = when { - taskInfo.isTransparentCaptionBarAppearance && - taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer - taskInfo.isTransparentCaptionBarAppearance && - !taskInfo.isLightCaptionBarAppearance -> materialColorOnSurface - isDarkMode() -> materialColorOnSurface - else -> materialColorOnSecondaryContainer - } - val appDetailsOpacity = when { - isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY - !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY - else -> FOCUSED_OPACITY - } - context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { - val color = getColor(0, 0) - return if (appDetailsOpacity == FOCUSED_OPACITY) { - color - } else { - Color.argb( - appDetailsOpacity, - Color.red(color), - Color.green(color), - Color.blue(color) - ) - } - } - return 0 - } - - private fun isDarkMode(): Boolean { - return context.resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES - } - - companion object { - private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" - private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% - private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% - private const val FOCUSED_OPACITY = 255 - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 81bc34c876b6..5ae8d252a908 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -1,3 +1,18 @@ +/* + * 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.viewholder import android.app.ActivityManager.RunningTaskInfo @@ -8,7 +23,7 @@ import android.view.View * Encapsulates the root [View] of a window decoration and its children to facilitate looking up * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. */ -internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { +internal abstract class WindowDecorationViewHolder(rootView: View) { val context: Context = rootView.context /** diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 0f24bb549158..b8a19ad35307 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -13,3 +13,5 @@ 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..d03d7799d675 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,25 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Test + @FlakyTest(bugId = 336510055) + override fun entireScreenCovered() { + super.entireScreenCovered() + } + 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/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp index f813b0d3b0b7..0fe7a16be851 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp @@ -65,9 +65,11 @@ android_test { android_test { name: "WMShellFlickerTestsSplitScreenGroup2", + defaults: ["WMShellFlickerTestsDefault"], manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.splitscreen", instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsSplitScreenBase-src", ":WMShellFlickerTestsSplitScreenGroup2-src", 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/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt new file mode 100644 index 000000000000..dad5db94d062 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.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.splitscreen + +import android.platform.test.annotations.Presubmit +import android.tools.Rotation +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.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark +import com.android.wm.shell.flicker.utils.ICommonAssertions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch between two split pairs. + * + * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTest) : + MultipleShowImeRequestsInSplitScreenBenchmark(flicker), ICommonAssertions { + override val transition: FlickerBuilder.() -> Unit + get() = { + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @Presubmit + @Test + fun imeLayerAlwaysVisible() = + flicker.assertLayers { + this.isVisible(ComponentNameMatcher.IME) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } +} 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..d34998815fca 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 @@ -47,7 +47,7 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen` */ @RequiresDevice -@Postsubmit +@FlakyTest(bugId = 293578017) @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -61,7 +61,6 @@ class UnlockKeyguardToSplitScreen(override val flicker: LegacyFlickerTest) : } @Test - @FlakyTest(bugId = 293578017) override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() 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/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt new file mode 100644 index 000000000000..249253185607 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt @@ -0,0 +1,75 @@ +/* + * 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.splitscreen.benchmark + +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 androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +abstract class MultipleShowImeRequestsInSplitScreenBenchmark( + override val flicker: LegacyFlickerTest +) : SplitScreenBase(flicker) { + override val primaryApp = ImeAppHelper(instrumentation) + override val defaultTeardown: FlickerBuilder.() -> Unit + get() = { + teardown { + primaryApp.closeIME(wmHelper) + super.defaultTeardown + } + } + + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit( + wmHelper, + tapl, + device, + primaryApp, + secondaryApp, + flicker.scenario.startRotation + ) + // initially open the IME + primaryApp.openIME(wmHelper) + } + transitions { + for (i in 1..OPEN_IME_COUNT) { + primaryApp.openIME(wmHelper) + } + } + } + + companion object { + const val OPEN_IME_COUNT = 30 + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt index 4b106034b2b5..51074f634e30 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt @@ -25,7 +25,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) { protected val context: Context = instrumentation.context - protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation) + protected open val primaryApp = SplitScreenUtils.getPrimary(instrumentation) protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation) protected open val defaultSetup: FlickerBuilder.() -> Unit = { 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..f9b4108bc8c2 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,11 @@ 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_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; @@ -62,6 +63,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellInit; @@ -78,7 +80,7 @@ import java.util.Optional; * Tests for the shell task organizer. * * Build/Install/Run: - * atest WMShellUnitTests:ShellTaskOrganizerTests + * atest WMShellUnitTests:ShellTaskOrganizerTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -92,6 +94,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase { private ShellExecutor mTestExecutor; @Mock private ShellCommandHandler mShellCommandHandler; + @Mock + private RecentTasksController mRecentTasksController; private ShellTaskOrganizer mOrganizer; private ShellInit mShellInit; @@ -120,6 +124,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { private class TrackingLocusIdListener implements ShellTaskOrganizer.LocusIdListener { final SparseArray<LocusId> visibleLocusTasks = new SparseArray<>(); final SparseArray<LocusId> invisibleLocusTasks = new SparseArray<>(); + @Override public void onVisibilityChanged(int taskId, LocusId locus, boolean visible) { if (visible) { @@ -130,18 +135,18 @@ public class ShellTaskOrganizerTests extends ShellTestCase { } } - @Before public void setUp() { MockitoAnnotations.initMocks(this); try { doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) .when(mTaskOrganizerController).registerTaskOrganizer(any()); - } catch (RemoteException e) {} + } catch (RemoteException e) { + } mShellInit = spy(new ShellInit(mTestExecutor)); mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler, - mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(), - mTestExecutor)); + mTaskOrganizerController, mCompatUI, Optional.empty(), + Optional.of(mRecentTasksController), mTestExecutor)); mShellInit.init(); } @@ -163,7 +168,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testTaskLeashReleasedAfterVanished() throws RemoteException { assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession()) .setName("task").build(); mOrganizer.registerOrganizer(); @@ -188,8 +193,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testRegisterWithExistingTasks() throws RemoteException { // Setup some tasks - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW); ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>(); taskInfos.add(new TaskAppearedInfo(task1, new SurfaceControl())); taskInfos.add(new TaskAppearedInfo(task2, new SurfaceControl())); @@ -208,10 +213,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAppearedVanished() { - RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener listener = new TrackingTaskListener(); mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW); - mOrganizer.onTaskAppeared(taskInfo, null); + mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null); assertTrue(listener.appeared.contains(taskInfo)); mOrganizer.onTaskVanished(taskInfo); @@ -220,7 +225,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAddListenerExistingTasks() { - RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); mOrganizer.onTaskAppeared(taskInfo, null); TrackingTaskListener listener = new TrackingTaskListener(); @@ -230,9 +235,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAddListenerForMultipleTypes() { - RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, null); - RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW); mOrganizer.onTaskAppeared(taskInfo2, null); TrackingTaskListener listener = new TrackingTaskListener(); @@ -247,10 +252,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testRemoveListenerForMultipleTypes() { - RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); - mOrganizer.onTaskAppeared(taskInfo1, null); - RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); - mOrganizer.onTaskAppeared(taskInfo2, null); + RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); + RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo2, /* leash= */ null); TrackingTaskListener listener = new TrackingTaskListener(); mOrganizer.addListenerForType(listener, @@ -267,12 +272,12 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testWindowingModeChange() { - RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); TrackingTaskListener pipListener = new TrackingTaskListener(); mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); mOrganizer.addListenerForType(pipListener, TASK_LISTENER_TYPE_PIP); - mOrganizer.onTaskAppeared(taskInfo, null); + mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null); assertTrue(mwListener.appeared.contains(taskInfo)); assertTrue(pipListener.appeared.isEmpty()); @@ -284,11 +289,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAddListenerForTaskId_afterTypeListener() { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); TrackingTaskListener task1Listener = new TrackingTaskListener(); mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); assertTrue(mwListener.appeared.contains(task1)); // Add task 1 specific listener @@ -299,11 +304,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAddListenerForTaskId_beforeTypeListener() { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); TrackingTaskListener task1Listener = new TrackingTaskListener(); - mOrganizer.onTaskAppeared(task1, null); - mOrganizer.addListenerForTaskId(task1Listener, 1); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1); assertTrue(task1Listener.appeared.contains(task1)); mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); @@ -312,7 +317,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testGetTaskListener() { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); @@ -324,7 +329,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { // Priority goes to the cookie listener so we would expect the task appear to show up there // instead of the multi-window type listener. - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); assertTrue(cookieListener.appeared.contains(task1)); assertFalse(mwListener.appeared.contains(task1)); @@ -332,7 +337,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { boolean gotException = false; try { - mOrganizer.addListenerForTaskId(task1Listener, 1); + mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1); } catch (Exception e) { gotException = true; } @@ -343,26 +348,27 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testGetParentTaskListener() { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); mOrganizer.addListenerForTaskId(mwListener, task1.taskId); RunningTaskInfo task2 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); task2.parentTaskId = task1.taskId; - mOrganizer.onTaskAppeared(task2, null); + mOrganizer.onTaskAppeared(task2, /* leash= */ null); assertTrue(mwListener.appeared.contains(task2)); } @Test public void testOnSizeCompatActivityChanged() { - final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN); + final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, + WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; taskInfo1.appCompatTaskInfo.topActivityInSizeCompat = false; final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); - mOrganizer.onTaskAppeared(taskInfo1, null); + mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); // sizeCompatActivity is null if top activity is not in size compat. verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); @@ -394,12 +400,13 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testOnEligibleForLetterboxEducationActivityChanged() { - final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN); + final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, + WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; taskInfo1.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = false; final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); - mOrganizer.onTaskAppeared(taskInfo1, null); + mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); // Task listener sent to compat UI is null if top activity isn't eligible for letterbox // education. @@ -433,12 +440,14 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testOnCameraCompatActivityChanged() { - final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 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); + mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); // Task listener sent to compat UI is null if top activity doesn't request a camera // compat control. @@ -449,7 +458,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 +470,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 +483,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 +494,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 +505,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); @@ -509,20 +518,20 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testAddLocusListener() { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); task1.isVisible = true; task1.mTopActivityLocusId = new LocusId("10"); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FULLSCREEN); + RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_FULLSCREEN); task2.isVisible = true; task2.mTopActivityLocusId = new LocusId("20"); - RunningTaskInfo task3 = createTaskInfo(3, WINDOWING_MODE_FULLSCREEN); + RunningTaskInfo task3 = createTaskInfo(/* taskId= */ 3, WINDOWING_MODE_FULLSCREEN); task3.isVisible = true; - mOrganizer.onTaskAppeared(task1, null); - mOrganizer.onTaskAppeared(task2, null); - mOrganizer.onTaskAppeared(task3, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + mOrganizer.onTaskAppeared(task2, /* leash= */ null); + mOrganizer.onTaskAppeared(task3, /* leash= */ null); TrackingLocusIdListener listener = new TrackingLocusIdListener(); mOrganizer.addLocusIdListener(listener); @@ -538,11 +547,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase { TrackingLocusIdListener listener = new TrackingLocusIdListener(); mOrganizer.addLocusIdListener(listener); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); task1.mTopActivityLocusId = new LocusId("10"); task1.isVisible = true; - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); assertTrue(listener.visibleLocusTasks.contains(task1.taskId)); assertEquals(listener.visibleLocusTasks.get(task1.taskId), task1.mTopActivityLocusId); @@ -557,9 +566,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase { TrackingLocusIdListener listener = new TrackingLocusIdListener(); mOrganizer.addLocusIdListener(listener); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); task1.isVisible = true; - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); assertEquals(listener.visibleLocusTasks.size(), 0); task1.mTopActivityLocusId = new LocusId("10"); @@ -584,9 +593,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase { TrackingLocusIdListener listener = new TrackingLocusIdListener(); mOrganizer.addLocusIdListener(listener); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); task1.isVisible = true; - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); task1.mTopActivityLocusId = new LocusId("10"); mOrganizer.onTaskInfoChanged(task1); @@ -608,9 +617,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase { TrackingLocusIdListener listener = new TrackingLocusIdListener(); mOrganizer.addLocusIdListener(listener); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); task1.isVisible = true; - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); assertEquals(listener.visibleLocusTasks.size(), 0); assertEquals(listener.invisibleLocusTasks.size(), 0); @@ -626,20 +635,63 @@ public class ShellTaskOrganizerTests extends ShellTestCase { @Test public void testOnSizeCompatRestartButtonClicked() throws RemoteException { - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); task1.token = mock(WindowContainerToken.class); - mOrganizer.onTaskAppeared(task1, null); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); mOrganizer.onSizeCompatRestartButtonClicked(task1.taskId); verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } + @Test + public void testRecentTasks_onTaskAppeared_shouldNotifyTaskController() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM); + + mOrganizer.onTaskAppeared(task1, null); + + verify(mRecentTasksController).onTaskAdded(task1); + } + + @Test + public void testRecentTasks_onTaskVanished_shouldNotifyTaskController() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + + mOrganizer.onTaskVanished(task1); + + verify(mRecentTasksController).onTaskRemoved(task1); + } + + @Test + public void testRecentTasks_visibilityChanges_shouldNotifyTaskController() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM); + task2.isVisible = false; + + mOrganizer.onTaskInfoChanged(task2); + + verify(mRecentTasksController).onTaskRunningInfoChanged(task2); + } + + @Test + public void testRecentTasks_windowingModeChanges_shouldNotifyTaskController() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM); + + mOrganizer.onTaskInfoChanged(task2); + + verify(mRecentTasksController).onTaskRunningInfoChanged(task2); + } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.isVisible = true; return taskInfo; } } 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..bd20c1143262 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; @@ -30,14 +32,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.animation.Animator; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.window.TransitionInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -54,12 +61,16 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { + @Rule + public SetFlagsRule mRule = new SetFlagsRule(); + @Before public void setup() { super.setUp(); doNothing().when(mController).onAnimationFinished(any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -85,6 +96,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim verify(mController).onAnimationFinished(mTransition); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testChangesBehindStartingWindow() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -99,8 +111,24 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim assertEquals(0, animator.getDuration()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @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()); + } + + @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test - public void testInvalidCustomAnimation() { + public void testInvalidCustomAnimation_disableAnimationOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) .build(); @@ -115,4 +143,22 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim // An invalid custom animation is equivalent to jump-cut. assertEquals(0, animator.getDuration()); } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testInvalidCustomAnimation_enableAnimationOptionsPerChange() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .build(); + info.getChanges().getFirst().setAnimationOptions(TransitionInfo.AnimationOptions + .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, + 0 /* backgroundColor */, false /* overrideTaskTransition */)); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */), + new ArrayList<>()); + + // An invalid custom animation is equivalent to jump-cut. + assertEquals(0, animator.getDuration()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index 974d69b2ac5d..39d55079ca3a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -32,6 +32,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.animation.Animator; import android.animation.ValueAnimator; import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -39,9 +42,11 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,6 +64,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500); private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500); + @Rule + public SetFlagsRule mRule = new SetFlagsRule(); + @Before public void setup() { super.setUp(); @@ -66,11 +74,13 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testInstantiate() { verify(mShellInit).addInitCallback(any(), any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOnInit() { mController.onInit(); @@ -78,6 +88,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mTransitions).addHandler(mController); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testSetAnimScaleSetting() { mController.setAnimScaleSetting(1.0f); @@ -86,6 +97,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mAnimSpec).setAnimScaleSetting(1.0f); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsNonActivityEmbeddingChange() { final TransitionInfo.Change nonEmbeddedOpen = createChange(0 /* flags */); @@ -122,6 +134,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation assertFalse(info2.getChanges().contains(nonEmbeddedClose)); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -138,6 +151,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishCallback); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsActivityEmbeddingSplitChange() { // Change that occupies only part of the Task. @@ -155,6 +169,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() { // Change that is entering ActivityEmbedding split. @@ -171,6 +186,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() { // Change that is exiting ActivityEmbedding split. @@ -187,8 +203,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test - public void testShouldAnimate_containsAnimationOptions() { + public void testShouldAnimate_containsAnimationOptions_disableAnimOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) .build(); @@ -206,6 +223,28 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation assertFalse(mController.shouldAnimate(info)); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testShouldAnimate_containsAnimationOptions_enableAnimOptionsPerChange() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) + .build(); + final TransitionInfo.Change change = info.getChanges().getFirst(); + + change.setAnimationOptions(TransitionInfo.AnimationOptions + .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, + 0 /* backgroundColor */, false /* overrideTaskTransition */)); + assertTrue(mController.shouldAnimate(info)); + + change.setAnimationOptions(TransitionInfo.AnimationOptions + .makeSceneTransitionAnimOptions()); + assertFalse(mController.shouldAnimate(info)); + + change.setAnimationOptions(TransitionInfo.AnimationOptions.makeCrossProfileAnimOptions()); + assertFalse(mController.shouldAnimate(info)); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @UiThreadTest @Test public void testMergeAnimation() { @@ -242,6 +281,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mFinishCallback).onTransitionFinished(any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOnAnimationFinished() { // Should not call finish when there is no transition. 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..57e469d5cbd2 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,12 +113,17 @@ 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; + private Rect mTouchableRegion; @Before public void setUp() throws Exception { @@ -131,12 +136,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( @@ -152,6 +159,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellCommandHandler); mShellInit.init(); mShellExecutor.flushAll(); + mTouchableRegion = new Rect(0, 0, 100, 100); + mController.mTouchableArea.set(mTouchableRegion); } private void createNavigationInfo(int backType, @@ -163,7 +172,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(enableAnimation) - .setAnimationCallback(isAnimationCallback); + .setAnimationCallback(isAnimationCallback) + .setTouchableRegion(mTouchableRegion); createNavigationInfo(builder); } @@ -178,7 +188,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(), @@ -226,7 +238,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setType(type) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(true) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion)); triggerBackGesture(); simulateRemoteAnimationStart(); mShellExecutor.flushAll(); @@ -347,6 +360,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 +401,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 +420,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, @@ -476,7 +517,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setType(type) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(true) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion)); triggerBackGesture(); simulateRemoteAnimationStart(); mShellExecutor.flushAll(); @@ -499,7 +541,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; @@ -507,7 +549,9 @@ public class BackAnimationControllerTest extends ShellTestCase { createNavigationInfo(new BackNavigationInfo.Builder() .setType(type) .setOnBackInvokedCallback(mAppCallback) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion) + .setAppProgressAllowed(true)); triggerBackGesture(); mShellExecutor.flushAll(); releaseBackGesture(); @@ -518,8 +562,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 +572,33 @@ 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)) + .setTouchableRegion(mTouchableRegion)); + 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 +609,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 +652,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 +691,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..4d0348b4f470 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.back; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import android.os.Handler; import android.os.Looper; @@ -95,16 +96,33 @@ public class BackProgressAnimatorTest { // Trigger animation cancel, the target progress should be 0. mTargetProgress = 0; mTargetProgressCalled = new CountDownLatch(1); - CountDownLatch cancelCallbackCalled = new CountDownLatch(1); + CountDownLatch finishCallbackCalled = new CountDownLatch(1); mMainThreadHandler.post( - () -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown())); - cancelCallbackCalled.await(1, TimeUnit.SECONDS); + () -> mProgressAnimator.onBackCancelled(finishCallbackCalled::countDown)); + finishCallbackCalled.await(1, TimeUnit.SECONDS); mTargetProgressCalled.await(1, TimeUnit.SECONDS); assertNotNull(mReceivedBackEvent); assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); } @Test + public void testBackInvoked() 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); + + // Trigger back invoked animation + CountDownLatch finishCallbackCalled = new CountDownLatch(1); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown)); + assertTrue("onBackInvoked finishCallback never called", + finishCallbackCalled.await(1, TimeUnit.SECONDS)); + } + + @Test public void testResetCallsCancelCallbackImmediately() throws InterruptedException { // Give the animator some progress. final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); @@ -134,6 +152,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..93e405131a58 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; @@ -1168,9 +1170,9 @@ public class BubbleDataTest extends ShellTestCase { // Verify the update has the removals. BubbleData.Update update = mUpdateCaptor.getValue(); assertThat(update.removedBubbles.get(0)).isEqualTo( - Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED)); + Pair.create(mBubbleA2, Bubbles.DISMISS_USER_ACCOUNT_REMOVED)); assertThat(update.removedBubbles.get(1)).isEqualTo( - Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED)); + Pair.create(mBubbleA1, Bubbles.DISMISS_USER_ACCOUNT_REMOVED)); // Verify no A bubbles in active or overflow. assertBubbleListContains(mBubbleC1, mBubbleB3); @@ -1191,20 +1193,93 @@ 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); + assertThat(update.expandedChanged).isFalse(); + assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey()); + } + + @Test + public void test_getInitialStateForBubbleBar_includesExpandedState() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleData.setExpanded(true); + + 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); + assertThat(update.expandedChanged).isTrue(); + assertThat(update.expanded).isTrue(); + assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey()); + } + + @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/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt index 432909f18813..5b22eddcb6ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt @@ -32,7 +32,17 @@ class BubbleInfoTest : ShellTestCase() { @Test fun bubbleInfo() { val bubbleInfo = - BubbleInfo("key", 0, "shortcut id", null, 6, "com.some.package", "title", true) + BubbleInfo( + "key", + 0, + "shortcut id", + null, + 6, + "com.some.package", + "title", + "Some app", + true + ) val parcel = Parcel.obtain() bubbleInfo.writeToParcel(parcel, PARCELABLE_WRITE_RETURN_VALUE) parcel.setDataPosition(0) @@ -46,6 +56,7 @@ class BubbleInfoTest : ShellTestCase() { assertThat(bubbleInfo.userId).isEqualTo(bubbleInfoFromParcel.userId) assertThat(bubbleInfo.packageName).isEqualTo(bubbleInfoFromParcel.packageName) assertThat(bubbleInfo.title).isEqualTo(bubbleInfoFromParcel.title) + assertThat(bubbleInfo.appName).isEqualTo(bubbleInfoFromParcel.appName) assertThat(bubbleInfo.isImportantConversation) .isEqualTo(bubbleInfoFromParcel.isImportantConversation) } 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 56d0f8e13f08..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 @@ -26,6 +26,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_STA import static com.google.common.truth.Truth.assertThat; 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.never; @@ -115,27 +116,27 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testUpdateDivideBounds() { - mSplitLayout.updateDivideBounds(anyInt()); + mSplitLayout.updateDividerBounds(anyInt(), anyBoolean()); verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(), - 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); } @@ -152,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()); } @@ -164,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()); } @@ -188,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 fef81af8946b..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; @@ -668,6 +668,18 @@ public class CompatUIControllerTest extends ShellTestCase { Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton()); } + @Test + public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false; + + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + } + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, @CameraCompatControlState int cameraCompatControlState) { return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState, @@ -689,10 +701,13 @@ 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; + taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true; + taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true; return taskInfo; } } 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 4f261cd79d39..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,12 +16,13 @@ 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; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -37,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; @@ -98,14 +99,28 @@ public class CompatUIWindowManagerTest extends ShellTestCase { private CompatUIWindowManager mWindowManager; private TaskInfo mTaskInfo; + private DisplayLayout mDisplayLayout; @Before public void setUp() { MockitoAnnotations.initMocks(this); doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = TASK_WIDTH; + displayInfo.logicalHeight = TASK_HEIGHT; + mDisplayLayout = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false); + final InsetsState insetsState = new InsetsState(); + insetsState.setDisplayFrame(new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT)); + final InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + insetsSource.setFrame(0, TASK_HEIGHT - 200, TASK_WIDTH, TASK_HEIGHT); + insetsState.addSource(insetsSource); + mDisplayLayout.setInsets(mContext.getResources(), insetsState); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, - mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(), mCompatUIConfiguration, mOnRestartButtonClicked); spyOn(mWindowManager); @@ -363,9 +378,9 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Update if the insets change on the existing display layout clearInvocations(mWindowManager); - InsetsState insetsState = new InsetsState(); + final InsetsState insetsState = new InsetsState(); insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000)); - InsetsSource insetsSource = new InsetsSource( + final InsetsSource insetsSource = new InsetsSource( InsetsSource.createId(null, 0, navigationBars()), navigationBars()); insetsSource.setFrame(0, 1800, 1000, 2000); insetsState.addSource(insetsSource); @@ -493,16 +508,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testShouldShowSizeCompatRestartButton() { mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); - - doReturn(86).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); + doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, - mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(), mCompatUIConfiguration, mOnRestartButtonClicked); // Simulate rotation of activity in square display TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN); - taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); - taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850; assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); @@ -512,26 +525,37 @@ public class CompatUIWindowManagerTest extends ShellTestCase { assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); // Simulate folding - taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000)); - assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + final InsetsState insetsState = new InsetsState(); + insetsState.setDisplayFrame(new Rect(0, 0, 1000, TASK_HEIGHT)); + final InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + insetsSource.setFrame(0, TASK_HEIGHT - 200, 1000, TASK_HEIGHT); + insetsState.addSource(insetsSource); + mDisplayLayout.setInsets(mContext.getResources(), insetsState); + mWindowManager.updateDisplayLayout(mDisplayLayout); + taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP - 100; + assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); - taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; - taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 500; + // Simulate floating app with 90& area, more than tolerance + taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 950; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1900; assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); } 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.configuration.windowConfiguration.setBounds( - new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT)); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + // Screen width dp larger than a normal phone. + taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; return taskInfo; } } 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/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 81ba4b37d13b..94e168ed70ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -292,6 +292,24 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + public void testUserFullscreenOverrideEnabled_buttonAlwaysShown() { + TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER); + + final Rect stableBounds = mWindowManager.getTaskStableBounds(); + + // Letterboxed activity that has user fullscreen override should always show button, + // layout should be inflated + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableBounds.height(); + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width(); + taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled = true; + + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + } + + @Test public void testUpdateDisplayLayout() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1000; 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..fb03f20f939c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -0,0 +1,637 @@ +/* + * 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.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN +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 +import org.mockito.kotlin.verifyZeroInteractions + +/** + * 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 transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() { + 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 transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() { + 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()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() { + 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()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 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()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitToFront_logTaskAddedAndEnterReasonOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(previousChange) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_TO_FRONT + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(previousChange) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_CHANGE + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(previousChange) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_OPEN + val change = createChange(TRANSIT_TO_FRONT, 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.OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + @Suppress("ktlint:standard:max-line-length") + fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() { + // Tests for AppFromOverview precedence in compared to cancelled Overview + + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(previousChange) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + + // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + val sessionId = transitionObserver.getLoggerSessionId() + + assertThat(sessionId).isNotNull() + verify(desktopModeEventLogger, times(1)) + .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitWake_logTaskAddedAndEnterReasonScreenOn() { + 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()) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun transitSleep_logTaskAddedAndExitReasonScreenOff_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)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_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(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).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)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_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(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_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(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_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(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_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)) + verifyZeroInteractions(desktopModeEventLogger) + assertThat(transitionObserver.getLoggerSessionId()).isNull() + } + + @Test + fun transitClose_logTaskRemovedAndExitReasonTaskFinished_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)) + verifyZeroInteractions(desktopModeEventLogger) + 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 + } + } +} 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..310ccc252469 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 @@ -301,11 +363,11 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() { - repo.addOrMoveFreeformTaskToTop(5) - repo.addOrMoveFreeformTaskToTop(6) - repo.addOrMoveFreeformTaskToTop(7) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) - val tasks = repo.getFreeformTasksInZOrder() + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks.size).isEqualTo(3) assertThat(tasks[0]).isEqualTo(7) assertThat(tasks[1]).isEqualTo(6) @@ -314,76 +376,138 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() { - repo.addOrMoveFreeformTaskToTop(5) - repo.addOrMoveFreeformTaskToTop(6) - repo.addOrMoveFreeformTaskToTop(7) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) - repo.addOrMoveFreeformTaskToTop(6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - val tasks = repo.getFreeformTasksInZOrder() + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks.size).isEqualTo(3) assertThat(tasks.first()).isEqualTo(6) } @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(THIRD_DISPLAY, 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 = DEFAULT_DISPLAY, taskId = 1) + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) + repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2) + repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)) + .containsExactly(1, 2, 3).inOrder() + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() { + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) + repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2) + repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1) + repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack( + displayId = DEFAULT_DISPLAY)).containsExactly(1, 3).inOrder() + } + + 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,23 +540,10 @@ 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 { const val SECOND_DISPLAY = 1 + const val THIRD_DISPLAY = 345 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt new file mode 100644 index 000000000000..518c00d377ad --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt @@ -0,0 +1,73 @@ +/* + * 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.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.TASK_DRAG +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopModeTransitionTypes] + * + * Usage: atest WMShellUnitTests:DesktopModeTransitionTypesTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeTransitionTypesTest { + + @Test + fun testGetEnterTransitionType() { + assertThat(UNKNOWN.getEnterTransitionType()).isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) + assertThat(APP_HANDLE_MENU_BUTTON.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON) + assertThat(APP_FROM_OVERVIEW.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW) + assertThat(TASK_DRAG.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) + assertThat(KEYBOARD_SHORTCUT.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT) + } + + @Test + fun testGetExitTransitionType() { + assertThat(UNKNOWN.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN) + assertThat(APP_HANDLE_MENU_BUTTON.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) + assertThat(APP_FROM_OVERVIEW.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN) + assertThat(TASK_DRAG.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG) + assertThat(KEYBOARD_SHORTCUT.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT) + } +} 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..51b291c0b7a4 --- /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(/* instanceIdMax */ 1 shl 20) + + + @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..e17f7f2f7b12 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.app.ActivityManager.RecentTaskInfo import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD @@ -23,56 +24,88 @@ 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.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.CONFIG_DENSITY +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.IWindowContainerToken import android.window.RemoteTransition import android.window.TransitionRequestInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK +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 android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID 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.desktopmode.DesktopModeTransitionSource.UNKNOWN +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 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask import com.android.wm.shell.draganddrop.DragAndDropController +import com.android.wm.shell.recents.RecentTasksController 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 java.util.Optional +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue 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,836 +117,1754 @@ 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.Mockito.`when` as whenever +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.capture +import org.mockito.quality.Strictness +/** + * Test class for {@link DesktopTasksController} + * + * Usage: atest WMShellUnitTests:DesktopTasksControllerTest + */ @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopTasksControllerTest : ShellTestCase() { - @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var shellCommandHandler: ShellCommandHandler - @Mock lateinit var shellController: ShellController - @Mock lateinit var displayController: DisplayController - @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: - 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 - - private lateinit var mockitoSession: StaticMockitoSession - private lateinit var controller: DesktopTasksController - private lateinit var shellInit: ShellInit - private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository - private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener - - private val shellExecutor = TestShellExecutor() - // Mock running tasks are registered here so we can get the list from mock shell task organizer - private val runningTasks = mutableListOf<RunningTaskInfo>() - - @Before - fun setUp() { - mockitoSession = mockitoSession().mockStatic(DesktopModeStatus::class.java).startMocking() - whenever(DesktopModeStatus.isEnabled()).thenReturn(true) - - shellInit = Mockito.spy(ShellInit(testExecutor)) - desktopModeTaskRepository = DesktopModeTaskRepository() - - whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - - controller = createController() - controller.setSplitScreenController(splitScreenController) - - shellInit.init() - - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) - verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value - } - - private fun createController(): DesktopTasksController { - return DesktopTasksController( - context, - shellInit, - shellCommandHandler, - shellController, - displayController, - shellTaskOrganizer, - syncQueue, - rootTaskDisplayAreaOrganizer, - dragAndDropController, - transitions, - enterDesktopTransitionHandler, - exitDesktopTransitionHandler, - mToggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, - desktopModeTaskRepository, - launchAdjacentController, - recentsTransitionHandler, - multiInstanceHelper, - shellExecutor - ) - } - - @After - fun tearDown() { - mockitoSession.finishMocking() - - runningTasks.clear() - } - - @Test - fun instantiate_addInitCallback() { - verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - fun instantiate_flagOff_doNotAddInitCallback() { - whenever(DesktopModeStatus.isEnabled()).thenReturn(false) - clearInvocations(shellInit) - - createController() - - verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - fun showDesktopApps_allAppsInvisible_bringsToFront() { - val homeTask = setUpHomeTask() - 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: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - fun showDesktopApps_appsAlreadyVisible_bringsToFront() { - val homeTask = setUpHomeTask() - 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: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - fun showDesktopApps_someAppsInvisible_reordersAll() { - val homeTask = setUpHomeTask() - 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: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - fun showDesktopApps_noActiveTasks_reorderHomeToTop() { - val homeTask = setUpHomeTask() - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, homeTask) - } - - @Test - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() { - val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) - 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: home, task - wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) - wct.assertReorderAt(index = 1, taskDefaultDisplay) - } - - @Test - fun getVisibleTaskCount_noTasks_returnsZero() { - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - } - - @Test - fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) - } - - @Test - fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskHidden) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) - } - - @Test - fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { - setUpHomeTask() - setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) - setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) - } - - @Test - fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() { - val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() { - val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun moveToDesktop_nonExistentTask_doesNothing() { - controller.moveToDesktop(999) - verifyWCTNotExecuted() - } - - @Test - fun moveToDesktop_otherFreeformTasksBroughtToFront() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val fullscreenTask = setUpFullscreenTask() - markTaskHidden(freeformTask) - - controller.moveToDesktop(fullscreenTask) - - with(getLatestMoveToDesktopWct()) { - // Operations should include home task, freeform task - assertThat(hierarchyOps).hasSize(3) - assertReorderSequence(homeTask, freeformTask, 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) - val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) - markTaskHidden(freeformTaskDefault) - - val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) - val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) - markTaskHidden(freeformTaskSecond) - - controller.moveToDesktop(fullscreenTaskDefault) - - with(getLatestMoveToDesktopWct()) { - // Check that hierarchy operations do not include tasks from second display - assertThat(hierarchyOps.map { it.container }) - .doesNotContain(homeTaskSecond.token.asBinder()) - assertThat(hierarchyOps.map { it.container }) - .doesNotContain(freeformTaskSecond.token.asBinder()) - } - } - - @Test - fun moveToDesktop_splitTaskExitsSplit() { - val task = setUpSplitScreenTask() - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { - val task = setUpFullscreenTask() - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController, never()).prepareExitSplitScreen(any(), anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() { - val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToFullscreen(task.taskId) - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() { - val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM - controller.moveToFullscreen(task.taskId) - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - @Test - fun moveToFullscreen_nonExistentTask_doesNothing() { - controller.moveToFullscreen(999) - verifyWCTNotExecuted() - } - - @Test - fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { - val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) - - controller.moveToFullscreen(taskDefaultDisplay.taskId) - - with(getLatestExitDesktopWct()) { - assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) - assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) + @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 toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler + @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler + @Mock lateinit var launchAdjacentController: LaunchAdjacentController + @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 + @Mock lateinit var recentTasksController: RecentTasksController + + 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() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(DesktopModeStatus.isEnabled()).thenReturn(true) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + 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(), 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) + + shellInit.init() + + val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) + recentsTransitionStateListener = captor.value + } + + private fun createController(): DesktopTasksController { + return DesktopTasksController( + context, + shellInit, + shellCommandHandler, + shellController, + displayController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + dragAndDropController, + transitions, + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, + dragToDesktopTransitionHandler, + desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, + launchAdjacentController, + recentsTransitionHandler, + multiInstanceHelper, + shellExecutor, + Optional.of(desktopTasksLimiter), + recentTasksController) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + + runningTasks.clear() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + fun instantiate_flagOff_doNotAddInitCallback() { + whenever(DesktopModeStatus.isEnabled()).thenReturn(false) + clearInvocations(shellInit) + + createController() + + verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + 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: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @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() + 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: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @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() + 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: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @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())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, homeTask) + } + + @Test + @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) + 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: home, task + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @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) + } + + @Test + fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + } + + @Test + fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskHidden) + assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + } + + @Test + fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + setUpHomeTask() + setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) + setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + val task = + setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + val task = + setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + val task = + setUpFullscreenTask( + isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + val task = + setUpFullscreenTask( + isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun moveToDesktop_nonExistentTask_doesNothing() { + controller.moveToDesktop(999, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + fun moveToDesktop_nonRunningTask_launchesInFreeform() { + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + + val task = createTaskInfo(1) + + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + + controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + with(getLatestEnterDesktopWct()) { + assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + + @Test + fun moveToDesktop_topActivityTranslucent_doesNothing() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = + setUpFullscreenTask().apply { + isTopActivityTransparent = true + numActivities = 1 } - } - - @Test - fun moveTaskToFront_postsWctWithReorderOp() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - controller.moveTaskToFront(task1) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, task1) - } - - @Test - fun moveToNextDisplay_noOtherDisplays() { - whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - verifyWCTNotExecuted() - } - @Test - fun moveToNextDisplay_moveFromFirstToSecondDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: second display - val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) - .thenReturn(secondDisplayArea) - - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + fun moveToDesktop_systemUIActivity_doesNothing() { + val task = setUpFullscreenTask() + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() { + val task = setUpFullscreenTask() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + 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() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Operations should include home task, freeform task + assertThat(hierarchyOps).hasSize(3) + assertReorderSequence(homeTask, freeformTask, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // 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) + val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + markTaskHidden(freeformTaskDefault) + + val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) + val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) + markTaskHidden(freeformTaskSecond) + + controller.moveToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Check that hierarchy operations do not include tasks from second display + assertThat(hierarchyOps.map { it.container }).doesNotContain(homeTaskSecond.token.asBinder()) + assertThat(hierarchyOps.map { it.container }) + .doesNotContain(freeformTaskSecond.token.asBinder()) + } + } + + @Test + fun moveToDesktop_splitTaskExitsSplit() { + val task = setUpSplitScreenTask() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { + val task = setUpFullscreenTask() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController, never()) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val homeTask = setUpHomeTask() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + + controller.moveToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + 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() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test + fun moveToFullscreen_nonExistentTask_doesNothing() { + controller.moveToFullscreen(999, transitionSource = UNKNOWN) + verifyExitDesktopWCTNotExecuted() + } + + @Test + fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { + val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) + + controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) + + with(getLatestExitDesktopWct()) { + assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) + assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) + } + } + + @Test + fun moveTaskToFront_postsWctWithReorderOp() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + controller.moveTaskToFront(task1) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, task1) + } + + @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) + controller.moveToNextDisplay(task.taskId) + verifyWCTNotExecuted() + } + + @Test + fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun getTaskWindowingMode() { + val fullscreenTask = setUpFullscreenTask() + val freeformTask = setUpFreeformTask() + + assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @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) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @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) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) + assertThat(result).isNull() + } + + @Test + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + 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 + fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() + markTaskHidden(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertThat(result?.hierarchyOps?.size).isEqualTo(2) + result!!.assertReorderAt(1, freeformTask2, toTop = true) + } + + @Test + fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertThat(result?.hierarchyOps?.size).isEqualTo(1) + result!!.assertReorderAt(0, task, toTop = true) + } + + @Test + fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + assertThat(result?.hierarchyOps?.size).isEqualTo(1) + result!!.assertReorderAt(0, taskDefaultDisplay, toTop = true) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) + assertFalse(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) + assertTrue(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_notOpenOrToFrontTransition_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build() + val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) + val result = controller.handleRequest(Binder(), transition) + assertThat(result).isNull() + } + + @Test + fun handleRequest_noTriggerTask_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotStandard_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_recentsAnimationRunning_returnNull() { + // Set up a visible freeform task so a fullscreen task should be converted to freeform + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Mark recents animation running + recentsTransitionStateListener.onAnimationStateChanged(true) + + // Open a fullscreen task, check that it does not result in a WCT with changes to it + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = + setUpFreeformTask().apply { + isTopActivityTransparent = true + numActivities = 1 } - } - @Test - fun moveToNextDisplay_moveFromSecondToFirstDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: default display - val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .thenReturn(defaultDisplayArea) - - val task = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToNextDisplay(task.taskId) - - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + fun handleRequest_systemUIActivity_returnSwitchToFullscreenWCT() { + val task = setUpFreeformTask() + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_noToken() { + val task = setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task = setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() { + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val task = setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + assertThat(result).isNotNull() + // Creates remove wallpaper transaction + result!!.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasks() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task1 = setUpFreeformTask() + setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { + val task = setUpFreeformTask() + clearInvocations(launchAdjacentController) + + markTaskVisible(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = false + } + + @Test + fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { + val task = setUpFreeformTask() + markTaskVisible(task) + clearInvocations(launchAdjacentController) + + markTaskHidden(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = true + } + + @Test + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { + val task1 = setUpFullscreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { + val task1 = setUpSplitScreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + val task4 = setUpSplitScreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + task4.isFocused = true + + task4.parentTaskId = task1.taskId + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + fun moveFocusedTaskToFullscreen() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + 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) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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) + val activityInfo = ActivityInfo() + task.topActivityInfo = activityInfo + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopModeTaskRepository.addActiveTask(displayId, task.taskId) + desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + runningTasks.add(task) + return task + } + + private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + val task = createHomeTask(displayId) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + 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 } - } - - @Test - fun getTaskWindowingMode() { - val fullscreenTask = setUpFullscreenTask() - val freeformTask = setUpFreeformTask() - - assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - - val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) - createFreeformTask(displayId = SECOND_DISPLAY) - - val result = - controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) - assertThat(result).isNull() - } - - @Test - fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { - 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) - - // Stashed state should be cleared - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - } - - @Test - fun handleRequest_freeformTask_freeformVisible_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull() - } - - @Test - fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask1 = setUpFreeformTask() - markTaskHidden(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = - controller.handleRequest( - Binder(), - createTransition(freeformTask2, type = TRANSIT_TO_FRONT) - ) - assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - @Test - fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - @Test - fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_returnSwitchToFullscreenWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - createFreeformTask(displayId = SECOND_DISPLAY) - - 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() - } - - @Test - fun handleRequest_notOpenOrToFrontTransition_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN) - .build() - val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) - val result = controller.handleRequest(Binder(), transition) - assertThat(result).isNull() - } - - @Test - fun handleRequest_noTriggerTask_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotStandard_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_recentsAnimationRunning_returnNull() { - // Set up a visible freeform task so a fullscreen task should be converted to freeform - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Mark recents animation running - recentsTransitionStateListener.onAnimationStateChanged(true) - - // Open a fullscreen task, check that it does not result in a WCT with changes to it - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun stashDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - controller.stashDesktopApps(DEFAULT_DISPLAY) - - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue() - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse() - } - - @Test - fun hideStashedDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true) - desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true) - controller.hideStashedDesktopApps(DEFAULT_DISPLAY) - - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - // Check that second display is not affected - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue() - } - - @Test - fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { - val task = setUpFreeformTask() - clearInvocations(launchAdjacentController) - - markTaskVisible(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = false - } - - @Test - fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { - val task = setUpFreeformTask() - markTaskVisible(task) - clearInvocations(launchAdjacentController) - - markTaskHidden(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = true - } - @Test - fun enterDesktop_fullscreenTaskIsMovedToDesktop() { - val task1 = setUpFullscreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - - controller.enterDesktop(DEFAULT_DISPLAY) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun enterDesktop_splitScreenTaskIsMovedToDesktop() { - val task1 = setUpSplitScreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - val task4 = setUpSplitScreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - task4.isFocused = true - - task4.parentTaskId = task1.taskId - - controller.enterDesktop(DEFAULT_DISPLAY) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveFocusedTaskToFullscreen() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - - controller.enterFullscreen(DEFAULT_DISPLAY) - - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createFreeformTask(displayId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - desktopModeTaskRepository.addActiveTask(displayId, task.taskId) - desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId) - runningTasks.add(task) - return task - } - - private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createHomeTask(displayId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createFullscreenTask(displayId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createSplitScreenTask(displayId) - whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun markTaskVisible(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( - task.displayId, - task.taskId, - visible = true - ) - } - - private fun markTaskHidden(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( - task.displayId, - task.taskId, - visible = false - ) - } + } 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(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(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks( + task.displayId, task.taskId, visible = true) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks( + task.displayId, task.taskId, visible = false) + } + + private fun getLatestWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + handlerClass: Class<out TransitionHandler>? = null + ): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + if (handlerClass == null) { + verify(transitions).startTransition(eq(type), arg.capture(), isNull()) + } else { + verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) + } + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + 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 getLatestEnterDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + 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) { + verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + 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()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun verifyExitDesktopWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun verifyEnterDesktopWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun createTransition( + task: RunningTaskInfo?, + @WindowManager.TransitionType type: Int = TRANSIT_OPEN + ): TransitionRequestInfo { + return TransitionRequestInfo(type, task, null /* remoteTransition */) + } + + companion object { + const val SECOND_DISPLAY = 2 + private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + } +} - private fun getLatestWct( - @WindowManager.TransitionType type: Int = TRANSIT_OPEN, - handlerClass: Class<out TransitionHandler>? = null - ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - if (handlerClass == null) { - verify(transitions).startTransition(eq(type), arg.capture(), isNull()) - } else { - verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) - } - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } +private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { + assertWithMessage("WCT does not have a hierarchy operation at index $index") + .that(hierarchyOps.size) + .isGreaterThan(index) +} - private fun getLatestMoveToDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } +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 getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(exitDesktopTransitionHandler) - .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } +private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { + for (i in tasks.indices) { + assertReorderAt(i, tasks[i]) + } +} - private fun verifyWCTNotExecuted() { - if (ENABLE_SHELL_TRANSITIONS) { - verify(transitions, never()).startTransition(anyInt(), any(), isNull()) - } else { - verify(shellTaskOrganizer, never()).applyTransaction(any()) - } - } +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 createTransition( - task: RunningTaskInfo?, - @WindowManager.TransitionType type: Int = TRANSIT_OPEN - ): TransitionRequestInfo { - return TransitionRequestInfo(type, task, null /* remoteTransition */) - } +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) +} - companion object { - const val SECOND_DISPLAY = 2 - } +private fun WindowContainerTransaction.assertLaunchTaskAt( + index: Int, + taskId: Int, + windowingMode: Int +) { + val keyLaunchWindowingMode = "android.activity.windowingMode" + + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK) + assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId) + assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED)) + .isEqualTo(windowingMode) } -private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { - assertWithMessage("WCT does not have a hierarchy operation at index $index") - .that(hierarchyOps.size) - .isGreaterThan(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) - assertThat(op.container).isEqualTo(task.token.asBinder()) +private fun WindowContainerTransaction?.anyDensityConfigChange( + token: WindowContainerToken +): Boolean { + return this?.changes?.any { change -> + change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) + } ?: false } -private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { - for (i in tasks.indices) { - assertReorderAt(i, tasks[i]) +private fun createTaskInfo(id: Int) = + RecentTaskInfo().apply { + taskId = id + token = WindowContainerToken(mock(IWindowContainerToken::class.java)) } -} 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..77f917cc28d8 --- /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(displayId, 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..bbf523bc40d2 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 @@ -18,6 +18,8 @@ import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +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.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP @@ -48,6 +50,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var transitions: Transitions @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController + @Mock private lateinit var dragAnimator: MoveToDesktopAnimator private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -68,7 +71,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_animateDragWhenReady() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) @@ -90,36 +92,36 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { - val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() - // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) - - handler.cancelDragToDesktopTransition() + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + verify(transitions) + .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + } - handler.startAnimation( - transition = transition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} + @Test + fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT) + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), + any() ) + } - // Don't even animate the "drag" since it was already cancelled. - verify(dragAnimator, never()).startAnimation() - // Instead, start the cancel transition. - verify(transitions) - .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + @Test + fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT) + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), + any() + ) } @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) // But the transition was aborted. @@ -137,14 +139,15 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_aborted_cancelDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) // But the transition was aborted. handler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Should not be attempted and state should be reset. assertFalse(handler.inProgress) @@ -153,7 +156,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_anotherTransitionInProgress_startDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate attempt to start two drag to desktop transitions. startDragToDesktopTransition(task, dragAnimator) @@ -169,39 +171,63 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_startWasReady_cancel() { - val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() - whenever(dragAnimator.position).thenReturn(PointF()) - // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) - handler.startAnimation( - transition = transition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} - ) + startDrag() // Then user cancelled after it had already started. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Cancel animation should run since it had already started. - verify(dragAnimator).endAnimator() + verify(dragAnimator).cancelAnimator() + } + + @Test + fun cancelDragToDesktop_splitLeftCancelType_splitRequested() { + startDrag() + + // Then user cancelled it, requesting split. + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT + ) + + // Verify the request went through split controller. + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), + any() + ) + } + + @Test + fun cancelDragToDesktop_splitRightCancelType_splitRequested() { + startDrag() + + // Then user cancelled it, requesting split. + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT + ) + + // Verify the request went through split controller. + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), + any() + ) } @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started and is ready to animate. startDragToDesktopTransition(task, dragAnimator) // Then user cancelled before the transition was ready and animated. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // No need to animate the cancel since the start animation couldn't even start. verifyZeroInteractions(dragAnimator) @@ -210,7 +236,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { // Then cancel is called before the transition was started. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Verify cancel is dropped. verify(transitions, never()).startTransition( @@ -233,6 +261,24 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } + private fun startDrag() { + val task = createTask() + whenever(dragAnimator.position).thenReturn(PointF()) + // Simulate transition is started and is ready to animate. + val transition = startDragToDesktopTransition(task, dragAnimator) + handler.startAnimation( + transition = transition, + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + } + private fun startDragToDesktopTransition( task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator @@ -250,6 +296,29 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { return token } + private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) { + val task = createTask() + // Simulate transition is started and is ready to animate. + val transition = startDragToDesktopTransition(task, dragAnimator) + + handler.cancelDragToDesktopTransition(cancelState) + + handler.startAnimation( + transition = transition, + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + // Don't even animate the "drag" since it was already cancelled. + verify(dragAnimator, never()).startAnimation() + } + private fun createTask( @WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN, isHome: Boolean = false, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index 0d0a08cb0ffb..b2467e9a62cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; + import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -45,6 +47,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -97,18 +100,18 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { @Test public void testTransitExitDesktopModeAnimation() throws Throwable { - final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE; + final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); doReturn(mToken).when(mTransitions) .startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler); - mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct, mPoint, - null); + mExitDesktopTaskTransitionHandler.startTransition(DesktopModeTransitionSource.UNKNOWN, + wct, mPoint, null); TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); - TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change); + TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change); ArrayList<Exception> exceptions = new ArrayList<>(); runOnUiThread(() -> { try { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index 5dd9d8a859d6..6e72e8df8d62 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -66,6 +66,7 @@ import android.graphics.Insets; import android.os.RemoteException; import android.view.DisplayInfo; import android.view.DragEvent; +import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -115,6 +116,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { private DragAndDropPolicy mPolicy; private ClipData mActivityClipData; + private PendingIntent mLaunchableIntentPendingIntent; private ClipData mLaunchableIntentClipData; private ClipData mNonResizeableActivityClipData; private ClipData mTaskClipData; @@ -151,7 +153,10 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mSplitScreenStarter)); mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); - mLaunchableIntentClipData = createIntentClipData(); + mLaunchableIntentPendingIntent = mock(PendingIntent.class); + when(mLaunchableIntentPendingIntent.getCreatorUserHandle()) + .thenReturn(android.os.Process.myUserHandle()); + mLaunchableIntentClipData = createIntentClipData(mLaunchableIntentPendingIntent); mNonResizeableActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); setClipDataResizeable(mNonResizeableActivityClipData, false); mTaskClipData = createAppClipData(MIMETYPE_APPLICATION_TASK); @@ -202,16 +207,13 @@ public class DragAndDropPolicyTest extends ShellTestCase { /** * Creates an intent-based clip data that is by default resizeable. */ - private ClipData createIntentClipData() { + private ClipData createIntentClipData(PendingIntent intent) { ClipDescription clipDescription = new ClipDescription("Intent", new String[] { MIMETYPE_TEXT_INTENT }); - PendingIntent intent = mock(PendingIntent.class); - when(intent.getCreatorUserHandle()).thenReturn(android.os.Process.myUserHandle()); ClipData.Item item = new ClipData.Item.Builder() .setIntentSender(intent.getIntentSender()) .build(); ClipData data = new ClipData(clipDescription, item); - when(DragUtils.getLaunchIntent((ClipData) any())).thenReturn(intent); return data; } @@ -259,16 +261,22 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragIntentOverFullscreenHome_expectOnlyFullscreenTarget() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); dragOverFullscreenHome_expectOnlyFullscreenTarget(mLaunchableIntentClipData); } @Test public void testDragIntentOverFullscreenApp_expectSplitScreenTargets() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); dragOverFullscreenApp_expectSplitScreenTargets(mLaunchableIntentClipData); } @Test public void testDragIntentOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { + when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn( + mLaunchableIntentPendingIntent); dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mLaunchableIntentClipData); } @@ -276,7 +284,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, data); + mLandscapeDisplayLayout, data, 0 /* dragFlags */); dragSession.update(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( @@ -291,7 +299,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, data); + mLandscapeDisplayLayout, data, 0 /* dragFlags */); dragSession.update(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( @@ -311,7 +319,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { doReturn(false).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, - mPortraitDisplayLayout, data); + mPortraitDisplayLayout, data, 0 /* dragFlags */); dragSession.update(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( @@ -331,7 +339,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { public void testTargetHitRects() { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, - mLandscapeDisplayLayout, mActivityClipData); + mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); dragSession.update(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); @@ -345,6 +353,11 @@ public class DragAndDropPolicyTest extends ShellTestCase { } } + @Test + public void testDisallowLaunchIntentWithoutDelegationFlag() { + assertTrue(DragUtils.getLaunchIntent(mLaunchableIntentClipData, 0) == null); + } + private Target filterTargetByType(ArrayList<Target> targets, int type) { for (Target t : targets) { if (type == t.type) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt index e731b06c0947..d410151b4602 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt @@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() { @Test fun onUnhandledDrop_noListener_expectNotifyUnhandled() { // Simulate an unhandled drop - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) @@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() { // Simulate an unhandled drop val dragSurface = mock<SurfaceControl>() - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, dragSurface, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) 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..3f3cafcf6375 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), @@ -88,7 +91,8 @@ public final class FreeformTaskListenerTests extends ShellTestCase { mFreeformTaskListener.onFocusTaskChanged(task); - verify(mDesktopModeTaskRepository).addOrMoveFreeformTaskToTop(task.taskId); + verify(mDesktopModeTaskRepository) + .addOrMoveFreeformTaskToTop(task.displayId, task.taskId); } @Test @@ -100,7 +104,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { mFreeformTaskListener.onFocusTaskChanged(fullscreenTask); verify(mDesktopModeTaskRepository, never()) - .addOrMoveFreeformTaskToTop(fullscreenTask.taskId); + .addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId); } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java index 3d5cd6939d1b..85f1da5322ea 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java @@ -16,33 +16,26 @@ package com.android.wm.shell.pip.phone; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; -import android.os.SystemProperties; import android.testing.AndroidTestingRunner; import android.util.Size; import android.view.DisplayInfo; -import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.SizeSpecSource; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.exceptions.misusing.InvalidUseOfMatchersException; import java.util.HashMap; import java.util.Map; @@ -63,15 +56,24 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { private static final float DEFAULT_PERCENT = 0.6f; /** Minimum sizing percentage */ private static final float MIN_PERCENT = 0.5f; + /** Threshold to determine if a Display is square-ish. */ + private static final float SQUARE_DISPLAY_THRESHOLD = 0.95f; + /** Default sizing percentage for square-ish Display. */ + private static final float SQUARE_DISPLAY_DEFAULT_PERCENT = 0.5f; + /** Minimum sizing percentage for square-ish Display. */ + private static final float SQUARE_DISPLAY_MIN_PERCENT = 0.4f; /** Aspect ratio that the new PIP size spec logic optimizes for. */ private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16; - /** A map of aspect ratios to be tested to expected sizes */ - private static Map<Float, Size> sExpectedMaxSizes; - private static Map<Float, Size> sExpectedDefaultSizes; - private static Map<Float, Size> sExpectedMinSizes; - /** A static mockito session object to mock {@link SystemProperties} */ - private static StaticMockitoSession sStaticMockitoSession; + /** Maps of aspect ratios to be tested to expected sizes on non-square Display. */ + private static Map<Float, Size> sNonSquareDisplayExpectedMaxSizes; + private static Map<Float, Size> sNonSquareDisplayExpectedDefaultSizes; + private static Map<Float, Size> sNonSquareDisplayExpectedMinSizes; + + /** Maps of aspect ratios to be tested to expected sizes on square Display. */ + private static Map<Float, Size> sSquareDisplayExpectedMaxSizes; + private static Map<Float, Size> sSquareDisplayExpectedDefaultSizes; + private static Map<Float, Size> sSquareDisplayExpectedMinSizes; @Mock private Context mContext; @Mock private Resources mResources; @@ -80,49 +82,55 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { private SizeSpecSource mSizeSpecSource; /** - * Sets up static Mockito session for SystemProperties and mocks necessary static methods. + * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes. + * This is to initialize the expectations on non-square Display only. */ - private static void setUpStaticSystemPropertiesSession() { - sStaticMockitoSession = mockitoSession() - .mockStatic(SystemProperties.class).startMocking(); - when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> { - String property = invocation.getArgument(0); - if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) { - return Float.toString(DEFAULT_PERCENT); - } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) { - return Float.toString(MIN_PERCENT); - } - - // throw an exception if illegal arguments are used for these tests - throw new InvalidUseOfMatchersException( - String.format("Argument %s does not match", property) - ); - }); + private static void initNonSquareDisplayExpectedSizes() { + sNonSquareDisplayExpectedMaxSizes = new HashMap<>(); + sNonSquareDisplayExpectedDefaultSizes = new HashMap<>(); + sNonSquareDisplayExpectedMinSizes = new HashMap<>(); + + sNonSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); + sNonSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(600, 338)); + sNonSquareDisplayExpectedMinSizes.put(16f / 9, new Size(501, 282)); + + sNonSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670)); + sNonSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(536, 402)); + sNonSquareDisplayExpectedMinSizes.put(4f / 3, new Size(447, 335)); + + sNonSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893)); + sNonSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(402, 536)); + sNonSquareDisplayExpectedMinSizes.put(3f / 4, new Size(335, 447)); + + sNonSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); + sNonSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(338, 601)); + sNonSquareDisplayExpectedMinSizes.put(9f / 16, new Size(282, 501)); } /** * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes. + * This is to initialize the expectations on square Display only. */ - private static void initExpectedSizes() { - sExpectedMaxSizes = new HashMap<>(); - sExpectedDefaultSizes = new HashMap<>(); - sExpectedMinSizes = new HashMap<>(); - - sExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); - sExpectedDefaultSizes.put(16f / 9, new Size(600, 338)); - sExpectedMinSizes.put(16f / 9, new Size(501, 282)); - - sExpectedMaxSizes.put(4f / 3, new Size(893, 670)); - sExpectedDefaultSizes.put(4f / 3, new Size(536, 402)); - sExpectedMinSizes.put(4f / 3, new Size(447, 335)); - - sExpectedMaxSizes.put(3f / 4, new Size(670, 893)); - sExpectedDefaultSizes.put(3f / 4, new Size(402, 536)); - sExpectedMinSizes.put(3f / 4, new Size(335, 447)); - - sExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); - sExpectedDefaultSizes.put(9f / 16, new Size(338, 601)); - sExpectedMinSizes.put(9f / 16, new Size(282, 501)); + private static void initSquareDisplayExpectedSizes() { + sSquareDisplayExpectedMaxSizes = new HashMap<>(); + sSquareDisplayExpectedDefaultSizes = new HashMap<>(); + sSquareDisplayExpectedMinSizes = new HashMap<>(); + + sSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); + sSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(500, 281)); + sSquareDisplayExpectedMinSizes.put(16f / 9, new Size(400, 225)); + + sSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670)); + sSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(447, 335)); + sSquareDisplayExpectedMinSizes.put(4f / 3, new Size(357, 268)); + + sSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893)); + sSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(335, 447)); + sSquareDisplayExpectedMinSizes.put(3f / 4, new Size(268, 357)); + + sSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); + sSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(282, 501)); + sSquareDisplayExpectedMinSizes.put(9f / 16, new Size(225, 400)); } private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes, @@ -137,20 +145,38 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { @Before public void setUp() { - initExpectedSizes(); - - when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE); - when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO); - when(mResources.getString(anyInt())).thenReturn("0x0"); + initNonSquareDisplayExpectedSizes(); + initSquareDisplayExpectedSizes(); + + when(mResources.getFloat(R.dimen.config_pipSystemPreferredDefaultSizePercent)) + .thenReturn(DEFAULT_PERCENT); + when(mResources.getFloat(R.dimen.config_pipSystemPreferredMinimumSizePercent)) + .thenReturn(MIN_PERCENT); + when(mResources.getDimensionPixelSize(R.dimen.default_minimal_size_pip_resizable_task)) + .thenReturn(DEFAULT_MIN_EDGE_SIZE); + when(mResources.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio)) + .thenReturn(OPTIMIZED_ASPECT_RATIO); + when(mResources.getString(R.string.config_defaultPictureInPictureScreenEdgeInsets)) + .thenReturn("0x0"); when(mResources.getDisplayMetrics()) .thenReturn(getContext().getResources().getDisplayMetrics()); + when(mResources.getFloat(R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize)) + .thenReturn(SQUARE_DISPLAY_THRESHOLD); + when(mResources.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay)) + .thenReturn(SQUARE_DISPLAY_DEFAULT_PERCENT); + when(mResources.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay)) + .thenReturn(SQUARE_DISPLAY_MIN_PERCENT); // set up the mock context for spec handler specifically when(mContext.getResources()).thenReturn(mResources); + } + private void setupSizeSpecWithDisplayDimension(int width, int height) { DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.logicalWidth = DISPLAY_EDGE_SIZE; - displayInfo.logicalHeight = DISPLAY_EDGE_SIZE; + displayInfo.logicalWidth = width; + displayInfo.logicalHeight = height; // use the parent context (not the mocked one) to obtain the display layout // this is done to avoid unnecessary mocking while allowing for custom display dimensions @@ -159,38 +185,57 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); mPipDisplayLayoutState.setDisplayLayout(displayLayout); - setUpStaticSystemPropertiesSession(); mSizeSpecSource = new PhoneSizeSpecSource(mContext, mPipDisplayLayoutState); // no overridden min edge size by default mSizeSpecSource.setOverrideMinSize(null); } - @After - public void cleanUp() { - sStaticMockitoSession.finishMocking(); + @Test + public void testGetMaxSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedMaxSizes, + (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio)); + } + + @Test + public void testGetDefaultSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedDefaultSizes, + (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio)); + } + + @Test + public void testGetMinSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedMinSizes, + (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio)); } @Test - public void testGetMaxSize() { - forEveryTestCaseCheck(sExpectedMaxSizes, + public void testGetMaxSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedMaxSizes, (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio)); } @Test - public void testGetDefaultSize() { - forEveryTestCaseCheck(sExpectedDefaultSizes, + public void testGetDefaultSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedDefaultSizes, (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio)); } @Test - public void testGetMinSize() { - forEveryTestCaseCheck(sExpectedMinSizes, + public void testGetMinSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedMinSizes, (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio)); } @Test public void testGetSizeForAspectRatio_noOverrideMinSize() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); // an initial size with 16:9 aspect ratio Size initSize = new Size(600, 337); @@ -202,6 +247,7 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { @Test public void testGetSizeForAspectRatio_withOverrideMinSize() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); // an initial size with a 1:1 aspect ratio Size initSize = new Size(OVERRIDE_MIN_EDGE_SIZE, OVERRIDE_MIN_EDGE_SIZE); mSizeSpecSource.setOverrideMinSize(initSize); 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..f3f3c37b645d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java @@ -0,0 +1,115 @@ +/* + * 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.Handler; +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; +import org.mockito.Mock; + +/** + * 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; + + @Mock + private Handler mMainHandler; + + @Before + public void setUp() { + mPipTransitionState = new PipTransitionState(mMainHandler); + 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..e291c0e1a151 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,15 @@ public class RecentTasksControllerTest extends ShellTestCase { private DesktopModeTaskRepository mDesktopModeTaskRepository; @Mock private ActivityTaskManager mActivityTaskManager; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private IRecentTasksListener mRecentTasksListener; + @Mock + private TaskStackTransitionObserver mTaskStackTransitionObserver; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; @@ -103,17 +125,24 @@ 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); + Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver, + mMainExecutor); mRecentTasksController = spy(mRecentTasksControllerReal); mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), @@ -121,6 +150,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 +294,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 +324,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); + + // 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)); - mockitoSession.finishMocking(); + // 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 +396,45 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(t2, recentTasks.get(1).getTaskInfo1()); assertEquals(t3, recentTasks.get(2).getTaskInfo1()); assertEquals(t4, recentTasks.get(3).getTaskInfo1()); + } + + @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); - mockitoSession.finishMocking(); + // 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 +481,109 @@ 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.onTaskRunningInfoChanged(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.onTaskRunningInfoChanged(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 + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + public void onTaskMovedToFront_TaskStackObserverEnabled_triggersOnTaskMovedToFront() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo); + + verify(mRecentTasksListener).onTaskMovedToFront(taskInfo); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + public void onTaskMovedToFront_TaskStackObserverEnabled_doesNotTriggersOnTaskMovedToFront() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskMovedToFront(taskInfo); + + verify(mRecentTasksListener, never()).onTaskMovedToFront(any()); + } + + @Test public void getNullSplitBoundsNonSplitTask() { SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3); assertNull("splitBounds should be null for non-split task", sb); @@ -420,6 +629,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/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt new file mode 100644 index 000000000000..f9599702e763 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -0,0 +1,217 @@ +/* + * 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.recents + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.os.IBinder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.ShellExecutor +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 dagger.Lazy +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.MockitoAnnotations +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + + +/** + * Test class for {@link TaskStackTransitionObserver} + * + * Usage: atest WMShellUnitTests:TaskStackTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskStackTransitionObserverTest { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Mock private lateinit var shellInit: ShellInit + @Mock lateinit var testExecutor: ShellExecutor + @Mock private lateinit var transitionsLazy: Lazy<Transitions> + @Mock private lateinit var transitions: Transitions + @Mock private lateinit var mockTransitionBinder: IBinder + + private lateinit var transitionObserver: TaskStackTransitionObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + shellInit = Mockito.spy(ShellInit(testExecutor)) + whenever(transitionsLazy.get()).thenReturn(transitions) + transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) + verify(shellInit) + .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) + initRunnableCaptor.value.run() + } else { + transitionObserver.onInit() + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun testRegistersObserverAtInit() { + verify(transitions).registerObserver(same(transitionObserver)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_freeformWindow_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_fullscreenWindow_listenerNotNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(0) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(WindowConfiguration.WINDOWING_MODE_UNDEFINED) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val freeformOpenChange = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val freeformReorderChange = + createChange( + WindowManager.TRANSIT_TO_BACK, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0) + .addChange(freeformOpenChange) + .addChange(freeformReorderChange) + .build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId) + .isEqualTo(freeformOpenChange.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) + } + + class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { + var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() + + override fun onTaskMovedToFrontThroughTransition( + taskInfo: ActivityManager.RunningTaskInfo + ) { + taskInfoToBeNotified = taskInfo + } + } + + /** Simulate calling the onTransitionReady() method */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val startT = Mockito.mock(SurfaceControl.Transaction::class.java) + val finishT = Mockito.mock(SurfaceControl.Transaction::class.java) + + transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT) + } + + /** Simulate calling the onTransitionFinished() method */ + private fun callOnTransitionFinished() { + transitionObserver.onTransitionFinished(mockTransitionBinder, false) + } + + companion object { + fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowingMode + + return taskInfo + } + + fun createChange( + mode: Int, + taskInfo: ActivityManager.RunningTaskInfo + ): TransitionInfo.Change { + val change = + TransitionInfo.Change( + WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)), + Mockito.mock(SurfaceControl::class.java) + ) + change.mode = mode + change.taskInfo = taskInfo + return change + } + } +} 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/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d819261ecba2..d18fec2f24ad 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,10 +16,7 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; 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; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -31,8 +28,9 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; @@ -40,10 +38,13 @@ import static org.mockito.Mockito.clearInvocations; 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 static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; @@ -51,7 +52,7 @@ import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceSession; -import android.window.WindowContainerToken; +import android.window.RemoteTransition; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -74,6 +75,7 @@ import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -111,6 +113,8 @@ public class StageCoordinatorTests extends ShellTestCase { private TransactionPool mTransactionPool; @Mock private LaunchAdjacentController mLaunchAdjacentController; + @Mock + private DefaultMixedHandler mDefaultMixedHandler; private final Rect mBounds1 = new Rect(10, 20, 30, 40); private final Rect mBounds2 = new Rect(5, 10, 15, 20); @@ -337,14 +341,14 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testAddActivityOptions_addsBackgroundActivitiesFlags() { - Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, + Bundle bundle = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */); + ActivityOptions options = ActivityOptions.fromBundle(bundle); - assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class), - mMainStage.mRootTaskInfo.token); - assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)); - assertTrue(options.getBoolean( - KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION)); + assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); + assertThat(options.getPendingIntentBackgroundActivityStartMode()) + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); } @Test @@ -370,6 +374,96 @@ public class StageCoordinatorTests extends ShellTestCase { } } + @Test + public void testSplitIntentAndTaskWithPippedApp_launchFullscreen() { + int taskId = 9; + SplitScreenTransitions splitScreenTransitions = + spy(mStageCoordinator.getSplitTransitions()); + mStageCoordinator.setSplitTransitions(splitScreenTransitions); + mStageCoordinator.setMixedHandler(mDefaultMixedHandler); + PendingIntent pendingIntent = mock(PendingIntent.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + when(remoteTransition.getDebugName()).thenReturn(""); + // Test launching second task full screen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); + mStageCoordinator.startIntentAndTask( + pendingIntent, + null /*fillInIntent*/, + null /*option1*/, + taskId, + null /*option2*/, + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(1)) + .startFullscreenTransition(any(), any()); + + // Test launching first intent fullscreen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false); + when(mDefaultMixedHandler.isTaskInPip(taskId, mTaskOrganizer)).thenReturn(true); + mStageCoordinator.startIntentAndTask( + pendingIntent, + null /*fillInIntent*/, + null /*option1*/, + taskId, + null /*option2*/, + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(2)) + .startFullscreenTransition(any(), any()); + } + + @Test + public void testSplitIntentsWithPippedApp_launchFullscreen() { + SplitScreenTransitions splitScreenTransitions = + spy(mStageCoordinator.getSplitTransitions()); + mStageCoordinator.setSplitTransitions(splitScreenTransitions); + mStageCoordinator.setMixedHandler(mDefaultMixedHandler); + PendingIntent pendingIntent = mock(PendingIntent.class); + PendingIntent pendingIntent2 = mock(PendingIntent.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + when(remoteTransition.getDebugName()).thenReturn(""); + // Test launching second task full screen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); + mStageCoordinator.startIntents( + pendingIntent, + null /*fillInIntent*/, + null /*shortcutInfo1*/, + new Bundle(), + pendingIntent2, + null /*fillInIntent2*/, + null /*shortcutInfo1*/, + new Bundle(), + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(1)) + .startFullscreenTransition(any(), any()); + + // Test launching first intent fullscreen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false); + when(mDefaultMixedHandler.isIntentInPip(pendingIntent2)).thenReturn(true); + mStageCoordinator.startIntents( + pendingIntent, + null /*fillInIntent*/, + null /*shortcutInfo1*/, + new Bundle(), + pendingIntent2, + null /*fillInIntent2*/, + null /*shortcutInfo1*/, + new Bundle(), + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(2)) + .startFullscreenTransition(any(), any()); + } + private Transitions createTestTransitions() { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), 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/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index c5e229feaba7..acc0bce5cce9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -298,6 +298,32 @@ public class UnfoldTransitionHandlerTest { } @Test + public void fold_animationInProgress_finishesTransition() { + TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo(); + TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class); + + // Unfold + mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ false); + mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo); + mUnfoldTransitionHandler.startAnimation( + mTransition, + mock(TransitionInfo.class), + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + finishCallback + ); + + // Start animation but don't finish it + mShellUnfoldProgressProvider.onStateChangeStarted(); + mShellUnfoldProgressProvider.onStateChangeProgress(0.5f); + + // Fold + mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ true); + + verify(finishCallback).onTransitionFinished(any()); + } + + @Test public void mergeAnimation_eatsDisplayOnlyTransitions() { TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo(); mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo); 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..ca1e3f173e24 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 @@ -22,13 +22,22 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.ComponentName import android.content.Context +import android.content.pm.ActivityInfo import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay +import android.hardware.input.InputManager 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 @@ -36,11 +45,18 @@ import android.view.InputChannel import android.view.InputMonitor import android.view.InsetsSource import android.view.InsetsState +import android.view.KeyEvent import android.view.SurfaceControl import android.view.SurfaceView +import android.view.View 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.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -51,16 +67,23 @@ 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.freeform.FreeformTaskTransitionStarter +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.Assert.assertEquals 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 +92,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 +135,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 +144,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Before fun setUp() { shellInit = ShellInit(mockShellExecutor) + windowDecorByTaskIdSpy.clear() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( mContext, mockShellExecutor, @@ -128,7 +163,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + windowDecorByTaskIdSpy ) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -251,6 +287,41 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testBackEventHasRightDisplayId() { + val secondaryDisplay = createVirtualDisplay() ?: return + val secondaryDisplayId = secondaryDisplay.display.displayId + val task = createTask( + displayId = secondaryDisplayId, + windowingMode = WINDOWING_MODE_FREEFORM + ) + val windowDecor = setUpMockDecorationForTask(task) + + onTaskOpening(task) + val onClickListenerCaptor = argumentCaptor<View.OnClickListener>() + verify(windowDecor).setCaptionListeners( + onClickListenerCaptor.capture(), any(), any(), any()) + + val onClickListener = onClickListenerCaptor.firstValue + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.back_button) + + val inputManager = mock(InputManager::class.java) + mContext.addMockSystemService(InputManager::class.java, inputManager) + + val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java) + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter) + + onClickListener.onClick(view) + + val eventCaptor = argumentCaptor<KeyEvent>() + verify(inputManager, times(2)).injectInputEvent(eventCaptor.capture(), anyInt()) + + assertEquals(secondaryDisplayId, eventCaptor.firstValue.displayId) + assertEquals(secondaryDisplayId, eventCaptor.secondValue.displayId) + } + + @Test fun testCaptionIsNotCreatedWhenKeyguardIsVisible() { val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>() @@ -272,6 +343,36 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testDecorationIsNotCreatedForTopTranslucentActivities() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { + isTopActivityTransparent = true + numActivities = 1 + } + onTaskOpening(task) + + verify(mockDesktopModeWindowDecorFactory, never()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } + + @Test + fun testDecorationIsNotCreatedForSystemUIActivities() { + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + 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 +393,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 +414,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 +435,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, @@ -354,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { displayId: Int = DEFAULT_DISPLAY, @WindowConfiguration.WindowingMode windowingMode: Int, activityType: Int = ACTIVITY_TYPE_STANDARD, - focused: Boolean = true + focused: Boolean = true, + activityInfo: ActivityInfo = ActivityInfo() ): RunningTaskInfo { return TestRunningTaskInfoBuilder() .setDisplayId(displayId) @@ -362,13 +549,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { .setVisible(true) .setActivityType(activityType) .build().apply { + topActivityInfo = activityInfo isFocused = focused } } 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 +576,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..46c158908226 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,13 +18,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.WINDOWING_MODE_MULTI_WINDOW; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,35 +40,52 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.res.Configuration; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Handler; import android.os.SystemProperties; +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.testing.TestableContext; +import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.Display; +import android.view.GestureDetector; +import android.view.InsetsState; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.R; +import com.android.window.flags.Flags; 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.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.quality.Strictness; import java.util.function.Supplier; @@ -81,6 +105,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY = "persist.wm.debug.desktop_use_rounded_corners"; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + @Mock private DisplayController mMockDisplayController; @Mock @@ -96,18 +122,26 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier; @Mock - private SurfaceControl.Transaction mMockTransaction; - @Mock private SurfaceControl mMockSurfaceControl; @Mock private SurfaceControlViewHost mMockSurfaceControlViewHost; @Mock + private AttachedSurfaceControl mMockRootSurfaceControl; + @Mock private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock private TypedArray mMockRoundedCornersRadiusArray; - private final Configuration mConfiguration = new Configuration(); + @Mock + private TestTouchEventListener mMockTouchEventListener; + @Mock + private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener; + @Mock + private PackageManager mMockPackageManager; + private final InsetsState mInsetsState = new InsetsState(); + private SurfaceControl.Transaction mMockTransaction; + private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; /** Set up run before test class. */ @@ -121,11 +155,29 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Before public void setUp() { + mMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus.class) + .startMocking(); + when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory).create( any(), any(), any()); + when(mMockSurfaceControlViewHost.getRootSurfaceControl()) + .thenReturn(mMockRootSurfaceControl); + mMockTransaction = createMockSurfaceControlTransaction(); doReturn(mMockTransaction).when(mMockTransactionSupplier).get(); mTestableContext = new TestableContext(mContext); mTestableContext.ensureTestableResources(); + mContext.setMockPackageManager(mMockPackageManager); + when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); + doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); } @Test @@ -173,10 +225,53 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test - public void updateRelayoutParams_freeformAndTransparent_allowsInputFallthrough() { + @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) + public void updateRelayoutParams_appHeader_usesTaskDensity() { + final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() + .getConfiguration().densityDpi; + final int customTaskDensity = systemDensity + 300; final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); - taskInfo.taskDescription.setStatusBarAppearance( + taskInfo.configuration.densityDpi = customTaskDensity; + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) + public void updateRelayoutParams_appHeader_usesSystemDensity() { + when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true); + final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() + .getConfiguration().densityDpi; + final int customTaskDensity = systemDensity + 300; + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskInfo.configuration.densityDpi = customTaskDensity; + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity); + } + + @Test + public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance( APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); final RelayoutParams relayoutParams = new RelayoutParams(); @@ -187,14 +282,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.setTopOpaqueSystemBarsAppearance(0); final RelayoutParams relayoutParams = new RelayoutParams(); DesktopModeWindowDecoration.updateRelayoutParams( @@ -204,7 +299,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* applyStartTransactionOnDraw= */ true, /* shouldSetTaskPositionAndCrop */ false); - assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse(); + assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test @@ -220,7 +315,148 @@ 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(); + } + + @Test + public void relayout_fullscreenTask_appliesTransactionImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockTransaction).apply(); + verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any()); + } + + @Test + public void relayout_freeformTask_appliesTransactionOnDraw() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) + taskInfo.isResizeable = false; + + spyWindowDecor.relayout(taskInfo); + + verify(mMockTransaction, never()).apply(); + verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction); + } + + @Test + public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); + } + + @Test + public void relayout_fullscreenTask_postsViewHostCreation() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + + verify(mMockHandler).post(runnableArgument.capture()); + runnableArgument.getValue().run(); + verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); + } + + @Test + public void relayout_freeformTask_createsViewHostImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) + taskInfo.isResizeable = false; + + spyWindowDecor.relayout(taskInfo); + + verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); + verify(mMockHandler, never()).post(any()); + } + + @Test + public void relayout_removesExistingHandlerCallback() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + verify(mMockHandler).post(runnableArgument.capture()); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); + } + + @Test + public void close_removesExistingHandlerCallback() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + verify(mMockHandler).post(runnableArgument.capture()); + + spyWindowDecor.close(); + + verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); } private void fillRoundedCornersResources(int fillValue) { @@ -243,12 +479,16 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { - return new DesktopModeWindowDecoration(mContext, mMockDisplayController, - mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mConfiguration, + DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, mMockSurfaceControlViewHostFactory); + windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, + mMockTouchEventListener, mMockTouchEventListener); + windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); + return windowDecor; } private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) { @@ -268,4 +508,37 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return taskInfo; } + + private static boolean hasNoInputChannelFeature(RelayoutParams params) { + return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) + != 0; + } + + private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener + implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, + View.OnGenericMotionListener, DragDetector.MotionEventHandler { + + @Override + public void onClick(View v) {} + + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + return false; + } + + @Override + public boolean onLongClick(View v) { + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + + @Override + public boolean handleMotionEvent(@Nullable View v, MotionEvent ev) { + return false; + } + } } 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..f750e6b9a6fe 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 @@ -16,14 +16,20 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager +import android.content.Context +import android.content.res.Resources import android.graphics.PointF import android.graphics.Rect import android.os.IBinder +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken +import com.android.window.flags.Flags +import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.shared.DesktopModeStatus 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 @@ -33,8 +39,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.any +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** @@ -57,6 +63,10 @@ class DragPositioningCallbackUtilityTest { private lateinit var mockDisplayLayout: DisplayLayout @Mock private lateinit var mockDisplay: Display + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources @Before fun setup() { @@ -69,16 +79,15 @@ class DragPositioningCallbackUtilityTest { (i.arguments.first() as Rect).set(STABLE_BOUNDS) } - mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { - taskId = TASK_ID - token = taskToken - minWidth = MIN_WIDTH - minHeight = MIN_HEIGHT - defaultMinSize = DEFAULT_MIN - displayId = DISPLAY_ID - configuration.windowConfiguration.setBounds(STARTING_BOUNDS) - } + initializeTaskInfo() mockWindowDecoration.mDisplay = mockDisplay + mockWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width)) + .thenReturn(DESKTOP_MODE_MIN_WIDTH) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height)) + .thenReturn(DESKTOP_MODE_MIN_HEIGHT) whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @@ -93,8 +102,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -113,8 +122,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5) @@ -127,14 +136,14 @@ class DragPositioningCallbackUtilityTest { val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) - // Resize to width of 95px and width of -5px with minimum of 10px + // Resize to width of 95px and height of -5px with minimum of 10px val newX = STARTING_BOUNDS.right.toFloat() - 5 val newY = STARTING_BOUNDS.top.toFloat() + 105 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -153,8 +162,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) @@ -172,8 +181,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) @@ -189,20 +198,20 @@ 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) assertThat(repositionTaskBounds.right) - .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) + .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) assertThat(repositionTaskBounds.bottom) - .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) + .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) } @Test fun testChangeBounds_toDisallowedBounds_freezesAtLimit() { - var hasMoved = false val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) @@ -211,26 +220,127 @@ class DragPositioningCallbackUtilityTest { var newY = STARTING_BOUNDS.bottom.toFloat() + 10 var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration)) - hasMoved = true + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration)) // Resize width to 120px, height to disallowed area which should not result in a change. newX += 10 newY = DISALLOWED_RESIZE_AREA.top.toFloat() delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration)) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration)) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20) assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeLessThanMin_shouldNotChangeBounds() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 1px. The default allowed width and height are defined in + // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height + val newX = STARTING_BOUNDS.right.toFloat() - 99 + val newY = STARTING_BOUNDS.bottom.toFloat() - 99 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 20px. The default allowed width and height are defined in + // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height + val newX = STARTING_BOUNDS.right.toFloat() - 80 + val newY = STARTING_BOUNDS.bottom.toFloat() - 80 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 80) + } + + @Test + fun taskMinWidthHeightUndefined_changeBoundsLessThanDefaultMinSize_shouldNotChangeBounds() { + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 1px. The default allowed width and height are defined in the + // defaultMinSize of the TaskInfo. + val newX = STARTING_BOUNDS.right.toFloat() - 99 + val newY = STARTING_BOUNDS.bottom.toFloat() - 99 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun taskMinWidthHeightUndefined_changeBoundsToAnAllowedSize_shouldChangeBounds() { + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 50px. The default allowed width and height are defined in the + // defaultMinSize of the TaskInfo. + val newX = STARTING_BOUNDS.right.toFloat() - 50 + val newY = STARTING_BOUNDS.bottom.toFloat() - 50 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 50) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50) + } + + private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) { + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = taskMinWidth + minHeight = taskMinHeight + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.setBounds(STARTING_BOUNDS) + } + } + companion object { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 private const val MIN_HEIGHT = 10 + private const val DESKTOP_MODE_MIN_WIDTH = 20 + private const val DESKTOP_MODE_MIN_HEIGHT = 20 private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 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..4dea5a75a0e8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -0,0 +1,390 @@ +/* + * 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.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +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 SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + /** + * 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 (touchscreen or cursor). + * <p>Note that capturing input does not necessarily mean that the event will be handled. + */ + @Test + @EnableFlags(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 + @DisableFlags(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 + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeEnabled_edges() { + // The input source (touchscreen or cursor) shouldn't impact the edge resize size. + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ false); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false); + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ true); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeDisabled_edges() { + // Edge resizing is not supported for touchscreen input when the flag is disabled. + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false); + } + + private void validateCtrlTypeForEdges(boolean isTouchscreen, boolean isEdgeResizePermitted) { + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + LEFT_EDGE_POINT.x, LEFT_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_LEFT : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + TOP_EDGE_POINT.x, TOP_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + RIGHT_EDGE_POINT.x, RIGHT_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_RIGHT : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + BOTTOM_EDGE_POINT.x, BOTTOM_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); + } + + @Test + @EnableFlags(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). + // Edge resizing permitted (events from stylus/cursor) should have no impact on corners. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ 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, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + } + + @Test + @DisableFlags(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 from touchscreen. + // Edge resize permitted (indicating the event is from a cursor/stylus) should have no + // impact. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, false); + + // Points within fine corners should never pass when not from touchscreen; expect edge + // resizing only. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, false); + + // When the flag is disabled, points near the large corners should never pass. + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, 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 isTouchscreen, boolean isEdgeResizePermitted, + boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopLeftPoint.x, mTopLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopRightPoint.x, mTopRightPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomLeftPoint.x, mBottomLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + 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 isTouchscreen, boolean isEdgeResizePermitted, + boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopRightPointOutside.x, mTopRightPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomLeftPointOutside.x, mBottomLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + 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..666750485ef2 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,9 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -11,10 +14,12 @@ 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 import androidx.test.filters.SmallTest +import com.android.wm.shell.R import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController @@ -41,6 +46,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 /** @@ -79,7 +86,10 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockTransaction: SurfaceControl.Transaction @Mock private lateinit var mockTransitionBinder: IBinder - + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources private lateinit var taskPositioner: FluidResizeTaskPositioner @Before @@ -115,6 +125,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } `when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA) mockWindowDecoration.mDisplay = mockDisplay + mockWindowDecoration.mDecorWindowContext = mockContext + whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width)) + .thenReturn(DESKTOP_MODE_MIN_WIDTH) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height)) + .thenReturn(DESKTOP_MODE_MIN_HEIGHT) whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } whenever(mockTransitions.startTransition(anyInt(), any(), any())) .doReturn(mockTransitionBinder) @@ -125,8 +141,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration, mockDisplayController, mockDragStartListener, - mockTransactionFactory, - DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT + mockTransactionFactory ) } @@ -577,28 +592,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 +672,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(), @@ -848,6 +800,8 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 private const val MIN_HEIGHT = 10 + private const val DESKTOP_MODE_MIN_WIDTH = 20 + private const val DESKTOP_MODE_MIN_HEIGHT = 20 private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt new file mode 100644 index 000000000000..5582e0f46321 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -0,0 +1,212 @@ +/* + * 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.app.ActivityManager +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.Bitmap +import android.graphics.Color +import android.graphics.Rect +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.testing.TestableLooper +import android.view.Display +import android.view.LayoutInflater +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.R +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.DisplayLayout +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.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +/** + * Tests for [HandleMenu]. + * + * Build/Install/Run: + * atest WMShellUnitTests:HandleMenuTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class HandleMenuTest : ShellTestCase() { + @JvmField + @Rule + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Mock + private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var onClickListener: View.OnClickListener + @Mock + private lateinit var onTouchListener: View.OnTouchListener + @Mock + private lateinit var appIcon: Bitmap + @Mock + private lateinit var appName: CharSequence + @Mock + private lateinit var displayController: DisplayController + @Mock + private lateinit var splitScreenController: SplitScreenController + @Mock + private lateinit var displayLayout: DisplayLayout + @Mock + private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost + + private lateinit var handleMenu: HandleMenu + + @Before + fun setUp() { + val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer( + mock(SurfaceControl::class.java), + mockSurfaceControlViewHost, + ) { + SurfaceControl.Transaction() + } + val menuView = LayoutInflater.from(context).inflate( + R.layout.desktop_mode_window_decor_handle_menu, null) + whenever(mockDesktopWindowDecoration.addWindow( + anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt()) + ).thenReturn(mockAdditionalViewHostViewContainer) + whenever(mockAdditionalViewHostViewContainer.view).thenReturn(menuView) + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) + whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) + whenever(displayLayout.isLandscape).thenReturn(true) + mockDesktopWindowDecoration.mDecorWindowContext = context + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testFullscreenMenuUsesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) + val handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top-center of display. + assertTrue(handleMenu.mHandleMenuPosition.equals(16f, -512f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testFreeformMenu_usesViewHostViewContainer() { + createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer) + // Verify menu is created near top-left of task. + assertTrue(handleMenu.mHandleMenuPosition.equals(12f, 8f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testSplitLeftMenu_usesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top of split left task. + assertTrue(handleMenu.mHandleMenuPosition.equals(-624f, -512f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testSplitRightMenu_usesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top of split right task. + assertTrue(handleMenu.mHandleMenuPosition.equals(656f, -512f)) + } + + private fun createTaskInfo(windowingMode: Int, splitPosition: Int) { + val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW) + val bounds = when (windowingMode) { + WINDOWING_MODE_FULLSCREEN -> DISPLAY_BOUNDS + WINDOWING_MODE_FREEFORM -> FREEFORM_BOUNDS + WINDOWING_MODE_MULTI_WINDOW -> { + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + SPLIT_LEFT_BOUNDS + } else { + SPLIT_RIGHT_BOUNDS + } + } + else -> error("Unsupported windowing mode") + } + mockDesktopWindowDecoration.mTaskInfo = TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setWindowingMode(windowingMode) + .setBounds(bounds) + .setVisible(true) + .build() + // Calculate captionX similar to how WindowDecoration calculates it. + whenever(mockDesktopWindowDecoration.captionX).thenReturn( + (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .bounds.width() - context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_width)) / 2) + whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition) + whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer { + (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS) + } + } + + private fun createAndShowHandleMenu(): HandleMenu { + val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { + R.layout.desktop_mode_app_header + } else { + R.layout.desktop_mode_app_header + } + val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, + onClickListener, onTouchListener, appIcon, appName, displayController, + splitScreenController, true /* shouldShowWindowingPill */, + 50 /* captionHeight */ ) + handleMenu.show() + return handleMenu + } + + companion object { + private val DISPLAY_BOUNDS = Rect(0, 0, 2560, 1600) + private val FREEFORM_BOUNDS = Rect(500, 500, 2000, 1200) + private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600) + private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600) + } +} 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..a07be79579eb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -0,0 +1,228 @@ +/* + * 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.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +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() + + doReturn(mockTransaction).whenever(mockTransaction).setLayer(any(), anyInt()) + doReturn(mockTransaction).whenever(mockTransaction).setAlpha(any(), anyFloat()) + doReturn(mockTransaction).whenever(mockTransaction).show(any()) + doReturn(mockTransaction).whenever(mockTransaction).hide(any()) + doReturn(mockTransaction).whenever(mockTransaction) + .setPosition(any(), anyFloat(), anyFloat()) + doReturn(mockTransaction).whenever(mockTransaction).setWindowCrop(any(), anyInt(), anyInt()) + } + + @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() + + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + + verify(mockTransaction).show(mockResizeVeilSurface) + verify(mockTransaction).show(mockBackgroundSurface) + verify(mockTransaction).show(mockIconSurface) + verify(mockTransaction).apply() + } + + @Test + fun showVeil_displayUnavailable_doesNotShow() { + val veil = createResizeVeil(withDisplayAvailable = false) + + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + + verify(mockTransaction, never()).show(mockResizeVeilSurface) + verify(mockTransaction, never()).show(mockBackgroundSurface) + verify(mockTransaction, never()).show(mockIconSurface) + verify(mockTransaction).apply() + } + + @Test + fun showVeil_alreadyVisible_doesNotShowAgain() { + val veil = createResizeVeil() + + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + + verify(mockTransaction, times(1)).show(mockResizeVeilSurface) + verify(mockTransaction, times(1)).show(mockBackgroundSurface) + verify(mockTransaction, times(1)).show(mockIconSurface) + verify(mockTransaction, times(2)).apply() + } + + @Test + fun showVeil_reparentsVeilToNewParent() { + val veil = createResizeVeil(parent = mock()) + + val newParent = mock<SurfaceControl>() + veil.showVeil( + mockTransaction, + newParent, + Rect(0, 0, 100, 100), + taskInfo, + false /* fadeIn */ + ) + + verify(mockTransaction).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, + parent, + { mockTransaction }, + mockSurfaceControlBuilderFactory, + mockSurfaceControlViewHostFactory, + taskInfo + ) + } +} 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..f3603e1d9b46 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 @@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; @@ -42,13 +43,13 @@ 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; import android.app.ActivityManager; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; @@ -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,8 +75,9 @@ 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 com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; import org.junit.Before; import org.junit.Test; @@ -133,7 +135,6 @@ public class WindowDecorationTests extends ShellTestCase { private SurfaceControl.Transaction mMockSurfaceControlFinishT; private SurfaceControl.Transaction mMockSurfaceControlAddWindowT; private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); - private Configuration mWindowConfiguration = new Configuration(); private int mCaptionMenuWidthId; @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); @@ -304,7 +303,6 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.isFocused = true; // Density is 2. Shadow radius is 10px. Caption height is 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mWindowConfiguration.densityDpi = taskInfo.configuration.densityDpi; final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); @@ -315,14 +313,16 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockWindowContainerTransaction, never()) .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); + final SurfaceControl.Transaction t2 = mock(SurfaceControl.Transaction.class); + mMockSurfaceControlTransactions.add(t2); taskInfo.isVisible = false; windowDecor.relayout(taskInfo); - final InOrder releaseOrder = inOrder(t, mMockSurfaceControlViewHost); + final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost); releaseOrder.verify(mMockSurfaceControlViewHost).release(); - releaseOrder.verify(t).remove(captionContainerSurface); - releaseOrder.verify(t).remove(decorContainerSurface); - releaseOrder.verify(t).apply(); + releaseOrder.verify(t2).remove(captionContainerSurface); + releaseOrder.verify(t2).remove(decorContainerSurface); + releaseOrder.verify(t2).apply(); // Expect to remove two insets sources, the caption insets and the mandatory gesture insets. verify(mMockWindowContainerTransaction, Mockito.times(2)) .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); @@ -373,7 +373,7 @@ public class WindowDecorationTests extends ShellTestCase { } @Test - public void testAddWindow() { + public void testAddViewHostViewContainer() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) .getDisplay(Display.DEFAULT_DISPLAY); @@ -395,6 +395,7 @@ public class WindowDecorationTests extends ShellTestCase { final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() .setDisplayId(Display.DEFAULT_DISPLAY) .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setWindowingMode(WINDOWING_MODE_FREEFORM) .setBounds(TASK_BOUNDS) .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) .setVisible(true) @@ -409,7 +410,7 @@ public class WindowDecorationTests extends ShellTestCase { createMockSurfaceControlBuilder(additionalWindowSurface); mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder); - WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow(); + windowDecor.addTestViewContainer(); verify(additionalWindowSurfaceBuilder).setContainerLayer(); verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); @@ -423,12 +424,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) .create(any(), eq(defaultDisplay), any()); - assertThat(additionalWindow.mWindowViewHost).isNotNull(); - - additionalWindow.releaseView(); - - assertThat(additionalWindow.mWindowViewHost).isNull(); - assertThat(additionalWindow.mWindowSurface).isNull(); } @Test @@ -613,26 +608,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 +695,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) @@ -698,10 +829,40 @@ public class WindowDecorationTests extends ShellTestCase { eq(mMockTaskSurface), anyInt(), anyInt()); } + @Test + public void updateViewHost_applyTransactionOnDrawIsTrue_surfaceControlIsUpdated() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = true; + + windowDecor.updateViewHost(mRelayoutParams, mMockSurfaceControlStartT, mRelayoutResult); + + verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); + } + + @Test + public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsTrue_throwsException() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = true; + + assertThrows(IllegalArgumentException.class, + () -> windowDecor.updateViewHost( + mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult)); + } + + @Test + public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsFalse_doesNotThrow() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = false; + + windowDecor.updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult); + } private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) { return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, - taskInfo, mMockTaskSurface, mWindowConfiguration, + taskInfo, mMockTaskSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), new MockObjectSupplier<>(mMockSurfaceControlTransactions, @@ -742,16 +903,15 @@ public class WindowDecorationTests extends ShellTestCase { TestWindowDecoration(Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Configuration windowConfiguration, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, SurfaceControlViewHostFactory surfaceControlViewHostFactory) { super(context, displayController, taskOrganizer, taskInfo, taskSurface, - windowConfiguration, surfaceControlBuilderSupplier, - surfaceControlTransactionSupplier, windowContainerTransactionSupplier, - surfaceControlSupplier, surfaceControlViewHostFactory); + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, + windowContainerTransactionSupplier, surfaceControlSupplier, + surfaceControlViewHostFactory); } @Override @@ -766,21 +926,22 @@ 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); } - private WindowDecoration.AdditionalWindow addTestWindow() { + private AdditionalViewContainer addTestViewContainer() { final Resources resources = mDecorWindowContext.getResources(); - int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); - int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); - String name = "Test Window"; - WindowDecoration.AdditionalWindow additionalWindow = + final int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); + final int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + final String name = "Test Window"; + final AdditionalViewContainer additionalViewContainer = addWindow(R.layout.desktop_mode_window_decor_handle_menu, name, mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, 0 /* x */, 0 /* y */, width, height); - return additionalWindow; + return additionalViewContainer; } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt new file mode 100644 index 000000000000..d3e996b12e1f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt @@ -0,0 +1,90 @@ +/* + * 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.additionalviewcontainer + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for [AdditionalSystemViewContainer]. + * + * Build/Install/Run: + * atest WMShellUnitTests:AdditionalSystemViewContainerTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class AdditionalSystemViewContainerTest : ShellTestCase() { + @Mock + private lateinit var mockView: View + @Mock + private lateinit var mockLayoutInflater: LayoutInflater + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockWindowManager: WindowManager + private lateinit var viewContainer: AdditionalSystemViewContainer + + @Before + fun setUp() { + whenever(mockContext.getSystemService(WindowManager::class.java)) + .thenReturn(mockWindowManager) + whenever(mockContext.getSystemService(Context + .LAYOUT_INFLATER_SERVICE)).thenReturn(mockLayoutInflater) + whenever(mockLayoutInflater.inflate( + R.layout.desktop_mode_window_decor_handle_menu, null)).thenReturn(mockView) + } + + @Test + fun testReleaseView_ViewRemoved() { + viewContainer = AdditionalSystemViewContainer( + mockContext, + R.layout.desktop_mode_window_decor_handle_menu, + TASK_ID, + X, + Y, + WIDTH, + HEIGHT + ) + verify(mockWindowManager).addView(eq(mockView), any()) + viewContainer.releaseView() + verify(mockWindowManager).removeViewImmediate(mockView) + } + + companion object { + private const val X = 500 + private const val Y = 50 + private const val WIDTH = 400 + private const val HEIGHT = 600 + private const val TASK_ID = 5 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt new file mode 100644 index 000000000000..82d557a28f52 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.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.windowdecor.additionalviewcontainer + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +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 org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.function.Supplier + +/** + * Tests for [AdditionalViewHostViewContainer]. + * + * Build/Install/Run: + * atest WMShellUnitTests:AdditionalViewHostViewContainerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class AdditionalViewHostViewContainerTest : ShellTestCase() { + @Mock + private lateinit var mockTransactionSupplier: Supplier<SurfaceControl.Transaction> + @Mock + private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock + private lateinit var mockSurface: SurfaceControl + @Mock + private lateinit var mockViewHost: SurfaceControlViewHost + private lateinit var viewContainer: AdditionalViewHostViewContainer + + @Before + fun setUp() { + whenever(mockTransactionSupplier.get()).thenReturn(mockTransaction) + } + + @Test + fun testReleaseView_ViewRemoved() { + viewContainer = AdditionalViewHostViewContainer( + mockSurface, + mockViewHost, + mockTransactionSupplier + ) + viewContainer.releaseView() + verify(mockViewHost).release() + verify(mockTransaction).remove(mockSurface) + verify(mockTransaction).apply() + } +} diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index b68143d82090..94351182871a 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -2451,10 +2451,10 @@ const struct { const char script[4]; const std::unordered_map<uint32_t, uint32_t>* map; } SCRIPT_PARENTS[] = { + {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS}, {{'D', 'e', 'v', 'a'}, &DEVA_PARENTS}, {{'H', 'a', 'n', 't'}, &HANT_PARENTS}, - {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, {{'~', '~', '~', 'B'}, &___B_PARENTS}, }; diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a3dd9833219e..de9991a8be5e 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -2650,8 +2650,9 @@ bool ResTable_config::isBetterThan(const ResTable_config& o, return (mnc); } } - - if (isLocaleBetterThan(o, requested)) { + // Cheaper to check for the empty locales here before calling the function + // as we often can skip it completely. + if (requested->locale && (locale || o.locale) && isLocaleBetterThan(o, requested)) { return true; } @@ -7237,27 +7238,11 @@ void DynamicRefTable::addMapping(uint8_t buildPackageId, uint8_t runtimePackageI status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { uint32_t res = *resId; - size_t packageId = Res_GETPACKAGE(res) + 1; - if (!Res_VALIDID(res)) { // Cannot look up a null or invalid id, so no lookup needs to be done. return NO_ERROR; } - - const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res, - [](const AliasMap::value_type& pair, uint32_t val) { return pair.first < val; }); - if (alias_it != mAliasId.end() && alias_it->first == res) { - // Rewrite the resource id to its alias resource id. Since the alias resource id is a - // compile-time id, it still needs to be resolved further. - res = alias_it->second; - } - - if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) { - // No lookup needs to be done, app and framework package IDs are absolute. - *resId = res; - return NO_ERROR; - } - + const size_t packageId = Res_GETPACKAGE(res) + 1; if (packageId == 0 || (packageId == APP_PACKAGE_ID && mAppAsLib)) { // The package ID is 0x00. That means that a shared library is accessing // its own local resource. @@ -7267,6 +7252,24 @@ status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { *resId = (0xFFFFFF & (*resId)) | (((uint32_t) mAssignedPackageId) << 24); return NO_ERROR; } + // All aliases are coming from the framework, and usually have their own separate ID range, + // skipping the whole binary search is much more efficient than not finding anything. + if (packageId == SYS_PACKAGE_ID && !mAliasId.empty() && + res >= mAliasId.front().first && res <= mAliasId.back().first) { + const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res, + [](const AliasMap::value_type& pair, + uint32_t val) { return pair.first < val; }); + if (alias_it != mAliasId.end() && alias_it->first == res) { + // Rewrite the resource id to its alias resource id. Since the alias resource id is a + // compile-time id, it still needs to be resolved further. + res = alias_it->second; + } + } + if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) { + // No lookup needs to be done, app and framework package IDs are absolute. + *resId = res; + return NO_ERROR; + } // Do a proper lookup. uint8_t translatedId = mLookupTable[packageId]; diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp index 829a39617012..a218a1ff1eb6 100644 --- a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp @@ -52,10 +52,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { // Populate the DynamicRefTable with fuzzed data populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider); + std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>(); + // Make sure the object here outlives the vector it's set to, otherwise it will try + // accessing an already freed buffer and crash. 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 } 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 7e70a7a76971..341599e79662 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"], } @@ -79,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", ], @@ -93,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)=", ], }, }, @@ -112,6 +115,7 @@ cc_defaults { "libharfbuzz_ng", "libminikin", "server_configurable_flags", + "libaconfig_storage_read_api_cc" ], static_libs: [ @@ -141,7 +145,6 @@ cc_defaults { "libsync", "libui", "aconfig_text_flags_c_lib", - "server_configurable_flags", ], static_libs: [ "libEGL_blobCache", @@ -266,6 +269,7 @@ cc_defaults { cppflags: ["-Wno-conversion-null"], srcs: [ + "apex/android_canvas.cpp", "apex/android_matrix.cpp", "apex/android_paint.cpp", "apex/android_region.cpp", @@ -278,7 +282,6 @@ cc_defaults { android: { srcs: [ // sources that depend on android only libraries "apex/android_bitmap.cpp", - "apex/android_canvas.cpp", "apex/jni_runtime.cpp", ], }, @@ -334,9 +337,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", @@ -346,6 +352,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", @@ -419,17 +426,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: [ @@ -448,6 +451,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", @@ -531,16 +540,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", @@ -549,6 +566,7 @@ cc_defaults { "hwui/MinikinUtils.cpp", "hwui/PaintImpl.cpp", "hwui/Typeface.cpp", + "thread/CommonPool.cpp", "utils/Blur.cpp", "utils/Color.cpp", "utils/LinearAllocator.cpp", @@ -565,8 +583,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", @@ -583,6 +604,7 @@ cc_defaults { "SkiaCanvas.cpp", "SkiaInterpolator.cpp", "Tonemapper.cpp", + "TreeInfo.cpp", "VectorDrawable.cpp", ], @@ -599,43 +621,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", ], @@ -653,6 +664,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..5d3bc89b40dd 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__ @@ -41,22 +39,15 @@ constexpr bool clip_surfaceviews() { constexpr bool hdr_10bit_plus() { return false; } +constexpr bool initialize_gl_always() { + return false; +} } // namespace hwui_flags #endif 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; @@ -269,5 +260,9 @@ bool Properties::isDrawingEnabled() { return drawingEnabled == DrawingEnabled::On; } +bool Properties::initializeGlAlways() { + return base::GetBoolProperty(PROPERTY_INITIALIZE_GL_ALWAYS, hwui_flags::initialize_gl_always()); +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ec53070f6cb8..d3176f6879d2 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -229,6 +229,11 @@ enum DebugLevel { #define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom" +/** + * Whether to initialize GL even when HWUI is running Vulkan. + */ +#define PROPERTY_INITIALIZE_GL_ALWAYS "debug.hwui.initialize_gl_always" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -242,7 +247,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 @@ -368,6 +373,8 @@ public: static bool isDrawingEnabled(); static void setDrawingEnabled(bool enable); + static bool initializeGlAlways(); + private: static StretchEffectBehavior stretchEffectBehavior; static ProfileType sProfileType; 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 b40c14a08680..cd3ae5342f4e 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -3,6 +3,7 @@ container: "system" flag { name: "clip_shader" + is_exported: true namespace: "core_graphics" description: "API for canvas shader clipping operations" bug: "280116960" @@ -10,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" @@ -17,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" @@ -45,6 +48,7 @@ flag { flag { name: "gainmap_animations" + is_exported: true namespace: "core_graphics" description: "APIs to help enable animations involving gainmaps" bug: "296482289" @@ -52,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" @@ -66,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" @@ -77,3 +83,17 @@ 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" +} + +flag { + name: "initialize_gl_always" + namespace: "core_graphics" + description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL." + bug: "335172671" +} 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..15b2bac50c79 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), @@ -192,5 +192,14 @@ void zygote_preload_graphics() { // Preload Vulkan driver if HWUI renders with Vulkan backend. uint32_t apiVersion; vkEnumerateInstanceVersion(&apiVersion); + + if (Properties::initializeGlAlways()) { + // Even though HWUI is rendering with Vulkan, some apps still use + // GL. Preload GL driver just in case. Since this happens prior to + // forking from the zygote, apps that do not use GL are unaffected. + // Any memory that (E)GL uses for this call is in shared memory, + // and this call only happens once. + eglGetDisplay(EGL_DEFAULT_DISPLAY); + } } } diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp index 3ebf7d19202d..eac03609d72f 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 { @@ -94,6 +96,7 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR #ifdef __ANDROID__ static constexpr char gGainmapSKSL[] = R"SKSL( + uniform shader linearBase; uniform shader base; uniform shader gainmap; uniform colorFilter workingSpaceToLinearSrgb; @@ -115,7 +118,11 @@ static constexpr char gGainmapSKSL[] = R"SKSL( } half4 main(float2 coord) { - half4 S = base.eval(coord); + if (W == 0.0) { + return base.eval(coord); + } + + half4 S = linearBase.eval(coord); half4 G = gainmap.eval(coord); if (gainmapIsAlpha == 1) { G = half4(G.a, G.a, G.a, 1.0); @@ -184,8 +191,10 @@ private: SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace); // The base image shader will convert into the color space in which the gainmap is applied. - auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) - ->makeWithColorFilter(colorXformSdrToGainmap); + auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) + ->makeWithColorFilter(colorXformSdrToGainmap); + + auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions); // The gainmap image shader will ignore any color space that the gainmap has. const SkMatrix gainmapRectToDstRect = @@ -199,6 +208,7 @@ private: auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform( gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear()); + mBuilder.child("linearBase") = std::move(linearBaseImageShader); mBuilder.child("base") = std::move(baseImageShader); mBuilder.child("gainmap") = std::move(gainmapImageShader); mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); @@ -206,12 +216,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 +258,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/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 3d0a53440bfb..785aef312072 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -688,8 +688,8 @@ static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteA static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fileDescriptor, jobject padding, jobject bitmapFactoryOptions, jlong inBitmapHandle, jlong colorSpaceHandle) { -#ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC - return nullObjectReturn("Not supported on Windows"); +#ifdef _WIN32 // LayoutLib for Windows does not support F_DUPFD_CLOEXEC + return nullObjectReturn("Not supported on Windows"); #else NPE_CHECK_RETURN_ZERO(env, fileDescriptor); diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 8315c4c0dd4d..a88139d6b5d6 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; @@ -587,6 +583,16 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace, transferParams.a, transferParams.b, transferParams.c, transferParams.d, transferParams.e, transferParams.f, transferParams.g); + // Some transfer functions that are considered valid by Skia are not + // accepted by android.graphics. + if (hasException(env)) { + // Callers (e.g. Bitmap#getColorSpace) are not expected to throw an + // Exception, so clear it and return null, which is a documented + // possibility. + env->ExceptionClear(); + return nullptr; + } + jfloatArray xyzArray = env->NewFloatArray(9); jfloat xyz[9] = { xyzMatrix.vals[0][0], @@ -789,13 +795,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/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/Android.bp b/libs/input/Android.bp index 5ce990fdeb82..7a82938435af 100644 --- a/libs/input/Android.bp +++ b/libs/input/Android.bp @@ -46,9 +46,7 @@ cc_library_shared { "liblog", "libutils", "libgui", - "libui", "libinput", - "libnativewindow", ], header_libs: [ diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index 6a465442c2b4..eecc741a3bbb 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; } @@ -165,6 +165,15 @@ void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) { } } +void MouseCursorController::setSkipScreenshot(bool skip) { + std::scoped_lock lock(mLock); + if (mLocked.skipScreenshot == skip) { + return; + } + mLocked.skipScreenshot = skip; + updatePointerLocked(); +} + void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) { std::scoped_lock lock(mLock); @@ -352,6 +361,7 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) { mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER); mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY); mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId); + mLocked.pointerSprite->setSkipScreenshot(mLocked.skipScreenshot); if (mLocked.pointerAlpha > 0) { mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha); @@ -467,10 +477,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..78f6413ff111 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -47,12 +47,15 @@ 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); void setStylusHoverMode(bool stylusHoverMode); + // Set/Unset flag to hide the mouse cursor on the mirrored display + void setSkipScreenshot(bool skip); + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void reloadPointerResources(bool getAdditionalMouseResources); @@ -94,6 +97,7 @@ private: PointerIconStyle requestedPointerType; PointerIconStyle resolvedPointerType; + bool skipScreenshot{false}; bool animating{false}; } mLocked GUARDED_BY(mLock); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index f9dc5fac7e21..11b27a214984 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,45 @@ 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::setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) { + std::scoped_lock lock(getLock()); + mLocked.displaysToSkipScreenshot.insert(displayId); + mCursorController.setSkipScreenshot(true); +} + +void PointerController::clearSkipScreenshotFlags() { + std::scoped_lock lock(getLock()); + mLocked.displaysToSkipScreenshot.clear(); + mCursorController.setSkipScreenshot(false); +} + 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 +329,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 +339,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 +360,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 +373,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 +386,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..4d1e1d733cc1 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,19 @@ 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 setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override; + void clearSkipScreenshotFlags() override; virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); @@ -86,12 +87,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 +104,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 +133,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 +143,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 +162,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 +178,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 +207,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..5b00fca4d857 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 @@ -174,33 +162,37 @@ public: }; class PointerControllerTest : public Test { +private: + void loopThread(); + + std::atomic<bool> mRunning = true; + class MyLooper : public Looper { + public: + MyLooper() : Looper(false) {} + ~MyLooper() = default; + }; + 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; std::unique_ptr<MockSpriteController> mSpriteController; std::shared_ptr<PointerController> mPointerController; sp<android::gui::WindowInfosListener> mRegisteredListener; + sp<MyLooper> mLooper; private: - void loopThread(); - - std::atomic<bool> mRunning = true; - class MyLooper : public Looper { - public: - MyLooper() : Looper(false) {} - ~MyLooper() = default; - }; - sp<MyLooper> mLooper; std::thread mThread; }; -PointerControllerTest::PointerControllerTest() : mPointerSprite(new NiceMock<MockSprite>), - mLooper(new MyLooper), mThread(&PointerControllerTest::loopThread, this) { +PointerControllerTest::PointerControllerTest() + : mPointerSprite(new NiceMock<MockSprite>), + mLooper(new MyLooper), + mThread(&PointerControllerTest::loopThread, this) { mSpriteController.reset(new NiceMock<MockSpriteController>(mLooper)); mPolicy = new MockPointerControllerPolicyInterface(); @@ -217,7 +209,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 +259,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 +269,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,30 +320,85 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } -TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) { - EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId()) - << "A pointer display change does not occur when PointerController is created."; +TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) { + ensureDisplayViewportIsSet(); - ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT); + 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->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT); + 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->clearSkipScreenshotFlags(); + EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false)); + mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits, + ui::LogicalDisplayId::DEFAULT); + testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); +} + +class PointerControllerSkipScreenshotFlagTest + : public PointerControllerTest, + public testing::WithParamInterface<PointerControllerInterface::ControllerType> {}; + +TEST_P(PointerControllerSkipScreenshotFlagTest, updatesSkipScreenshotFlag) { + sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>); + EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite)); - 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."; + // Create a pointer controller + mPointerController = + PointerController::create(mPolicy, mLooper, *mSpriteController, GetParam()); + ensureDisplayViewportIsSet(ui::LogicalDisplayId::DEFAULT); - ensureDisplayViewportIsSet(42); + // By default skip screenshot flag is not set for the sprite + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false)); - EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId()) - << "The policy is notified when the pointer display changes."; + // Update pointer to sync state with sprite + mPointerController->setPosition(100, 100); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); - // Release the PointerController. - mPointerController = nullptr; + // Marking the controller to skip screenshot should update pointer sprite + mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT); + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(true)); - EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId()) - << "The pointer display changes to invalid when PointerController is destroyed."; + // Update pointer to sync state with sprite + mPointerController->move(10, 10); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); + + // Reset flag and verify again + mPointerController->clearSkipScreenshotFlags(); + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false)); + mPointerController->move(10, 10); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); } +INSTANTIATE_TEST_SUITE_P(PointerControllerSkipScreenshotFlagTest, + PointerControllerSkipScreenshotFlagTest, + testing::Values(PointerControllerInterface::ControllerType::MOUSE, + PointerControllerInterface::ControllerType::STYLUS)); + class PointerControllerWindowInfoListenerTest : public Test {}; TEST_F(PointerControllerWindowInfoListenerTest, 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)); |