diff options
Diffstat (limited to 'libs')
70 files changed, 1981 insertions, 743 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java index 68ff806c6765..65955b1d9bcc 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java @@ -21,6 +21,7 @@ import static androidx.window.util.ExtensionHelper.isZero; import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.util.Log; import androidx.annotation.NonNull; @@ -33,7 +34,8 @@ import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** A representation of a folding feature for both Extension and Sidecar. +/** + * A representation of a folding feature for both Extension and Sidecar. * For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and * {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of * {@link androidx.window.extensions.layout.FoldingFeature}. @@ -67,10 +69,11 @@ public final class CommonFoldingFeature { public static final int COMMON_STATE_UNKNOWN = -1; /** - * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar - * and Extensions do not match exactly. + * A common state that contains no folding features. For example, an in-folding device in the + * "closed" device state. */ - public static final int COMMON_STATE_FLAT = 3; + public static final int COMMON_STATE_NO_FOLDING_FEATURES = 1; + /** * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in * Sidecar and Extensions do not match exactly. @@ -78,9 +81,27 @@ public final class CommonFoldingFeature { public static final int COMMON_STATE_HALF_OPENED = 2; /** - * The possible states for a folding hinge. + * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar + * and Extensions do not match exactly. + */ + public static final int COMMON_STATE_FLAT = 3; + + /** + * A common state where the hinge state should be derived using the base state from + * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)} instead of the + * emulated state. This is an internal state and must not be passed to clients. */ - @IntDef({COMMON_STATE_UNKNOWN, COMMON_STATE_FLAT, COMMON_STATE_HALF_OPENED}) + public static final int COMMON_STATE_USE_BASE_STATE = 1000; + + /** + * The possible states for a folding hinge. Common in this context means normalized between + * extensions and sidecar. + */ + @IntDef({COMMON_STATE_UNKNOWN, + COMMON_STATE_NO_FOLDING_FEATURES, + COMMON_STATE_HALF_OPENED, + COMMON_STATE_FLAT, + COMMON_STATE_USE_BASE_STATE}) @Retention(RetentionPolicy.SOURCE) public @interface State { } @@ -167,7 +188,7 @@ public final class CommonFoldingFeature { } String stateString = featureMatcher.group(6); stateString = stateString == null ? "" : stateString; - final int state; + @State final int state; switch (stateString) { case PATTERN_STATE_FLAT: state = COMMON_STATE_FLAT; @@ -191,8 +212,8 @@ public final class CommonFoldingFeature { @NonNull private final Rect mRect; - CommonFoldingFeature(int type, int state, @NonNull Rect rect) { - assertValidState(state); + CommonFoldingFeature(int type, @State int state, @NonNull Rect rect) { + assertReportableState(state); this.mType = type; this.mState = state; if (rect.width() == 0 && rect.height() == 0) { @@ -231,13 +252,22 @@ public final class CommonFoldingFeature { } @Override + public String toString() { + return "CommonFoldingFeature=[Type: " + mType + ", state: " + mState + "]"; + } + + @Override public int hashCode() { return Objects.hash(mType, mState, mRect); } - private static void assertValidState(@Nullable Integer state) { - if (state != null && state != COMMON_STATE_FLAT - && state != COMMON_STATE_HALF_OPENED && state != COMMON_STATE_UNKNOWN) { + /** + * Checks if the provided folding feature state should be reported to clients. See + * {@link androidx.window.extensions.layout.FoldingFeature} + */ + private static void assertReportableState(@State int state) { + if (state != COMMON_STATE_FLAT && state != COMMON_STATE_HALF_OPENED + && state != COMMON_STATE_UNKNOWN) { throw new IllegalArgumentException("Invalid state: " + state + "must be either COMMON_STATE_FLAT or COMMON_STATE_HALF_OPENED"); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 0bdf98c8680f..66f27f517ab3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -19,6 +19,7 @@ package androidx.window.common; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; 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; @@ -52,13 +53,54 @@ public final class DeviceStateManagerFoldingFeatureProducer DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); private static final boolean DEBUG = false; + /** + * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} 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. + */ private int mCurrentDeviceState = INVALID_DEVICE_STATE; + /** + * 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; + @NonNull private final BaseDataProducer<String> mRawFoldSupplier; + private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + @Override + public void onStateChanged(int 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, @NonNull BaseDataProducer<String> rawFoldSupplier) { mRawFoldSupplier = rawFoldSupplier; @@ -92,12 +134,8 @@ public final class DeviceStateManagerFoldingFeatureProducer } if (mDeviceStateToPostureMap.size() > 0) { - DeviceStateCallback deviceStateCallback = (state) -> { - mCurrentDeviceState = state; - mRawFoldSupplier.getData(this::notifyFoldingFeatureChange); - }; Objects.requireNonNull(context.getSystemService(DeviceStateManager.class)) - .registerCallback(context.getMainExecutor(), deviceStateCallback); + .registerCallback(context.getMainExecutor(), mDeviceStateCallback); } } @@ -178,11 +216,18 @@ public final class DeviceStateManagerFoldingFeatureProducer } private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { - final int globalHingeState = globalHingeState(); - return parseListFromString(displayFeaturesString, globalHingeState); + return parseListFromString(displayFeaturesString, currentHingeState()); } - private int globalHingeState() { - return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); + @CommonFoldingFeature.State + private int currentHingeState() { + @CommonFoldingFeature.State + int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); + + if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { + posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN); + } + + return posture; } } 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 274dcaee5a43..575b0cea78f7 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -263,8 +263,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, return; } @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus(); + DisplayMetrics metrics = + currentStatus == STATUS_UNSUPPORTED ? null : getRearDisplayMetrics(); consumer.accept( - new RearDisplayPresentationStatus(currentStatus, getRearDisplayMetrics())); + new RearDisplayPresentationStatus(currentStatus, metrics)); } } @@ -408,6 +410,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private int getCurrentRearDisplayModeStatus() { + if (mRearDisplayState == INVALID_DEVICE_STATE) { + return WindowAreaComponent.STATUS_UNSUPPORTED; + } + if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState) || isRearDisplayActive()) { @@ -441,6 +447,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, @GuardedBy("mLock") private int getCurrentRearDisplayPresentationModeStatus() { + if (mConcurrentDisplayState == INVALID_DEVICE_STATE) { + return WindowAreaComponent.STATUS_UNSUPPORTED; + } + if (mCurrentDeviceState == mConcurrentDisplayState || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState) || isDeviceFolded()) { diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index 4b125904004a..852edef544b8 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,4 +1,4 @@ xutan@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = hwwang@google.com, lbill@google.com, madym@google.com +per-file res*/*/*.xml = hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com 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 852fae695046..48fe65d3ce59 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 @@ -139,6 +139,30 @@ public class BubbleController implements ConfigurationChangeListener { private static final boolean BUBBLE_BAR_ENABLED = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + + /** + * Common interface to send updates to bubble views. + */ + public interface BubbleViewCallback { + /** Called when the provided bubble should be removed. */ + void removeBubble(Bubble removedBubble); + /** Called when the provided bubble should be added. */ + void addBubble(Bubble addedBubble); + /** Called when the provided bubble should be updated. */ + void updateBubble(Bubble updatedBubble); + /** Called when the provided bubble should be selected. */ + void selectionChanged(BubbleViewProvider selectedBubble); + /** Called when the provided bubble's suppression state has changed. */ + void suppressionChanged(Bubble bubble, boolean isSuppressed); + /** Called when the expansion state of bubbles has changed. */ + void expansionChanged(boolean isExpanded); + /** + * Called when the order of the bubble list has changed. Depending on the expanded state + * the pointer might need to be updated. + */ + void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); + } + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -162,7 +186,6 @@ public class BubbleController implements ConfigurationChangeListener { // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBackgroundExecutor; private BubbleLogger mLogger; @@ -1005,19 +1028,21 @@ public class BubbleController implements ConfigurationChangeListener { * the bubble or bubble stack. * * Some notes: - * - Only one app bubble is supported at a time + * - Only one app bubble is supported at a time, regardless of users. Multi-users support is + * tracked in b/273533235. * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. + * @param user the {@link UserHandle} of the user to start this activity for. */ - public void showOrHideAppBubble(Intent intent) { + public void showOrHideAppBubble(Intent intent, UserHandle user) { if (intent == null || intent.getPackage() == null) { Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + ((intent != null) ? " with package: " + intent.getPackage() : " ")); return; } - PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId); + PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); @@ -1038,7 +1063,7 @@ public class BubbleController implements ConfigurationChangeListener { } } else { // App bubble does not exist, lets add and expand it - Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + Bubble b = new Bubble(intent, user, mMainExecutor); b.setShouldAutoExpand(true); inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } @@ -1320,6 +1345,58 @@ public class BubbleController implements ConfigurationChangeListener { }); } + private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() { + @Override + public void removeBubble(Bubble removedBubble) { + if (mStackView != null) { + mStackView.removeBubble(removedBubble); + } + } + + @Override + public void addBubble(Bubble addedBubble) { + if (mStackView != null) { + mStackView.addBubble(addedBubble); + } + } + + @Override + public void updateBubble(Bubble updatedBubble) { + if (mStackView != null) { + mStackView.updateBubble(updatedBubble); + } + } + + @Override + public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbleOrder, updatePointer); + } + } + + @Override + public void suppressionChanged(Bubble bubble, boolean isSuppressed) { + if (mStackView != null) { + mStackView.setBubbleSuppressed(bubble, isSuppressed); + } + } + + @Override + public void expansionChanged(boolean isExpanded) { + if (mStackView != null) { + mStackView.setExpanded(isExpanded); + } + } + + @Override + public void selectionChanged(BubbleViewProvider selectedBubble) { + if (mStackView != null) { + mStackView.setSelectedBubble(selectedBubble); + } + + } + }; + @SuppressWarnings("FieldCanBeLocal") private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { @@ -1342,7 +1419,8 @@ public class BubbleController implements ConfigurationChangeListener { // Lazy load overflow bubbles from disk loadOverflowBubblesFromDisk(); - mStackView.updateOverflowButtonDot(); + // If bubbles in the overflow have a dot, make sure the overflow shows a dot + updateOverflowButtonDot(); // Update bubbles in overflow. if (mOverflowListener != null) { @@ -1357,9 +1435,7 @@ public class BubbleController implements ConfigurationChangeListener { final Bubble bubble = removed.first; @Bubbles.DismissReason final int reason = removed.second; - if (mStackView != null) { - mStackView.removeBubble(bubble); - } + mBubbleViewCallback.removeBubble(bubble); // Leave the notification in place if we're dismissing due to user switching, or // because DND is suppressing the bubble. In both of those cases, we need to be able @@ -1389,49 +1465,47 @@ public class BubbleController implements ConfigurationChangeListener { } mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); - if (update.addedBubble != null && mStackView != null) { + if (update.addedBubble != null) { mDataRepository.addBubble(mCurrentUserId, update.addedBubble); - mStackView.addBubble(update.addedBubble); + mBubbleViewCallback.addBubble(update.addedBubble); } - if (update.updatedBubble != null && mStackView != null) { - mStackView.updateBubble(update.updatedBubble); + if (update.updatedBubble != null) { + mBubbleViewCallback.updateBubble(update.updatedBubble); } - if (update.suppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.suppressedBubble, true); + if (update.suppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true); } - if (update.unsuppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); + if (update.unsuppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false); } boolean collapseStack = update.expandedChanged && !update.expanded; // At this point, the correct bubbles are inflated in the stack. // Make sure the order in bubble data is reflected in bubble row. - if (update.orderChanged && mStackView != null) { + if (update.orderChanged) { mDataRepository.addBubbles(mCurrentUserId, update.bubbles); // if the stack is going to be collapsed, do not update pointer position // after reordering - mStackView.updateBubbleOrder(update.bubbles, !collapseStack); + mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack); } if (collapseStack) { - mStackView.setExpanded(false); + mBubbleViewCallback.expansionChanged(/* expanded= */ false); mSysuiProxy.requestNotificationShadeTopUi(false, TAG); } - if (update.selectionChanged && mStackView != null) { - mStackView.setSelectedBubble(update.selectedBubble); + if (update.selectionChanged) { + mBubbleViewCallback.selectionChanged(update.selectedBubble); } // Expanding? Apply this last. if (update.expandedChanged && update.expanded) { - if (mStackView != null) { - mStackView.setExpanded(true); - mSysuiProxy.requestNotificationShadeTopUi(true, TAG); - } + mBubbleViewCallback.expansionChanged(/* expanded= */ true); + mSysuiProxy.requestNotificationShadeTopUi(true, TAG); } mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); @@ -1442,6 +1516,19 @@ public class BubbleController implements ConfigurationChangeListener { } }; + private void updateOverflowButtonDot() { + BubbleOverflow overflow = mBubbleData.getOverflow(); + if (overflow == null) return; + + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (b.showDot()) { + overflow.setShowDot(true); + return; + } + } + overflow.setShowDot(false); + } + private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { @@ -1784,10 +1871,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void showOrHideAppBubble(Intent intent) { - mMainExecutor.execute(() -> { - BubbleController.this.showOrHideAppBubble(intent); - }); + public void showOrHideAppBubble(Intent intent, UserHandle user) { + mMainExecutor.execute( + () -> BubbleController.this.showOrHideAppBubble(intent, user)); } @Override 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 ecddbda0fff4..e5a4362e5bf0 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 @@ -236,12 +236,17 @@ public class BubbleExpandedView extends LinearLayout { fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); if (mBubble.isAppBubble()) { - PendingIntent pi = PendingIntent.getActivity(mContext, 0, + Context context = + mContext.createContextAsUser( + mBubble.getUser(), Context.CONTEXT_RESTRICTED); + PendingIntent pi = PendingIntent.getActivity( + context, + /* requestCode= */ 0, mBubble.getAppBubbleIntent() .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), PendingIntent.FLAG_IMMUTABLE, - null); + /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { 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 7d71089ef4fa..0b947c8b9b08 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 @@ -1359,16 +1359,6 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); } - void updateOverflowButtonDot() { - for (Bubble b : mBubbleData.getOverflowBubbles()) { - if (b.showDot()) { - mBubbleOverflow.setShowDot(true); - return; - } - } - mBubbleOverflow.setShowDot(false); - } - /** * Handle theme changes. */ 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 4c0a93fb9355..5555bec6a28e 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 @@ -129,12 +129,14 @@ public interface Bubbles { * the bubble or bubble stack. * * Some notes: - * - Only one app bubble is supported at a time + * - Only one app bubble is supported at a time, regardless of users. Multi-users support is + * tracked in b/273533235. * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. + * @param user the {@link UserHandle} of the user to start this activity for. */ - void showOrHideAppBubble(Intent intent); + void showOrHideAppBubble(Intent intent, UserHandle user); /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */ boolean isAppBubbleTaskId(int taskId); 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 bf226283ae54..cb1a6e7ace6b 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 @@ -22,10 +22,12 @@ import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; +import android.annotation.IntDef; import android.annotation.NonNull; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Configuration; +import android.os.SystemProperties; import android.util.ArraySet; import android.view.Surface; @@ -34,6 +36,8 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -49,8 +53,34 @@ import java.util.Set; public class TabletopModeController implements DevicePostureController.OnDevicePostureChangedListener, DisplayController.OnDisplaysChangedListener { + /** + * When {@code true}, floating windows like PiP would auto move to the position + * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode. + */ + private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP = + SystemProperties.getBoolean( + "persist.wm.debug.enable_move_floating_window_in_tabletop", false); + + /** + * Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled, + * {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise. + * See also {@link #getPreferredHalfInTabletopMode()}. + */ + private static final boolean PREFER_TOP_HALF_IN_TABLETOP = + SystemProperties.getBoolean("persist.wm.debug.prefer_top_half_in_tabletop", true); + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + @IntDef(prefix = {"PREFERRED_TABLETOP_HALF_"}, value = { + PREFERRED_TABLETOP_HALF_TOP, + PREFERRED_TABLETOP_HALF_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PreferredTabletopHalf {} + + public static final int PREFERRED_TABLETOP_HALF_TOP = 0; + public static final int PREFERRED_TABLETOP_HALF_BOTTOM = 1; + private final Context mContext; private final DevicePostureController mDevicePostureController; @@ -132,6 +162,22 @@ public class TabletopModeController implements } } + /** + * @return {@code true} if floating windows like PiP would auto move to the position + * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode. + */ + public boolean enableMoveFloatingWindowInTabletop() { + return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP; + } + + /** @return Preferred half for floating windows like PiP when in tabletop mode. */ + @PreferredTabletopHalf + public int getPreferredHalfInTabletopMode() { + return PREFER_TOP_HALF_IN_TABLETOP + ? PREFERRED_TABLETOP_HALF_TOP + : PREFERRED_TABLETOP_HALF_BOTTOM; + } + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ public void registerOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index ab968563854c..3d1ed87f1305 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -31,6 +31,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -70,7 +71,7 @@ public abstract class TvPipModule { ShellInit shellInit, ShellController shellController, TvPipBoundsState tvPipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -91,7 +92,7 @@ public abstract class TvPipModule { shellInit, shellController, tvPipBoundsState, - pipSizeSpecHandler, + pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, pipAppOpsListener, @@ -141,14 +142,15 @@ public abstract class TvPipModule { @WMSingleton @Provides static TvPipBoundsState provideTvPipBoundsState(Context context, - PipSizeSpecHandler pipSizeSpecHandler) { - return new TvPipBoundsState(context, pipSizeSpecHandler); + PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) { + return new TvPipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState); } @WMSingleton @Provides - static PipSizeSpecHandler providePipSizeSpecHelper(Context context) { - return new PipSizeSpecHandler(context); + static PipSizeSpecHandler providePipSizeSpecHelper(Context context, + PipDisplayLayoutState pipDisplayLayoutState) { + return new PipSizeSpecHandler(context, pipDisplayLayoutState); } // Handler needed for loadDrawableAsync() in PipControlsViewController @@ -203,7 +205,7 @@ public abstract class TvPipModule { TvPipMenuController tvPipMenuController, SyncTransactionQueue syncTransactionQueue, TvPipBoundsState tvPipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, PipTransitionState pipTransitionState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, @@ -215,7 +217,7 @@ public abstract class TvPipModule { PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new TvPipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipSizeSpecHandler, + syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, displayController, pipUiEventLogger, 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 948bf2d100f9..7a83d101578f 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 @@ -45,6 +45,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; 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.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellBackgroundThread; @@ -65,6 +66,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -344,6 +346,7 @@ public abstract class WMShellModule { PhonePipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, @@ -355,23 +358,24 @@ public abstract class WMShellModule { TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create( context, shellInit, shellCommandHandler, shellController, displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm, - pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipMotionHelper, - pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, - pipTouchHandler, pipTransitionController, windowManagerShellWrapper, - taskStackListener, pipParamsChangedForwarder, displayInsetsController, - oneHandedController, mainExecutor)); + pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipDisplayLayoutState, + pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTransitionState, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor)); } @WMSingleton @Provides static PipBoundsState providePipBoundsState(Context context, - PipSizeSpecHandler pipSizeSpecHandler) { - return new PipBoundsState(context, pipSizeSpecHandler); + PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) { + return new PipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState); } @WMSingleton @@ -388,8 +392,9 @@ public abstract class WMShellModule { @WMSingleton @Provides - static PipSizeSpecHandler providePipSizeSpecHelper(Context context) { - return new PipSizeSpecHandler(context); + static PipSizeSpecHandler providePipSizeSpecHelper(Context context, + PipDisplayLayoutState pipDisplayLayoutState) { + return new PipSizeSpecHandler(context, pipDisplayLayoutState); } @WMSingleton @@ -446,7 +451,7 @@ public abstract class WMShellModule { SyncTransactionQueue syncTransactionQueue, PipTransitionState pipTransitionState, PipBoundsState pipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, PipBoundsAlgorithm pipBoundsAlgorithm, PhonePipMenuController menuPhoneController, PipAnimationController pipAnimationController, @@ -458,7 +463,7 @@ public abstract class WMShellModule { PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler, + syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, pipBoundsAlgorithm, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, displayController, pipUiEventLogger, @@ -477,12 +482,12 @@ public abstract class WMShellModule { static PipTransitionController providePipTransitionController(Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler, + PipBoundsState pipBoundsState, PipDisplayLayoutState pipDisplayLayoutState, PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenOptional) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, pipSizeSpecHandler, pipTransitionState, pipMenuController, + pipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, splitScreenOptional); } 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 73a740381090..31c5e33f21e3 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 @@ -25,6 +25,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context import android.os.IBinder +import android.os.SystemProperties import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE @@ -32,6 +33,7 @@ import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.TransitionRequestInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.annotation.BinderThread import com.android.internal.protolog.common.ProtoLog @@ -115,10 +117,7 @@ class DesktopTasksController( val wct = WindowContainerTransaction() // Bring other apps to front first bringDesktopAppsToFront(wct) - - wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FREEFORM) - wct.reorder(task.getToken(), true /* onTop */) - + addMoveToDesktopChanges(wct, task.token) if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) } else { @@ -136,8 +135,7 @@ class DesktopTasksController( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId) val wct = WindowContainerTransaction() - wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FULLSCREEN) - wct.setBounds(task.getToken(), null) + addMoveToFullscreenChanges(wct, task.token) if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) } else { @@ -234,8 +232,8 @@ class DesktopTasksController( " taskId=%d", task.taskId ) - return WindowContainerTransaction().apply { - setWindowingMode(task.token, WINDOWING_MODE_FREEFORM) + return WindowContainerTransaction().also { wct -> + addMoveToDesktopChanges(wct, task.token) } } } @@ -251,15 +249,44 @@ class DesktopTasksController( " taskId=%d", task.taskId ) - return WindowContainerTransaction().apply { - setWindowingMode(task.token, WINDOWING_MODE_FULLSCREEN) - setBounds(task.token, null) + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task.token) } } } return null } + private fun addMoveToDesktopChanges( + wct: WindowContainerTransaction, + token: WindowContainerToken + ) { + wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM) + wct.reorder(token, true /* onTop */) + if (isDesktopDensityOverrideSet()) { + wct.setDensityDpi(token, getDesktopDensityDpi()) + } + } + + private fun addMoveToFullscreenChanges( + wct: WindowContainerTransaction, + token: WindowContainerToken + ) { + wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) + wct.setBounds(token, null) + if (isDesktopDensityOverrideSet()) { + wct.setDensityDpi(token, getFullscreenDensityDpi()) + } + } + + private fun getFullscreenDensityDpi(): Int { + 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) @@ -318,4 +345,18 @@ class DesktopTasksController( return result[0] } } + + companion object { + private val DESKTOP_DENSITY_OVERRIDE = + SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0) + private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) + + /** + * Check if desktop density override is enabled + */ + @JvmStatic + fun isDesktopDensityOverrideSet(): Boolean { + return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE + } + } } 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 d094c229e0f8..998728d65e6a 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 @@ -129,6 +129,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { public void onTaskInfoChanged(RunningTaskInfo taskInfo) { final State state = mTasks.get(taskInfo.taskId); final Point oldPositionInParent = state.mTaskInfo.positionInParent; + boolean oldVisible = state.mTaskInfo.isVisible; if (mWindowDecorViewModelOptional.isPresent()) { mWindowDecorViewModelOptional.get().onTaskInfoChanged(taskInfo); @@ -138,12 +139,18 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { updateRecentsForVisibleFullscreenTask(taskInfo); final Point positionInParent = state.mTaskInfo.positionInParent; - if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) { + boolean positionInParentChanged = !oldPositionInParent.equals(positionInParent); + boolean becameVisible = !oldVisible && state.mTaskInfo.isVisible; + + if (becameVisible || positionInParentChanged) { mSyncQueue.runInSync(t -> { if (!state.mLeash.isValid()) { // Task vanished before sync completion return; } + if (becameVisible) { + t.show(state.mLeash); + } t.setPosition(state.mLeash, positionInParent.x, positionInParent.y); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index 9796e4c29352..2d84d211e30a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -267,6 +267,11 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { mLaunchRootTask = taskInfo; } + if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId + && !taskInfo.equals(mHomeTask)) { + mHomeTask = taskInfo; + } + super.onTaskInfoChanged(taskInfo); } @@ -376,6 +381,7 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { final WindowContainerTransaction wct = getWindowContainerTransaction(); final Rect taskBounds = calculateBounds(); wct.setBounds(mLaunchRootTask.token, taskBounds); + wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); mSyncQueue.queue(wct); final SurfaceControl finalLeash = mLaunchRootLeash; mSyncQueue.runInSync(t -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 2624ee536b58..d961d8658b98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -70,4 +70,9 @@ interface IPip { * Sets the next pip animation type to be the alpha animation. */ oneway void setPipAnimationTypeToAlpha() = 5; + + /** + * Sets the height and visibility of the Launcher keep clear area. + */ + oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 6; } 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 23f73f614294..fe8ede67c415 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 @@ -36,6 +36,7 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.transition.Transitions; @@ -372,7 +373,8 @@ public class PipAnimationController { void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) { reattachContentOverlay( - new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo)); + new PipContentOverlay.PipAppIconOverlay(context, bounds, + () -> new IconProvider(context).getIcon(activityInfo))); } private void reattachContentOverlay(PipContentOverlay overlay) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index 5be18d852990..f08742db8ebf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -30,7 +30,6 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; -import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -44,7 +43,9 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -77,6 +78,7 @@ public class PipBoundsState { private final @NonNull Rect mExpandedBounds = new Rect(); private final @NonNull Rect mNormalMovementBounds = new Rect(); private final @NonNull Rect mExpandedMovementBounds = new Rect(); + private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState; private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); private final @NonNull Context mContext; @@ -86,8 +88,6 @@ public class PipBoundsState { private @Nullable PipReentryState mPipReentryState; private final @Nullable PipSizeSpecHandler mPipSizeSpecHandler; private @Nullable ComponentName mLastPipComponentName; - private int mDisplayId = Display.DEFAULT_DISPLAY; - private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout(); private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); private boolean mIsImeShowing; private int mImeHeight; @@ -115,15 +115,23 @@ public class PipBoundsState { * @see android.view.View#setPreferKeepClearRects */ private final Set<Rect> mUnrestrictedKeepClearAreas = new ArraySet<>(); + /** + * Additional to {@link #mUnrestrictedKeepClearAreas}, allow the caller to append named bounds + * as unrestricted keep clear area. Values in this map would be appended to + * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only. + */ + private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>(); private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); - public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler) { + public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState) { mContext = context; reloadResources(); mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; } /** Reloads the resources. */ @@ -290,31 +298,16 @@ public class PipBoundsState { return mLastPipComponentName; } - /** Get the current display id. */ - public int getDisplayId() { - return mDisplayId; - } - - /** Set the current display id for the associated display layout. */ - public void setDisplayId(int displayId) { - mDisplayId = displayId; - } - /** Returns the display's bounds. */ @NonNull public Rect getDisplayBounds() { - return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); - } - - /** Update the display layout. */ - public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { - mDisplayLayout.set(displayLayout); + return mPipDisplayLayoutState.getDisplayBounds(); } /** Get a copy of the display layout. */ @NonNull public DisplayLayout getDisplayLayout() { - return new DisplayLayout(mDisplayLayout); + return mPipDisplayLayoutState.getDisplayLayout(); } @VisibleForTesting @@ -393,6 +386,16 @@ public class PipBoundsState { mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); } + /** Add a named unrestricted keep clear area. */ + public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) { + mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea); + } + + /** Remove a named unrestricted keep clear area. */ + public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) { + mNamedUnrestrictedKeepClearAreas.remove(name); + } + @NonNull public Set<Rect> getRestrictedKeepClearAreas() { return mRestrictedKeepClearAreas; @@ -400,7 +403,10 @@ public class PipBoundsState { @NonNull public Set<Rect> getUnrestrictedKeepClearAreas() { - return mUnrestrictedKeepClearAreas; + if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas; + final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas); + unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values()); + return unrestrictedAreas; } /** @@ -568,7 +574,6 @@ public class PipBoundsState { pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); - pw.println(innerPrefix + "mDisplayId=" + mDisplayId); pw.println(innerPrefix + "mStashedState=" + mStashedState); pw.println(innerPrefix + "mStashOffset=" + mStashOffset); pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 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 53bf42a3c911..d228dfbb7705 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 @@ -20,9 +20,6 @@ import static android.util.TypedValue.COMPLEX_UNIT_DIP; import android.annotation.Nullable; import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -35,6 +32,8 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.TaskSnapshot; +import java.util.function.Supplier; + /** * Represents the content overlay used during the entering PiP animation. */ @@ -177,7 +176,9 @@ public abstract class PipContentOverlay { /** A {@link PipContentOverlay} shows app icon on solid color background. */ public static final class PipAppIconOverlay extends PipContentOverlay { private static final String TAG = PipAppIconOverlay.class.getSimpleName(); - private static final int APP_ICON_SIZE_DP = 48; + // Align with the practical / reasonable launcher:iconImageSize as in + // vendor/unbundled_google/packages/NexusLauncher/res/xml/device_profiles.xml + private static final int APP_ICON_SIZE_DP = 66; private final Context mContext; private final int mAppIconSizePx; @@ -187,14 +188,14 @@ public abstract class PipContentOverlay { private Bitmap mBitmap; - public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) { + public PipAppIconOverlay(Context context, Rect appBounds, Supplier<Drawable> iconSupplier) { mContext = context; mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); mAppBounds = new Rect(appBounds); mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(), Bitmap.Config.ARGB_8888); - prepareAppIconOverlay(activityInfo); + prepareAppIconOverlay(iconSupplier); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) .setName(LAYER_NAME) @@ -237,7 +238,7 @@ public abstract class PipContentOverlay { } } - private void prepareAppIconOverlay(ActivityInfo activityInfo) { + private void prepareAppIconOverlay(Supplier<Drawable> iconSupplier) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); final TypedArray ta = mContext.obtainStyledAttributes(new int[] { @@ -251,8 +252,7 @@ public abstract class PipContentOverlay { } finally { ta.recycle(); } - final Drawable appIcon = loadActivityInfoIcon(activityInfo, - mContext.getResources().getConfiguration().densityDpi); + final Drawable appIcon = iconSupplier.get(); final Rect appIconBounds = new Rect( mAppBounds.centerX() - mAppIconSizePx / 2, mAppBounds.centerY() - mAppIconSizePx / 2, @@ -262,24 +262,5 @@ public abstract class PipContentOverlay { appIcon.draw(canvas); mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); } - - // Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon - private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) { - final int iconRes = ai.getIconResource(); - Drawable icon = null; - // Get the preferred density icon from the app's resources - if (density != 0 && iconRes != 0) { - try { - final Resources resources = mContext.getPackageManager() - .getResourcesForApplication(ai.applicationInfo); - icon = resources.getDrawableForDensity(iconRes, density); - } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { } - } - // Get the default density icon - if (icon == null) { - icon = ai.loadIcon(mContext.getPackageManager()); - } - return icon; - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java new file mode 100644 index 000000000000..0f76af48199f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java @@ -0,0 +1,91 @@ +/* + * 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.pip; + +import android.content.Context; +import android.graphics.Rect; +import android.view.Surface; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.dagger.WMSingleton; + +import java.io.PrintWriter; + +import javax.inject.Inject; + +/** + * Acts as a source of truth for display related information for PIP. + */ +@WMSingleton +public class PipDisplayLayoutState { + private static final String TAG = PipDisplayLayoutState.class.getSimpleName(); + + private Context mContext; + private int mDisplayId; + @NonNull private DisplayLayout mDisplayLayout; + + @Inject + public PipDisplayLayoutState(Context context) { + mContext = context; + mDisplayLayout = new DisplayLayout(); + } + + /** Update the display layout. */ + public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + /** Get a copy of the display layout. */ + @NonNull + public DisplayLayout getDisplayLayout() { + return new DisplayLayout(mDisplayLayout); + } + + /** Get the display bounds */ + @NonNull + public Rect getDisplayBounds() { + return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); + } + + /** + * Apply a rotation to this layout and its parameters. + * @param targetRotation + */ + public void rotateTo(@Surface.Rotation int targetRotation) { + mDisplayLayout.rotateTo(mContext.getResources(), targetRotation); + } + + /** Get the current display id */ + public int getDisplayId() { + return mDisplayId; + } + + /** Set the current display id for the associated display layout. */ + public void setDisplayId(int displayId) { + mDisplayId = displayId; + } + + /** Dumps internal state. */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "getDisplayBounds=" + getDisplayBounds()); + } +} 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 e9d257139779..d5b9c5e8d8ff 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 @@ -79,13 +79,11 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayLayout; 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.pip.phone.PipMotionHelper; -import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; @@ -128,7 +126,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; - private final PipSizeSpecHandler mPipSizeSpecHandler; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; @@ -316,7 +314,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, - @NonNull PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @@ -332,7 +330,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSyncTransactionQueue = syncTransactionQueue; mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; - mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; mPipTransitionController = pipTransitionController; @@ -653,7 +651,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // If the displayId of the task is different than what PipBoundsHandler has, then update // it. This is possible if we entered PiP on an external display. - if (info.displayId != mPipBoundsState.getDisplayId() + if (info.displayId != mPipDisplayLayoutState.getDisplayId() && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(info.displayId); } @@ -1179,6 +1177,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * Directly update the animator bounds. + */ + public void updateAnimatorBounds(Rect bounds) { + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { + animator.updateEndValue(bounds); + } + animator.setDestinationBounds(bounds); + } + } + + /** * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { @@ -1621,15 +1633,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return animator; } - /** Computes destination bounds in old rotation and returns source hint rect if available. */ + /** Computes destination bounds in old rotation and returns source hint rect if available. + * + * Note: updates the internal state of {@link PipDisplayLayoutState} by applying a rotation + * transformation onto the display layout. + */ private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, Rect outDestinationBounds, Rect sourceHintRect) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); - - layoutCopy.rotateTo(mContext.getResources(), mNextRotation); - mPipBoundsState.setDisplayLayout(layoutCopy); - mPipSizeSpecHandler.setDisplayLayout(layoutCopy); + mPipDisplayLayoutState.rotateTo(mNextRotation); final Rect displayBounds = mPipBoundsState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); 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 a91a3424f3a0..45bb73bdc0d2 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 @@ -64,8 +64,6 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; @@ -85,7 +83,7 @@ public class PipTransition extends PipTransitionController { private final Context mContext; private final PipTransitionState mPipTransitionState; - private final PipSizeSpecHandler mPipSizeSpecHandler; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Optional<SplitScreenController> mSplitScreenOptional; @@ -116,7 +114,7 @@ public class PipTransition extends PipTransitionController { @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, PipBoundsState pipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, PipTransitionState pipTransitionState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, @@ -127,7 +125,7 @@ public class PipTransition extends PipTransitionController { pipBoundsAlgorithm, pipAnimationController); mContext = context; mPipTransitionState = pipTransitionState; - mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = pipSurfaceTransactionHelper; @@ -313,11 +311,7 @@ public class PipTransition extends PipTransitionController { // initial state under the new rotation. int rotationDelta = deltaRotation(startRotation, endRotation); if (rotationDelta != Surface.ROTATION_0) { - DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); - - layoutCopy.rotateTo(mContext.getResources(), endRotation); - mPipBoundsState.setDisplayLayout(layoutCopy); - mPipSizeSpecHandler.setDisplayLayout(layoutCopy); + mPipDisplayLayoutState.rotateTo(endRotation); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); wct.setBounds(mRequestedEnterTask, destinationBounds); @@ -398,7 +392,7 @@ public class PipTransition extends PipTransitionController { // Launcher may update the Shelf height during the animation, which will update the // destination bounds. Because this is in fixed rotation, We need to make sure the // finishTransaction is using the updated bounds in the display rotation. - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); final Rect finishBounds = new Rect(destinationBounds); rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation); mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds); @@ -495,10 +489,11 @@ public class PipTransition extends PipTransitionController { // Reparent the pip leash to the root with max layer so that we can animate it outside of // parent crop, and make sure it is not covered by other windows. final SurfaceControl pipLeash = pipChange.getLeash(); - startTransaction.reparent(pipLeash, info.getRootLeash()); + final int rootIdx = TransitionUtil.rootIndexFor(pipChange, info); + startTransaction.reparent(pipLeash, info.getRoot(rootIdx).getLeash()); startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); // Note: because of this, the bounds to animate should be translated to the root coordinate. - final Point offset = info.getRootOffset(); + final Point offset = info.getRoot(rootIdx).getOffset(); final Rect currentBounds = mPipBoundsState.getBounds(); currentBounds.offset(-offset.x, -offset.y); startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top); @@ -640,7 +635,7 @@ public class PipTransition extends PipTransitionController { @NonNull TaskInfo taskInfo) { startTransaction.apply(); finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), - mPipBoundsState.getDisplayBounds()); + mPipDisplayLayoutState.getDisplayBounds()); mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(null, null); } @@ -803,8 +798,15 @@ public class PipTransition extends PipTransitionController { if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. + // TODO(b/272819817): cleanup the null-check and extra logging. + final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; + if (!hasTopActivityInfo) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "%s: TaskInfo.topActivityInfo is null", TAG); + } if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", true)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true) + && hasTopActivityInfo) { animator.setAppIconContentOverlay( mContext, currentBounds, taskInfo.topActivityInfo); } else { @@ -834,13 +836,9 @@ public class PipTransition extends PipTransitionController { /** Computes destination bounds in old rotation and updates source hint rect if available. */ private void computeEnterPipRotatedBounds(int rotationDelta, int startRotation, int endRotation, TaskInfo taskInfo, Rect outDestinationBounds, @Nullable Rect outSourceHintRect) { - DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); - - layoutCopy.rotateTo(mContext.getResources(), endRotation); - mPipBoundsState.setDisplayLayout(layoutCopy); - mPipSizeSpecHandler.setDisplayLayout(layoutCopy); + mPipDisplayLayoutState.rotateTo(endRotation); - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); // Transform the destination bounds to current display coordinates. rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java index c6b5ce93fd35..db6138a0891f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -93,6 +93,11 @@ public class PipTransitionState { return hasEnteredPip(mState); } + /** Returns true if activity is currently entering PiP mode. */ + public boolean isEnteringPip() { + return isEnteringPip(mState); + } + public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { mInSwipePipToHomeTransition = inSwipePipToHomeTransition; } @@ -130,6 +135,11 @@ public class PipTransitionState { return state == ENTERED_PIP; } + /** Returns true if activity is currently entering PiP mode. */ + public static boolean isEnteringPip(@TransitionState int state) { + return state == ENTERING_PIP; + } + public interface OnPipTransitionStateChangedListener { void onPipTransitionStateChanged(@TransitionState int oldState, @TransitionState int newState); 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 a1483a8dedae..ea2559af6142 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 @@ -43,6 +43,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; @@ -71,6 +72,7 @@ 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.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; @@ -83,6 +85,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; @@ -115,6 +118,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb UserChangeListener { private static final String TAG = "PipController"; + private static final String LAUNCHER_KEEP_CLEAR_AREA_TAG = "hotseat"; + private static final long PIP_KEEP_CLEAR_AREAS_DELAY = SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); @@ -138,12 +143,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; private PipBoundsState mPipBoundsState; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; private PipMotionHelper mPipMotionHelper; private PipTouchHandler mTouchHandler; private PipTransitionController mPipTransitionController; private TaskStackListenerImpl mTaskStackListener; private PipParamsChangedForwarder mPipParamsChangedForwarder; private DisplayInsetsController mDisplayInsetsController; + private TabletopModeController mTabletopModeController; private Optional<OneHandedController> mOneHandedController; private final ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; @@ -181,14 +188,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb // early bail out if the keep clear areas feature is disabled return; } - // only move if already in pip, other transitions account for keep clear areas - if (mPipTransitionState.hasEnteredPip()) { + // only move if we're in PiP or transitioning into PiP + if (!mPipTransitionState.shouldBlockResizeRequest()) { Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, mPipBoundsAlgorithm); // only move if the bounds are actually different if (destBounds != mPipBoundsState.getBounds()) { - mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, - mEnterAnimationDuration, null); + if (mPipTransitionState.hasEnteredPip()) { + // if already in PiP, schedule separate animation + mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, + mEnterAnimationDuration, null); + } else if (mPipTransitionState.isEnteringPip()) { + // while entering PiP we just need to update animator bounds + mPipTaskOrganizer.updateAnimatorBounds(destBounds); + } } } } @@ -303,7 +316,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onDisplayAdded(int displayId) { - if (displayId != mPipBoundsState.getDisplayId()) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -312,7 +325,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - if (displayId != mPipBoundsState.getDisplayId()) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -322,7 +335,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { - if (mPipBoundsState.getDisplayId() == displayId) { + if (mPipDisplayLayoutState.getDisplayId() == displayId) { if (mEnablePipKeepClearAlgorithm) { mPipBoundsState.setKeepClearAreas(restricted, unrestricted); @@ -382,6 +395,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, @@ -393,6 +407,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { @@ -404,10 +419,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb return new PipController(context, shellInit, shellCommandHandler, shellController, displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, - pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTransitionState, pipTouchHandler, pipTransitionController, + pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController, + pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor) + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) .mImpl; } @@ -422,6 +437,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, @NonNull PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, @@ -433,6 +449,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController tabletopModeController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor ) { @@ -448,6 +465,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipKeepClearAlgorithm = pipKeepClearAlgorithm; mPipBoundsState = pipBoundsState; mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; mPipMotionHelper = pipMotionHelper; mPipTaskOrganizer = pipTaskOrganizer; mPipTransitionState = pipTransitionState; @@ -465,6 +483,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb .getInteger(R.integer.config_pipEnterAnimationDuration); mPipParamsChangedForwarder = pipParamsChangedForwarder; mDisplayInsetsController = displayInsetsController; + mTabletopModeController = tabletopModeController; shellInit.addInitCallback(this::onInit, this); } @@ -475,7 +494,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb INPUT_CONSUMER_PIP, mMainExecutor); mPipTransitionController.registerPipTransitionCallback(this); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { - mPipBoundsState.setDisplayId(displayId); + mPipDisplayLayoutState.setDisplayId(displayId); onDisplayChanged(mDisplayController.getDisplayLayout(displayId), false /* saveRestoreSnapFraction */); }); @@ -515,11 +534,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Ensure that we have the display info in case we get calls to update the bounds before the // listener calls back - mPipBoundsState.setDisplayId(mContext.getDisplayId()); + mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); - mPipSizeSpecHandler.setDisplayLayout(layout); - mPipBoundsState.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayLayout(layout); try { mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener); @@ -614,12 +632,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); - mDisplayInsetsController.addInsetsChangedListener(mPipBoundsState.getDisplayId(), + mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), new DisplayInsetsController.OnInsetsChangedListener() { @Override public void insetsChanged(InsetsState insetsState) { - DisplayLayout pendingLayout = - mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()); + DisplayLayout pendingLayout = mDisplayController + .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()); if (mIsInFixedRotation || pendingLayout.rotation() != mPipBoundsState.getDisplayLayout().rotation()) { @@ -627,8 +645,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb return; } int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; - onDisplayChangedUncheck( - mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()), + onDisplayChangedUncheck(mDisplayController + .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()), false /* saveRestoreSnapFraction */); int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; if (!mEnablePipKeepClearAlgorithm) { @@ -648,6 +666,42 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); + mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { + if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return; + final String tag = "tabletop-mode"; + if (!isInTabletopMode) { + mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); + return; + } + + // To prepare for the entry bounds. + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + if (mTabletopModeController.getPreferredHalfInTabletopMode() + == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { + // Prefer top, avoid the bottom half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.centerY(), + displayBounds.right, displayBounds.bottom)); + } else { + // Prefer bottom, avoid the top half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.top, + displayBounds.right, displayBounds.centerY())); + } + + // Try to move the PiP window if we have entered PiP mode. + if (mPipTransitionState.hasEnteredPip()) { + final Rect pipBounds = mPipBoundsState.getBounds(); + final Point edgeInsets = mPipSizeSpecHandler.getScreenEdgeInsets(); + if ((pipBounds.height() + 2 * edgeInsets.y) > (displayBounds.height() / 2)) { + // PiP bounds is too big to fit either half, bail early. + return; + } + mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.execute(mMovePipInResponseToKeepClearAreasChangeCallback); + } + }); + mOneHandedController.ifPresent(controller -> { controller.registerTransitionCallback( new OneHandedTransitionCallback() { @@ -714,7 +768,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) { - if (!mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) { + if (!mPipDisplayLayoutState.getDisplayLayout().isSameGeometry(layout)) { PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator != null && animator.isRunning()) { @@ -728,11 +782,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) { Runnable updateDisplayLayout = () -> { final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS - && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation(); + && mPipDisplayLayoutState.getDisplayLayout().rotation() != layout.rotation(); // update the internal state of objects subscribed to display changes - mPipSizeSpecHandler.setDisplayLayout(layout); - mPipBoundsState.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayLayout(layout); final WindowContainerTransaction wct = fromRotation ? new WindowContainerTransaction() : null; @@ -756,11 +809,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getStashedState()); // Scale PiP on density dpi change, so it appears to be the same size physically. - final boolean densityDpiChanged = mPipBoundsState.getDisplayLayout().densityDpi() != 0 - && (mPipBoundsState.getDisplayLayout().densityDpi() != layout.densityDpi()); + final boolean densityDpiChanged = + mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 + && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() + != layout.densityDpi()); if (densityDpiChanged) { final float scale = (float) layout.densityDpi() - / mPipBoundsState.getDisplayLayout().densityDpi(); + / mPipDisplayLayoutState.getDisplayLayout().densityDpi(); postChangeBounds.set(0, 0, (int) (postChangeBounds.width() * scale), (int) (postChangeBounds.height() * scale)); @@ -775,8 +830,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds, snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), - mPipBoundsState.getDisplayBounds(), - mPipBoundsState.getDisplayLayout().stableInsets()); + mPipDisplayLayoutState.getDisplayBounds(), + mPipDisplayLayoutState.getDisplayLayout().stableInsets()); if (densityDpiChanged) { // Using PipMotionHelper#movePip directly here may cause race condition since @@ -874,6 +929,19 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setLauncherKeepClearAreaHeight(boolean visible, int height) { + if (visible) { + Rect rect = new Rect( + 0, mPipBoundsState.getDisplayBounds().bottom - height, + mPipBoundsState.getDisplayBounds().right, + mPipBoundsState.getDisplayBounds().bottom); + mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); + updatePipPositionForKeepClearAreas(); + } else { + mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); + } + } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mOnIsInPipStateChangedListener = callback; if (mOnIsInPipStateChangedListener != null) { @@ -1018,7 +1086,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before // passing to mTouchHandler/mPipTaskOrganizer final Rect outBounds = new Rect(toBounds); - final int rotation = mPipBoundsState.getDisplayLayout().rotation(); + final int rotation = mPipDisplayLayoutState.getDisplayLayout().rotation(); mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds()); @@ -1042,11 +1110,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void onDisplayRotationChangedNotInPip(Context context, int toRotation) { // Update the display layout, note that we have to do this on every rotation even if we // aren't in PIP since we need to update the display layout to get the right resources - DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); - - layoutCopy.rotateTo(context.getResources(), toRotation); - mPipBoundsState.setDisplayLayout(layoutCopy); - mPipSizeSpecHandler.setDisplayLayout(layoutCopy); + mPipDisplayLayoutState.rotateTo(toRotation); } /** @@ -1059,7 +1123,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb Rect outInsetBounds, int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { // Bail early if the event is not sent to current display - if ((displayId != mPipBoundsState.getDisplayId()) || (fromRotation == toRotation)) { + if ((displayId != mPipDisplayLayoutState.getDisplayId()) || (fromRotation == toRotation)) { return false; } @@ -1083,11 +1147,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getStashedState()); // Update the display layout - DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); - - layoutCopy.rotateTo(context.getResources(), toRotation); - mPipBoundsState.setDisplayLayout(layoutCopy); - mPipSizeSpecHandler.setDisplayLayout(layoutCopy); + mPipDisplayLayoutState.rotateTo(toRotation); // Calculate the stack bounds in the new orientation based on same fraction along the // rotated movement bounds. @@ -1095,8 +1155,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb postChangeStackBounds, false /* adjustForIme */); pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), - mPipBoundsState.getDisplayBounds(), - mPipBoundsState.getDisplayLayout().stableInsets()); + mPipDisplayLayoutState.getDisplayBounds(), + mPipDisplayLayoutState.getDisplayLayout().stableInsets()); mPipBoundsAlgorithm.getInsetBounds(outInsetBounds); outBounds.set(postChangeStackBounds); @@ -1114,6 +1174,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.dump(pw, innerPrefix); mPipInputConsumer.dump(pw, innerPrefix); mPipSizeSpecHandler.dump(pw, innerPrefix); + mPipDisplayLayoutState.dump(pw, innerPrefix); } /** @@ -1240,6 +1301,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", + (controller) -> { + controller.setLauncherKeepClearAreaHeight(visible, height); + }); + } + + @Override public void setPipAnimationListener(IPipAnimationListener listener) { executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java index d03d075b38af..23988a62735d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java @@ -31,6 +31,7 @@ import android.util.Size; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.PipDisplayLayoutState; import java.io.PrintWriter; @@ -40,10 +41,9 @@ import java.io.PrintWriter; public class PipSizeSpecHandler { private static final String TAG = PipSizeSpecHandler.class.getSimpleName(); - @NonNull private final DisplayLayout mDisplayLayout = new DisplayLayout(); + @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState; - @VisibleForTesting - final SizeSpecSource mSizeSpecSourceImpl; + private final SizeSpecSource mSizeSpecSourceImpl; /** The preferred minimum (and default minimum) size specified by apps. */ @Nullable private Size mOverrideMinSize; @@ -361,8 +361,9 @@ public class PipSizeSpecHandler { } } - public PipSizeSpecHandler(Context context) { + public PipSizeSpecHandler(Context context, PipDisplayLayoutState pipDisplayLayoutState) { mContext = context; + mPipDisplayLayoutState = pipDisplayLayoutState; boolean enablePipSizeLargeScreen = SystemProperties .getBoolean("persist.wm.debug.enable_pip_size_large_screen", false); @@ -403,15 +404,9 @@ public class PipSizeSpecHandler { mSizeSpecSourceImpl.reloadResources(); } - /** Returns the display's bounds. */ @NonNull - public Rect getDisplayBounds() { - return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); - } - - /** Update the display layout. */ - public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { - mDisplayLayout.set(displayLayout); + private Rect getDisplayBounds() { + return mPipDisplayLayoutState.getDisplayBounds(); } public Point getScreenEdgeInsets() { @@ -423,11 +418,12 @@ public class PipSizeSpecHandler { */ public Rect getInsetBounds() { Rect insetBounds = new Rect(); - Rect insets = mDisplayLayout.stableInsets(); + DisplayLayout displayLayout = mPipDisplayLayoutState.getDisplayLayout(); + Rect insets = displayLayout.stableInsets(); insetBounds.set(insets.left + mScreenEdgeInsets.x, insets.top + mScreenEdgeInsets.y, - mDisplayLayout.width() - insets.right - mScreenEdgeInsets.x, - mDisplayLayout.height() - insets.bottom - mScreenEdgeInsets.y); + displayLayout.width() - insets.right - mScreenEdgeInsets.x, + displayLayout.height() - insets.bottom - mScreenEdgeInsets.y); return insetBounds; } @@ -522,8 +518,8 @@ public class PipSizeSpecHandler { public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); - pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl.toString()); - pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout); + pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl); pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize); + pw.println(innerPrefix + "mScreenEdgeInsets=" + mScreenEdgeInsets); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java index 22b3f4987e82..e1737eccc6e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java @@ -31,6 +31,7 @@ import android.view.View; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import java.lang.annotation.Retention; @@ -75,8 +76,9 @@ public class TvPipBoundsState extends PipBoundsState { private Insets mPipMenuTemporaryDecorInsets = Insets.NONE; public TvPipBoundsState(@NonNull Context context, - @NonNull PipSizeSpecHandler pipSizeSpecHandler) { - super(context, pipSizeSpecHandler); + @NonNull PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState) { + super(context, pipSizeSpecHandler, pipDisplayLayoutState); mContext = context; updateDefaultGravity(); mPreviousCollapsedGravity = mDefaultGravity; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index a437a3bc2826..d73723cc02ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -51,11 +51,11 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellController; @@ -119,7 +119,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final ShellController mShellController; private final TvPipBoundsState mTvPipBoundsState; - private final PipSizeSpecHandler mPipSizeSpecHandler; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final TvPipBoundsController mTvPipBoundsController; private final PipAppOpsListener mAppOpsListener; @@ -154,7 +154,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ShellInit shellInit, ShellController shellController, TvPipBoundsState tvPipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -174,7 +174,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal shellInit, shellController, tvPipBoundsState, - pipSizeSpecHandler, + pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, pipAppOpsListener, @@ -196,7 +196,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ShellInit shellInit, ShellController shellController, TvPipBoundsState tvPipBoundsState, - PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -220,10 +220,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal DisplayLayout layout = new DisplayLayout(context, context.getDisplay()); mTvPipBoundsState = tvPipBoundsState; - mTvPipBoundsState.setDisplayLayout(layout); - mTvPipBoundsState.setDisplayId(context.getDisplayId()); - mPipSizeSpecHandler = pipSizeSpecHandler; - mPipSizeSpecHandler.setDisplayLayout(layout); + + mPipDisplayLayoutState = pipDisplayLayoutState; + mPipDisplayLayoutState.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayId(context.getDisplayId()); + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; mTvPipBoundsController = tvPipBoundsController; mTvPipBoundsController.setListener(this); @@ -392,7 +393,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { - if (mTvPipBoundsState.getDisplayId() == displayId) { + if (mPipDisplayLayoutState.getDisplayId() == displayId) { boolean unrestrictedAreasChanged = !Objects.equals(unrestricted, mTvPipBoundsState.getUnrestrictedKeepClearAreas()); mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index be9b9361b359..f6856f15f16f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -28,6 +28,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -36,7 +37,6 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.PipUtils; -import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.Objects; @@ -51,7 +51,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, - @NonNull PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @@ -63,8 +63,8 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, ShellExecutor mainExecutor) { - super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler, - boundsHandler, pipMenuController, pipAnimationController, + super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, + pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController, surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); 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 e1c089550c2d..e09c3c9e4d3f 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 @@ -37,6 +37,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; @@ -128,6 +129,7 @@ class SplitScreenTransitions { final int mode = info.getChanges().get(i).getMode(); if (mode == TRANSIT_CHANGE) { + final int rootIdx = TransitionUtil.rootIndexFor(change, info); if (change.getParent() != null) { // This is probably reparented, so we want the parent to be immediately visible final TransitionInfo.Change parentChange = info.getChange(change.getParent()); @@ -135,7 +137,7 @@ class SplitScreenTransitions { t.setAlpha(parentChange.getLeash(), 1.f); // and then animate this layer outside the parent (since, for example, this is // the home task animating from fullscreen to part-screen). - t.reparent(leash, info.getRootLeash()); + t.reparent(leash, info.getRoot(rootIdx).getLeash()); t.setLayer(leash, info.getChanges().size() - i); // build the finish reparent/reposition mFinishTransaction.reparent(leash, parentChange.getLeash()); @@ -145,8 +147,9 @@ class SplitScreenTransitions { // TODO(shell-transitions): screenshot here final Rect startBounds = new Rect(change.getStartAbsBounds()); final Rect endBounds = new Rect(change.getEndAbsBounds()); - startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); - endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + final Point rootOffset = info.getRoot(rootIdx).getOffset(); + startBounds.offset(-rootOffset.x, -rootOffset.y); + endBounds.offset(-rootOffset.x, -rootOffset.y); startExampleResizeAnimation(leash, startBounds, endBounds); } boolean isRootOrSplitSideRoot = change.getParent() == null 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 18a3849ea17d..a1eaf851da23 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 @@ -207,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsDividerRemoteAnimating; private boolean mIsDropEntering; private boolean mIsExiting; + private boolean mIsRootTranslucent; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -258,37 +259,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }; - private final SplitScreenTransitions.TransitionFinishedCallback - mRecentTransitionFinishedCallback = - new SplitScreenTransitions.TransitionFinishedCallback() { - @Override - public void onFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - // Check if the recent transition is finished by returning to the current - // split, so we - // can restore the divider bar. - for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { - final WindowContainerTransaction.HierarchyOp op = - finishWct.getHierarchyOps().get(i); - final IBinder container = op.getContainer(); - if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() - && (mMainStage.containsContainer(container) - || mSideStage.containsContainer(container))) { - updateSurfaceBounds(mSplitLayout, finishT, - false /* applyResizingOffset */); - setDividerVisibility(true, finishT); - return; - } - } - - // Dismiss the split screen if it's not returning to split. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); - setSplitsVisible(false); - setDividerVisibility(false, finishT); - logExit(EXIT_REASON_UNKNOWN); - } - }; - protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, @@ -387,6 +357,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mMainStage.isActive(); } + /** Checks if `transition` is a pending enter-split transition. */ + public boolean isPendingEnter(IBinder transition) { + return mSplitTransitions.isPendingEnter(transition); + } + @StageType int getStageOfTask(int taskId) { if (mMainStage.containsTask(taskId)) { @@ -422,6 +397,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + if (!isSplitActive()) { + // prevent the fling divider to center transitioni if split screen didn't active. + mIsDropEntering = true; + } + setSideStagePosition(sideStagePosition, wct); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); targetStage.evictAllChildren(evictWct); @@ -436,28 +416,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // reparent the task to an invisible split root will make the activity invisible. Reorder // the root task to front to make the entering transition from pip to split smooth. wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, true); wct.reorder(targetStage.mRootTaskInfo.token, true); - wct.setForceTranslucent(targetStage.mRootTaskInfo.token, true); - // prevent the fling divider to center transition - mIsDropEntering = true; - targetStage.addTask(task, wct); - if (ENABLE_SHELL_TRANSITIONS) { - prepareEnterSplitScreen(wct); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, - null, this, null /* consumedCallback */, (finishWct, finishT) -> { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } - } /* finishedCallback */); - } else { - if (!evictWct.isEmpty()) { - wct.merge(evictWct, true /* transfer */); - } - mTaskOrganizer.applyTransaction(wct); + if (!evictWct.isEmpty()) { + wct.merge(evictWct, true /* transfer */); } + mTaskOrganizer.applyTransaction(wct); return true; } @@ -716,7 +681,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(splitRatio); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); // Make sure the launch options will put tasks in the corresponding split roots mainOptions = mainOptions != null ? mainOptions : new Bundle(); @@ -923,7 +888,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); // TODO(b/268008375): Merge APIs to start a split pair into one. if (mainTaskId != INVALID_TASK_ID) { @@ -1351,7 +1316,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { @@ -1383,7 +1348,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); - finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(finishedWCT); mSyncQueue.runInSync(at -> { @@ -1505,7 +1470,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { @@ -1709,6 +1674,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRootTaskInfo = null; mRootTaskLeash = null; + mIsRootTranslucent = false; } @@ -1727,7 +1693,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); mSplitLayout.getInvisibleBounds(mTempRect1); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(wct); @@ -1751,7 +1717,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictOtherChildren(wct, taskId); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1775,6 +1741,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } + private void setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct) { + if (mIsRootTranslucent == translucent) return; + + mIsRootTranslucent = translucent; + wct.setForceTranslucent(mRootTaskInfo.token, translucent); + } + private void onStageVisibilityChanged(StageListenerImpl stageListener) { // If split didn't active, just ignore this callback because we should already did these // on #applyExitSplitScreen. @@ -1801,12 +1774,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - if (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping) { - wct.setForceTranslucent(mRootTaskInfo.token, true); - } + setRootForceTranslucent(true, wct); } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); @@ -1938,7 +1910,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); @@ -2266,11 +2238,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } else if (isOpening && inFullscreen) { final int activityType = triggerTask.getActivityType(); - if (activityType == ACTIVITY_TYPE_HOME - || activityType == ACTIVITY_TYPE_RECENTS) { - // Enter overview panel, so start recent transition. - mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(), - mRecentTransitionFinishedCallback); + if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { + if (request.getRemoteTransition() != null) { + // starting recents/home, so don't handle this and let it fall-through to + // the remote handler. + return null; + } + // Need to use the old stuff for non-remote animations, otherwise we don't + // exit split-screen. + mSplitTransitions.setRecentTransition(transition, null /* remote */, + this::onRecentsInSplitAnimationFinish); } } } else { @@ -2400,7 +2377,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, shouldAnimate = startPendingEnterAnimation( transition, info, startTransaction, finishTransaction); } else if (mSplitTransitions.isPendingRecent(transition)) { - shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction); + onRecentsInSplitAnimationStart(startTransaction); } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); @@ -2655,10 +2632,35 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } - private boolean startPendingRecentAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + /** Call this when starting the open-recents animation while split-screen is active. */ + public void onRecentsInSplitAnimationStart(@NonNull SurfaceControl.Transaction t) { setDividerVisibility(false, t); - return true; + } + + /** Call this when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + // Check if the recent transition is finished by returning to the current + // split, so we can restore the divider bar. + for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { + final WindowContainerTransaction.HierarchyOp op = + finishWct.getHierarchyOps().get(i); + final IBinder container = op.getContainer(); + if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() + && (mMainStage.containsContainer(container) + || mSideStage.containsContainer(container))) { + updateSurfaceBounds(mSplitLayout, finishT, + false /* applyResizingOffset */); + setDividerVisibility(true, finishT); + return; + } + } + + // Dismiss the split screen if it's not returning to split. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); + setSplitsVisible(false); + setDividerVisibility(false, finishT); + logExit(EXIT_REASON_UNKNOWN); } private void addDividerBarToTransition(@NonNull TransitionInfo info, 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 75112b62c1c6..d0948923dc6e 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -25,6 +26,7 @@ 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.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.util.TransitionUtil.isOpeningType; import android.annotation.NonNull; import android.annotation.Nullable; @@ -68,14 +70,20 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { /** Pip was entered while handling an intent with its own remoteTransition. */ static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3; + /** Recents transition while split-screen active. */ + static final int TYPE_RECENTS_DURING_SPLIT = 4; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */ static final int ANIM_TYPE_GOING_HOME = 1; + /** For RECENTS_DURING_SPLIT, is set when this turns into a pair->pair task switch. */ + static final int ANIM_TYPE_PAIR_TO_PAIR = 1; + final int mType; - int mAnimType = 0; + int mAnimType = ANIM_TYPE_DEFAULT; final IBinder mTransition; Transitions.TransitionHandler mLeftoversHandler = null; @@ -167,6 +175,27 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); return handler.second; + } else if (mSplitHandler.isSplitActive() + && isOpeningType(request.getType()) + && request.getTriggerTask() != null + && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FULLSCREEN + && (request.getTriggerTask().getActivityType() == ACTIVITY_TYPE_HOME + || request.getTriggerTask().getActivityType() == ACTIVITY_TYPE_RECENTS) + && request.getRemoteTransition() != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + + "Split-Screen is active, so treat it as Mixed."); + Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler = + mPlayer.dispatchRequest(transition, request, this); + if (handler == null) { + android.util.Log.e(Transitions.TAG, " No handler for remote? This is unexpected" + + ", there should at-least be RemoteHandler."); + return null; + } + final MixedTransition mixed = new MixedTransition( + MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition); + mixed.mLeftoversHandler = handler.first; + mActiveTransitions.add(mixed); + return handler.second; } return null; } @@ -179,7 +208,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { out.getChanges().add(info.getChanges().get(i)); } } - out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y); + for (int i = 0; i < info.getRootCount(); ++i) { + out.addRoot(info.getRoot(i)); + } out.setAnimationOptions(info.getAnimationOptions()); return out; } @@ -214,6 +245,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { return animateOpenIntentWithRemoteAndPip(mixed, info, startTransaction, finishTransaction, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction, + finishCallback); } else { mActiveTransitions.remove(mixed); throw new IllegalStateException("Starting mixed animation without a known mixed type? " @@ -439,12 +473,40 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { return true; } + private boolean animateRecentsDuringSplit(@NonNull final MixedTransition mixed, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // Split-screen is only interested in the recents transition finishing (and merging), so + // just wrap finish and start recents animation directly. + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + mixed.mInFlightSubAnimations = 0; + mActiveTransitions.remove(mixed); + // If pair-to-pair switching, the post-recents clean-up isn't needed. + if (mixed.mAnimType != MixedTransition.ANIM_TYPE_PAIR_TO_PAIR) { + wct = wct != null ? wct : new WindowContainerTransaction(); + mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + } + mSplitHandler.onTransitionAnimationComplete(); + finishCallback.onTransitionFinished(wct, wctCB); + }; + mixed.mInFlightSubAnimations = 1; + mSplitHandler.onRecentsInSplitAnimationStart(startTransaction); + final boolean handled = mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info, + startTransaction, finishTransaction, finishCB); + if (!handled) { + mActiveTransitions.remove(mixed); + } + return handled; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { for (int i = 0; i < mActiveTransitions.size(); ++i) { - if (mActiveTransitions.get(i) != mergeTarget) continue; + if (mActiveTransitions.get(i).mTransition != mergeTarget) continue; MixedTransition mixed = mActiveTransitions.get(i); if (mixed.mInFlightSubAnimations <= 0) { // Already done, so no need to end it. @@ -472,6 +534,14 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + if (mSplitHandler.isPendingEnter(transition)) { + // Recents -> enter-split means that we are switching from one pair to + // another pair. + mixed.mAnimType = MixedTransition.ANIM_TYPE_PAIR_TO_PAIR; + } + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); } else { throw new IllegalStateException("Playing a mixed transition with unknown type? " + mixed.mType); @@ -491,6 +561,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { if (mixed == null) return; if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { mPipHandler.onTransitionConsumed(transition, aborted, finishT); + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); } } } 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 f66c26bb87e4..63c7969291a0 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 @@ -383,9 +383,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { continue; } // No default animation for this, so just update bounds/position. + final int rootIdx = TransitionUtil.rootIndexFor(change, info); startTransaction.setPosition(change.getLeash(), - change.getEndAbsBounds().left - info.getRootOffset().x, - change.getEndAbsBounds().top - info.getRootOffset().y); + change.getEndAbsBounds().left - info.getRoot(rootIdx).getOffset().x, + change.getEndAbsBounds().top - info.getRoot(rootIdx).getOffset().y); // Seamless display transition doesn't need to animate. if (isSeamlessDisplayChange) continue; if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) @@ -474,8 +475,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } if (backgroundColorForTransition != 0) { - addBackgroundToTransition(info.getRootLeash(), backgroundColorForTransition, - startTransaction, finishTransaction); + for (int i = 0; i < info.getRootCount(); ++i) { + addBackgroundToTransition(info.getRoot(i).getLeash(), backgroundColorForTransition, + startTransaction, finishTransaction); + } } if (postStartTransactionCallbacks.size() > 0) { @@ -520,8 +523,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private void startRotationAnimation(SurfaceControl.Transaction startTransaction, TransitionInfo.Change change, TransitionInfo info, int animHint, ArrayList<Animator> animations, Runnable onAnimFinish) { + final int rootIdx = TransitionUtil.rootIndexFor(change, info); final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession, - mTransactionPool, startTransaction, change, info.getRootLeash(), animHint); + mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), + animHint); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real // content, and background color. The item of "animGroup" will be removed if the sub // animation is finished. Then if the list becomes empty, the rotation animation is done. 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 155990a40836..039bde95815e 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 @@ -75,11 +75,29 @@ import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; import java.util.Arrays; -/** Plays transition animations */ +/** + * Plays transition animations. Within this player, each transition has a lifecycle. + * 1. When a transition is directly started or requested, it is added to "pending" state. + * 2. Once WMCore applies the transition and notifies, the transition moves to "ready" state. + * 3. When a transition starts animating, it is moved to the "active" state. + * + * Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> | + * --merge--> MERGED --^ + * + * At the moment, only one transition can be animating at a time. While a transition is animating, + * transitions will be queued in the "ready" state for their turn. At the same time, whenever a + * transition makes it to the head of the "ready" queue, it will attempt to merge to with the + * "active" transition. If the merge succeeds, it will be moved to the "active" transition's + * "merged" and then the next "ready" transition can attempt to merge. + * + * Once the "active" transition animation is finished, it will be removed from the "active" list + * and then the next "ready" transition can play. + */ public class Transitions implements RemoteCallable<Transitions> { static final String TAG = "ShellTransitions"; @@ -150,14 +168,22 @@ public class Transitions implements RemoteCallable<Transitions> { private static final class ActiveTransition { IBinder mToken; TransitionHandler mHandler; - boolean mMerged; boolean mAborted; TransitionInfo mInfo; SurfaceControl.Transaction mStartT; SurfaceControl.Transaction mFinishT; + + /** Ordered list of transitions which have been merged into this one. */ + private ArrayList<ActiveTransition> mMerged; } - /** Keeps track of currently playing transitions in the order of receipt. */ + /** Keeps track of transitions which have been started, but aren't ready yet. */ + private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>(); + + /** Keeps track of transitions which are ready to play but still waiting for their turn. */ + private final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>(); + + /** Keeps track of currently playing transitions. For now, there can only be 1 max. */ private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>(); public Transitions(@NonNull Context context, @@ -322,7 +348,8 @@ public class Transitions implements RemoteCallable<Transitions> { * will be executed when the last active transition is finished. */ public void runOnIdle(Runnable runnable) { - if (mActiveTransitions.isEmpty()) { + if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty() + && mReadyTransitions.isEmpty()) { runnable.run(); } else { mRunWhenIdleQueue.add(runnable); @@ -389,8 +416,8 @@ public class Transitions implements RemoteCallable<Transitions> { private static void setupAnimHierarchy(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { boolean isOpening = isOpeningType(info.getType()); - if (info.getRootLeash().isValid()) { - t.show(info.getRootLeash()); + for (int i = 0; i < info.getRootCount(); ++i) { + t.show(info.getRoot(i).getLeash()); } final int numChanges = info.getChanges().size(); // Put animating stuff above this line and put static stuff below it. @@ -408,10 +435,12 @@ public class Transitions implements RemoteCallable<Transitions> { boolean hasParent = change.getParent() != null; + final int rootIdx = TransitionUtil.rootIndexFor(change, info); if (!hasParent) { - t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, - change.getStartAbsBounds().top - info.getRootOffset().y); + t.reparent(leash, info.getRoot(rootIdx).getLeash()); + t.setPosition(leash, + change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x, + change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y); } final int layer; // Put all the OPEN/SHOW on top @@ -441,9 +470,9 @@ public class Transitions implements RemoteCallable<Transitions> { } } - private int findActiveTransition(IBinder token) { - for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { - if (mActiveTransitions.get(i).mToken == token) return i; + private static int findByToken(ArrayList<ActiveTransition> list, IBinder token) { + for (int i = list.size() - 1; i >= 0; --i) { + if (list.get(i).mToken == token) return i; } return -1; } @@ -481,14 +510,24 @@ public class Transitions implements RemoteCallable<Transitions> { @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", transitionToken, info); - final int activeIdx = findActiveTransition(transitionToken); + final int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { - throw new IllegalStateException("Got transitionReady for non-active transition " + throw new IllegalStateException("Got transitionReady for non-pending transition " + transitionToken + ". expecting one of " - + Arrays.toString(mActiveTransitions.stream().map( + + Arrays.toString(mPendingTransitions.stream().map( + activeTransition -> activeTransition.mToken).toArray())); + } + if (activeIdx > 0) { + Log.e(TAG, "Transition became ready out-of-order " + transitionToken + ". Expected" + + " order: " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } - final ActiveTransition active = mActiveTransitions.get(activeIdx); + // Move from pending to ready + final ActiveTransition active = mPendingTransitions.remove(activeIdx); + mReadyTransitions.add(active); + active.mInfo = info; + active.mStartT = t; + active.mFinishT = finishT; for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); @@ -496,15 +535,6 @@ public class Transitions implements RemoteCallable<Transitions> { if (info.getType() == TRANSIT_SLEEP) { if (activeIdx > 0) { - active.mInfo = info; - active.mStartT = t; - active.mFinishT = finishT; - if (!info.getRootLeash().isValid()) { - // Shell has some debug settings which makes calling binders with invalid - // surfaces crash, so replace it with a "real" one. - info.setRootLeash(new SurfaceControl.Builder().setName("Invalid") - .setContainerLayer().build(), 0, 0); - } // Sleep starts a process of forcing all prior transitions to finish immediately finishForSleep(null /* forceFinish */); return; @@ -513,14 +543,12 @@ public class Transitions implements RemoteCallable<Transitions> { // Allow to notify keyguard un-occluding state to KeyguardService, which can happen while // screen-off, so there might no visibility change involved. - if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) { - // Invalid root-leash implies that the transition is empty/no-op, so just do + if (info.getRootCount() == 0 && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) { + // No root-leashes implies that the transition is empty/no-op, so just do // housekeeping and return. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s", + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots (%s): %s", transitionToken, info); - t.apply(); - finishT.apply(); - onAbort(transitionToken); + onAbort(active); return; } @@ -546,40 +574,90 @@ public class Transitions implements RemoteCallable<Transitions> { // changes are underneath another change. || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT) && allOccluded)) { - t.apply(); - finishT.apply(); // Treat this as an abort since we are bypassing any merge logic and effectively // finishing immediately. - onAbort(transitionToken); - releaseSurfaces(info); + onAbort(active); return; } - active.mInfo = info; - active.mStartT = t; - active.mFinishT = finishT; setupStartState(active.mInfo, active.mStartT, active.mFinishT); - if (activeIdx > 0) { - // This is now playing at the same time as an existing animation, so try merging it. - attemptMergeTransition(mActiveTransitions.get(0), active); + if (mReadyTransitions.size() > 1) { + // There are already transitions waiting in the queue, so just return. return; } - // The normal case, just play it. - playTransition(active); + processReadyQueue(); } - /** - * Attempt to merge by delegating the transition start to the handler of the currently - * playing transition. - */ - void attemptMergeTransition(@NonNull ActiveTransition playing, - @NonNull ActiveTransition merging) { + void processReadyQueue() { + if (mReadyTransitions.isEmpty()) { + // Check if idle. + if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + + "animations finished"); + // Run all runnables from the run-when-idle queue. + for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { + mRunWhenIdleQueue.get(i).run(); + } + mRunWhenIdleQueue.clear(); + } + return; + } + final ActiveTransition ready = mReadyTransitions.get(0); + if (mActiveTransitions.isEmpty()) { + // The normal case, just play it (currently we only support 1 active transition). + mReadyTransitions.remove(0); + mActiveTransitions.add(ready); + if (ready.mAborted) { + // finish now since there's nothing to animate. Calls back into processReadyQueue + onFinish(ready, null, null); + return; + } + playTransition(ready); + // Attempt to merge any more queued-up transitions. + processReadyQueue(); + return; + } + // An existing animation is playing, so see if we can merge. + final ActiveTransition playing = mActiveTransitions.get(0); + if (ready.mAborted) { + // record as merged since it is no-op. Calls back into processReadyQueue + onMerged(playing, ready); + return; + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" + " another transition %s is still animating. Notify the animating transition" - + " in case they can be merged", merging.mToken, playing.mToken); - playing.mHandler.mergeAnimation(merging.mToken, merging.mInfo, merging.mStartT, - playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb)); + + " in case they can be merged", ready.mToken, playing.mToken); + playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, + playing.mToken, (wct, cb) -> onMerged(playing, ready)); + } + + private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged %s", + merged.mToken); + int readyIdx = 0; + if (mReadyTransitions.isEmpty() || mReadyTransitions.get(0) != merged) { + Log.e(TAG, "Merged transition out-of-order?"); + readyIdx = mReadyTransitions.indexOf(merged); + if (readyIdx < 0) { + Log.e(TAG, "Merged a transition that is no-longer queued?"); + return; + } + } + mReadyTransitions.remove(readyIdx); + if (playing.mMerged == null) { + playing.mMerged = new ArrayList<>(); + } + playing.mMerged.add(merged); + // if it was aborted, then onConsumed has already been reported. + if (merged.mHandler != null && !merged.mAborted) { + merged.mHandler.onTransitionConsumed(merged.mToken, false /* abort */, merged.mFinishT); + } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken); + } + // See if we should merge another transition. + processReadyQueue(); } private void playTransition(@NonNull ActiveTransition active) { @@ -594,7 +672,7 @@ public class Transitions implements RemoteCallable<Transitions> { 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, cb) -> onFinish(active.mToken, wct, cb)); + active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active, wct, cb)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); return; @@ -602,7 +680,7 @@ 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, cb) -> onFinish(active.mToken, wct, cb), active.mHandler); + active.mFinishT, (wct, cb) -> onFinish(active, wct, cb), active.mHandler); } /** @@ -645,15 +723,28 @@ public class Transitions implements RemoteCallable<Transitions> { return null; } - /** Special version of finish just for dealing with no-op/invalid transitions. */ - private void onAbort(IBinder transition) { - onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */); - } + /** Aborts a transition. This will still queue it up to maintain order. */ + private void onAbort(ActiveTransition transition) { + // apply immediately since they may be "parallel" operations: We currently we use abort for + // thing which are independent to other transitions (like starting-window transfer). + transition.mStartT.apply(); + transition.mFinishT.apply(); + transition.mAborted = true; - private void onFinish(IBinder transition, - @Nullable WindowContainerTransaction wct, - @Nullable WindowContainerTransactionCallback wctCB) { - onFinish(transition, wct, wctCB, false /* abort */); + if (transition.mHandler != null) { + // Notifies to clean-up the aborted transition. + transition.mHandler.onTransitionConsumed( + transition.mToken, true /* aborted */, null /* finishTransaction */); + } + + releaseSurfaces(transition.mInfo); + + // This still went into the queue (to maintain the correct finish ordering). + if (mReadyTransitions.size() > 1) { + // There are already transitions waiting in the queue, so just return. + return; + } + processReadyQueue(); } /** @@ -665,167 +756,97 @@ public class Transitions implements RemoteCallable<Transitions> { info.releaseAnimSurfaces(); } - private void onFinish(IBinder transition, + private void onFinish(ActiveTransition active, @Nullable WindowContainerTransaction wct, - @Nullable WindowContainerTransactionCallback wctCB, - boolean abort) { - int activeIdx = findActiveTransition(transition); + @Nullable WindowContainerTransactionCallback wctCB) { + int activeIdx = mActiveTransitions.indexOf(active); if (activeIdx < 0) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " - + " a handler didn't properly deal with a merge.", new RuntimeException()); + + " a handler didn't properly deal with a merge. " + active.mToken, + new RuntimeException()); return; - } else if (activeIdx > 0) { - // This transition was merged. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:" - + " %s", abort, transition); - final ActiveTransition active = mActiveTransitions.get(activeIdx); - active.mMerged = true; - active.mAborted = abort; - if (active.mHandler != null) { - active.mHandler.onTransitionConsumed( - active.mToken, abort, abort ? null : active.mFinishT); - } - for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionMerged( - active.mToken, mActiveTransitions.get(0).mToken); - } - return; - } - final ActiveTransition active = mActiveTransitions.get(activeIdx); - active.mAborted = abort; - if (active.mAborted && active.mHandler != null) { - // Notifies to clean-up the aborted transition. - active.mHandler.onTransitionConsumed( - transition, true /* aborted */, null /* finishTransaction */); + } else if (activeIdx != 0) { + // Relevant right now since we only allow 1 active transition at a time. + Log.e(TAG, "Finishing a transition out of order. " + active.mToken); } + mActiveTransitions.remove(activeIdx); + for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted); } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animation finished (abort=%b), notifying core %s", abort, transition); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished " + + "(aborted=%b), notifying core %s", active.mAborted, active.mToken); if (active.mStartT != null) { // Applied by now, so clear immediately to remove any references. Do not set to null // yet, though, since nullness is used later to disambiguate malformed transitions. active.mStartT.clear(); } - // Merge all relevant transactions together + // Merge all associated transactions together SurfaceControl.Transaction fullFinish = active.mFinishT; - for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { - final ActiveTransition toMerge = mActiveTransitions.get(iA); - if (!toMerge.mMerged) break; - // Include start. It will be a no-op if it was already applied. Otherwise, we need it - // to maintain consistent state. - if (toMerge.mStartT != null) { - if (fullFinish == null) { - fullFinish = toMerge.mStartT; - } else { - fullFinish.merge(toMerge.mStartT); + if (active.mMerged != null) { + for (int iM = 0; iM < active.mMerged.size(); ++iM) { + final ActiveTransition toMerge = active.mMerged.get(iM); + // Include start. It will be a no-op if it was already applied. Otherwise, we need + // it to maintain consistent state. + if (toMerge.mStartT != null) { + if (fullFinish == null) { + fullFinish = toMerge.mStartT; + } else { + fullFinish.merge(toMerge.mStartT); + } } - } - if (toMerge.mFinishT != null) { - if (fullFinish == null) { - fullFinish = toMerge.mFinishT; - } else { - fullFinish.merge(toMerge.mFinishT); + if (toMerge.mFinishT != null) { + if (fullFinish == null) { + fullFinish = toMerge.mFinishT; + } else { + fullFinish.merge(toMerge.mFinishT); + } } } } if (fullFinish != null) { fullFinish.apply(); } - // Now perform all the finishes. + // Now perform all the finish callbacks (starting with the playing one and then all the + // transitions merged into it). releaseSurfaces(active.mInfo); - mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(transition, wct, wctCB); - while (activeIdx < mActiveTransitions.size()) { - if (!mActiveTransitions.get(activeIdx).mMerged) break; - ActiveTransition merged = mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); - releaseSurfaces(merged.mInfo); - } - // sift through aborted transitions - while (mActiveTransitions.size() > activeIdx - && mActiveTransitions.get(activeIdx).mAborted) { - ActiveTransition aborted = mActiveTransitions.remove(activeIdx); - // Notifies to clean-up the aborted transition. - if (aborted.mHandler != null) { - aborted.mHandler.onTransitionConsumed( - transition, true /* aborted */, null /* finishTransaction */); + mOrganizer.finishTransition(active.mToken, wct, wctCB); + if (active.mMerged != null) { + for (int iM = 0; iM < active.mMerged.size(); ++iM) { + ActiveTransition merged = active.mMerged.get(iM); + mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); + releaseSurfaces(merged.mInfo); } - mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); - for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionFinished(aborted.mToken, true); - } - releaseSurfaces(aborted.mInfo); - } - if (mActiveTransitions.size() <= activeIdx) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " - + "finished"); - // Run all runnables from the run-when-idle queue. - for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { - mRunWhenIdleQueue.get(i).run(); - } - mRunWhenIdleQueue.clear(); - return; + active.mMerged.clear(); } - // Start animating the next active transition - final ActiveTransition next = mActiveTransitions.get(activeIdx); - if (next.mInfo == null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transition after one" - + " finished, but it isn't ready yet."); - return; + + // Now that this is done, check the ready queue for more work. + processReadyQueue(); + } + + private boolean isTransitionKnown(IBinder token) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + if (mPendingTransitions.get(i).mToken == token) return true; } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transitions after one" - + " finished, so start the next one."); - playTransition(next); - // Now try to merge the rest of the transitions (re-acquire activeIdx since next may have - // finished immediately) - activeIdx = findActiveTransition(next.mToken); - if (activeIdx < 0) { - // This means 'next' finished immediately and thus re-entered this function. Since - // that is the case, just return here since all relevant logic has already run in the - // re-entered call. - return; + for (int i = 0; i < mReadyTransitions.size(); ++i) { + if (mReadyTransitions.get(i).mToken == token) return true; } - - // This logic is also convoluted because 'next' may finish immediately in response to any of - // the merge requests (eg. if it decided to "cancel" itself). - int mergeIdx = activeIdx + 1; - while (mergeIdx < mActiveTransitions.size()) { - ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx); - if (mergeCandidate.mAborted) { - // transition was aborted, so we can skip for now (still leave it in the list - // so that it gets cleaned-up in the right order). - ++mergeIdx; - continue; - } - if (mergeCandidate.mInfo == null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition merge candidate" - + " %s is not ready yet", mergeCandidate.mToken); - // The later transition should not be merged if the prior one is not ready. - return; - } - if (mergeCandidate.mMerged) { - throw new IllegalStateException("Can't merge a transition after not-merging" - + " a preceding one."); - } - attemptMergeTransition(next, mergeCandidate); - mergeIdx = findActiveTransition(mergeCandidate.mToken); - if (mergeIdx < 0) { - // This means 'next' finished immediately and thus re-entered this function. Since - // that is the case, just return here since all relevant logic has already run in - // the re-entered call. - return; + for (int i = 0; i < mActiveTransitions.size(); ++i) { + final ActiveTransition active = mActiveTransitions.get(i); + if (active.mToken == token) return true; + if (active.mMerged == null) continue; + for (int m = 0; m < active.mMerged.size(); ++m) { + if (active.mMerged.get(m).mToken == token) return true; } - ++mergeIdx; } + return false; } void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s", transitionToken, request); - if (findActiveTransition(transitionToken) >= 0) { + if (isTransitionKnown(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } final ActiveTransition active = new ActiveTransition(); @@ -858,15 +879,13 @@ public class Transitions implements RemoteCallable<Transitions> { } mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); active.mToken = transitionToken; - int insertIdx = 0; - for (; insertIdx < mActiveTransitions.size(); ++insertIdx) { - if (mActiveTransitions.get(insertIdx).mInfo == null) { - // A `startNewTransition` was sent to WMCore, but wasn't acknowledged before WMCore - // made this request, so insert this request beforehand to keep order in sync. - break; - } - } - mActiveTransitions.add(insertIdx, active); + // 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 + // `startNewTransition` but if we have a request now, it means WM created the request + // transition before it acknowledged any of the pending `startNew` transitions. So, insert + // it at the front. + mPendingTransitions.add(0, active); } /** Start a new transition directly. */ @@ -875,7 +894,7 @@ public class Transitions implements RemoteCallable<Transitions> { final ActiveTransition active = new ActiveTransition(); active.mHandler = handler; active.mToken = mOrganizer.startNewTransition(type, wct); - mActiveTransitions.add(active); + mPendingTransitions.add(active); return active.mToken; } @@ -894,27 +913,38 @@ public class Transitions implements RemoteCallable<Transitions> { * @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 finishForSleep(@Nullable IBinder forceFinish) { - if (mActiveTransitions.isEmpty() || mSleepHandler.mSleepTransitions.isEmpty()) { + private void finishForSleep(@Nullable ActiveTransition forceFinish) { + if ((mActiveTransitions.isEmpty() && mReadyTransitions.isEmpty()) + || mSleepHandler.mSleepTransitions.isEmpty()) { + // Done finishing things. + // Prevent any weird leaks... shouldn't happen though. + mSleepHandler.mSleepTransitions.clear(); return; } - if (forceFinish != null && mActiveTransitions.get(0).mToken == forceFinish) { + if (forceFinish != null && mActiveTransitions.contains(forceFinish)) { Log.e(TAG, "Forcing transition to finish due to sleep timeout: " - + mActiveTransitions.get(0).mToken); - onFinish(mActiveTransitions.get(0).mToken, null, null, true); + + forceFinish.mToken); + forceFinish.mAborted = true; + // Last notify of it being consumed. Note: mHandler should never be null, + // but check just to be safe. + if (forceFinish.mHandler != null) { + forceFinish.mHandler.onTransitionConsumed( + forceFinish.mToken, true /* aborted */, null /* finishTransaction */); + } + onFinish(forceFinish, null, null); } final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction(); while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) { final ActiveTransition playing = mActiveTransitions.get(0); - int sleepIdx = findActiveTransition(mSleepHandler.mSleepTransitions.get(0)); + int sleepIdx = findByToken(mReadyTransitions, mSleepHandler.mSleepTransitions.get(0)); if (sleepIdx >= 0) { // Try to signal that we are sleeping by attempting to merge the sleep transition // into the playing one. - final ActiveTransition nextSleep = mActiveTransitions.get(sleepIdx); + final ActiveTransition nextSleep = mReadyTransitions.get(sleepIdx); playing.mHandler.mergeAnimation(nextSleep.mToken, nextSleep.mInfo, dummyT, playing.mToken, (wct, cb) -> {}); } else { - Log.e(TAG, "Couldn't find sleep transition in active list: " + Log.e(TAG, "Couldn't find sleep transition in ready list: " + mSleepHandler.mSleepTransitions.get(0)); } // it's possible to complete immediately. If that happens, just repeat the signal @@ -922,8 +952,7 @@ public class Transitions implements RemoteCallable<Transitions> { // finishing immediately. if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) { // Give it a (very) short amount of time to process it before forcing. - mMainExecutor.executeDelayed( - () -> finishForSleep(playing.mToken), SLEEP_ALLOWANCE_MS); + mMainExecutor.executeDelayed(() -> finishForSleep(playing), SLEEP_ALLOWANCE_MS); break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java index 8c6e1e7f5f1b..7595c9617709 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -139,11 +139,12 @@ public class TransitionUtil { // changes should be ordered top-to-bottom in z final int mode = change.getMode(); - t.reparent(leash, info.getRootLeash()); + final int rootIdx = TransitionUtil.rootIndexFor(change, info); + t.reparent(leash, info.getRoot(rootIdx).getLeash()); final Rect absBounds = (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); - t.setPosition(leash, absBounds.left - info.getRootOffset().x, - absBounds.top - info.getRootOffset().y); + t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x, + absBounds.top - info.getRoot(rootIdx).getOffset().y); // Put all the OPEN/SHOW on top if (TransitionUtil.isOpeningType(mode)) { @@ -179,12 +180,13 @@ public class TransitionUtil { // making leashes means we have to handle them specially. return change.getLeash(); } + final int rootIdx = TransitionUtil.rootIndexFor(change, info); SurfaceControl leashSurface = new SurfaceControl.Builder() .setName(change.getLeash().toString() + "_transition-leash") .setContainerLayer() // Initial the surface visible to respect the visibility of the original surface. .setHidden(false) - .setParent(info.getRootLeash()) + .setParent(info.getRoot(rootIdx).getLeash()) .build(); // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); @@ -261,4 +263,18 @@ public class TransitionUtil { target.setRotationChange(change.getEndRotation() - change.getStartRotation()); return target; } + + /** + * Finds the "correct" root idx for a change. The change's end display is prioritized, then + * the start display. If there is no display, it will fallback on the 0th root in the + * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op). + */ + public static int rootIndexFor(@NonNull TransitionInfo.Change change, + @NonNull TransitionInfo info) { + int rootIdx = info.findRootIndex(change.getEndDisplayId()); + if (rootIdx >= 0) return rootIdx; + rootIdx = info.findRootIndex(change.getStartDisplayId()); + if (rootIdx >= 0) return rootIdx; + return 0; + } } 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 9224b3cbd798..6b7ca421f5ed 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 @@ -193,6 +193,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final DragDetector mDragDetector; private int mDragPointerId = -1; + private boolean mIsDragging; private CaptionTouchEventListener( RunningTaskInfo taskInfo, @@ -223,19 +224,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (v.getId() != R.id.caption) { return false; } - mDragDetector.onMotionEvent(e); - - if (e.getAction() != MotionEvent.ACTION_DOWN) { - return false; - } - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - if (taskInfo.isFocused) { - return false; + if (e.getAction() == MotionEvent.ACTION_DOWN) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (!taskInfo.isFocused) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, true /* onTop */); + mSyncQueue.queue(wct); + } } - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reorder(mTaskToken, true /* onTop */); - mSyncQueue.queue(wct); - return true; + return mDragDetector.onMotionEvent(e); } /** @@ -253,20 +250,24 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - break; + mIsDragging = false; + return false; } case MotionEvent.ACTION_MOVE: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + mIsDragging = true; + return true; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; } } return true; 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 2aa6d1217fa6..6b45149e35a2 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 @@ -223,6 +223,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; + private boolean mIsDragging; private int mDragPointerId = -1; private DesktopModeTouchEventListener( @@ -273,23 +274,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) { return false; } - switch (e.getAction()) { - case MotionEvent.ACTION_DOWN: - mDragDetector.onMotionEvent(e); - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - if (taskInfo.isFocused) { - return mDragDetector.isDragEvent(); - } - return false; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - boolean res = mDragDetector.isDragEvent(); - mDragDetector.onMotionEvent(e); - return res; - default: - mDragDetector.onMotionEvent(e); - return mDragDetector.isDragEvent(); - } + return mDragDetector.onMotionEvent(e); } /** @@ -313,13 +298,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - break; + mIsDragging = false; + return false; } case MotionEvent.ACTION_MOVE: { final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + mIsDragging = true; + return true; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { @@ -336,7 +323,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { c -> c.moveToFullscreen(taskInfo)); } } - break; + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; } } return true; 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 72da1089c91c..3c0ef965f4f5 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 @@ -23,6 +23,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; @@ -46,6 +47,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopTasksController; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with @@ -95,6 +97,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDesktopActive = DesktopModeStatus.isActive(mContext); } + @Override + protected Configuration getConfigurationWithOverrides( + ActivityManager.RunningTaskInfo taskInfo) { + Configuration configuration = taskInfo.getConfiguration(); + if (DesktopTasksController.isDesktopDensityOverrideSet()) { + // Density is overridden for desktop tasks. Keep system density for window decoration. + configuration.densityDpi = mContext.getResources().getConfiguration().densityDpi; + } + return configuration; + } + void setCaptionListeners( View.OnClickListener onCaptionButtonClickListener, View.OnTouchListener onCaptionTouchListener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index cf1850b92373..65b5a7a17afe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -56,10 +56,15 @@ class DragDetector { * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed */ boolean onMotionEvent(MotionEvent ev) { + final boolean isTouchScreen = + (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + if (!isTouchScreen) { + // Only touches generate noisy moves, so mouse/trackpad events don't need to filtered + // to take the slop threshold into consideration. + return mEventHandler.handleMotionEvent(ev); + } switch (ev.getActionMasked()) { case ACTION_DOWN: { - // Only touch screens generate noisy moves. - mIsDragEvent = (ev.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN; mDragPointerId = ev.getPointerId(0); float rawX = ev.getRawX(0); float rawY = ev.getRawY(0); @@ -72,8 +77,12 @@ class DragDetector { int dragPointerIndex = ev.findPointerIndex(mDragPointerId); float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + // Touches generate noisy moves, so only once the move is past the touch + // slop threshold should it be considered a drag. mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop; } + // The event handler should only be notified about 'move' events if a drag has been + // detected. if (mIsDragEvent) { return mEventHandler.handleMotionEvent(ev); } else { @@ -94,10 +103,6 @@ class DragDetector { mTouchSlop = touchSlop; } - boolean isDragEvent() { - return mIsDragEvent; - } - private void resetState() { mIsDragEvent = false; mInputDownPoint.set(0, 0); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS new file mode 100644 index 000000000000..4417209b85ed --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS @@ -0,0 +1 @@ +jorgegil@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index a3d364a0068e..0bce3acecb3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,7 @@ class TaskPositioner implements DragPositioningCallback { private final DisplayController mDisplayController; private final WindowDecoration mWindowDecoration; + private final Rect mTempBounds = new Rect(); private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); @@ -117,17 +118,32 @@ class TaskPositioner implements DragPositioningCallback { final float deltaX = x - mRepositionStartPoint.x; final float deltaY = y - mRepositionStartPoint.y; mRepositionTaskBounds.set(mTaskBoundsAtDragStart); + + final Rect stableBounds = mTempBounds; + // Make sure the new resizing destination in any direction falls within the stable bounds. + // If not, set the bounds back to the old location that was valid to avoid conflicts with + // some regions such as the gesture area. + mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) + .getStableBounds(stableBounds); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { - mRepositionTaskBounds.left += deltaX; + final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX; + mRepositionTaskBounds.left = (candidateLeft > stableBounds.left) + ? candidateLeft : oldLeft; } if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { - mRepositionTaskBounds.right += deltaX; + final int candidateRight = mRepositionTaskBounds.right + (int) deltaX; + mRepositionTaskBounds.right = (candidateRight < stableBounds.right) + ? candidateRight : oldRight; } if ((mCtrlType & CTRL_TYPE_TOP) != 0) { - mRepositionTaskBounds.top += deltaY; + final int candidateTop = mRepositionTaskBounds.top + (int) deltaY; + mRepositionTaskBounds.top = (candidateTop > stableBounds.top) + ? candidateTop : oldTop; } if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { - mRepositionTaskBounds.bottom += deltaY; + final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY; + mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) + ? candidateBottom : oldBottom; } if (mCtrlType == CTRL_TYPE_UNDEFINED) { mRepositionTaskBounds.offset((int) deltaX, (int) deltaY); 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 7a7ac476879e..ddd3b440e1a6 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 @@ -131,7 +131,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); - mDecorWindowContext = mContext.createConfigurationContext(mTaskInfo.getConfiguration()); + mDecorWindowContext = mContext.createConfigurationContext( + getConfigurationWithOverrides(mTaskInfo)); + } + + /** + * Get {@link Configuration} from supplied {@link RunningTaskInfo}. + * + * Allows values to be overridden before returning the configuration. + */ + protected Configuration getConfigurationWithOverrides(RunningTaskInfo taskInfo) { + return taskInfo.getConfiguration(); } /** @@ -165,7 +175,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = rootView; rootView = null; // Clear it just in case we use it accidentally - final Configuration taskConfig = mTaskInfo.getConfiguration(); + final Configuration taskConfig = getConfigurationWithOverrides(mTaskInfo); if (oldTaskConfig.densityDpi != taskConfig.densityDpi || mDisplay == null || mDisplay.getDisplayId() != mTaskInfo.displayId) { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt new file mode 100644 index 000000000000..53ce3936fbe4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt @@ -0,0 +1,111 @@ +/* + * 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.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.graphics.Rect +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test the snapping of a PIP window via dragging, releasing, and checking its final location. + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipDragThenSnapTest(flicker: FlickerTest) : PipTransition(flicker){ + // represents the direction in which the pip window should be snapping + private var willSnapRight: Boolean = true + + override val transition: FlickerBuilder.() -> Unit + get() = { + val stringExtras: Map<String, String> = + mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + + // cache the starting bounds here + val startBounds = Rect() + + setup { + // Launch the PIP activity and wait for it to enter PiP mode + setRotation(Rotation.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + + // get the initial region bounds and cache them + val initRegion = pipApp.getWindowRect(wmHelper) + startBounds + .set(initRegion.left, initRegion.top, initRegion.right, initRegion.bottom) + + // drag the pip window away from the edge + pipApp.dragPipWindowAwayFromEdge(wmHelper, 50) + + // determine the direction in which the snapping should occur + willSnapRight = pipApp.isCloserToRightEdge(wmHelper) + } + transitions { + // continue the transition until the PIP snaps + pipApp.waitForPipToSnapTo(wmHelper, startBounds) + } + } + + /** + * Checks that the visible region area of [pipApp] moves to closest edge during the animation. + */ + @Postsubmit + @Test + fun pipLayerMovesToClosestEdge() { + flicker.assertLayers { + val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + if (willSnapRight) { + current.visibleRegion.isToTheRight(previous.visibleRegion.region) + } else { + previous.visibleRegion.isToTheRight(current.visibleRegion.region) + } + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java index 35c374ddd974..26b787fa836c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java @@ -30,6 +30,7 @@ import android.window.TransitionInfo; */ public class TransitionInfoBuilder { final TransitionInfo mInfo; + static final int DISPLAY_ID = 0; public TransitionInfoBuilder(@WindowManager.TransitionType int type) { this(type, 0 /* flags */); @@ -38,7 +39,7 @@ public class TransitionInfoBuilder { public TransitionInfoBuilder(@WindowManager.TransitionType int type, @WindowManager.TransitionFlags int flags) { mInfo = new TransitionInfo(type, flags); - mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); + mInfo.addRootLeash(DISPLAY_ID, createMockSurface(true /* valid */), 0, 0); } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, @@ -61,6 +62,7 @@ public class TransitionInfoBuilder { } public TransitionInfoBuilder addChange(TransitionInfo.Change change) { + change.setDisplayId(DISPLAY_ID, DISPLAY_ID); mInfo.addChange(change); return this; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index ec264a643785..addc2338144f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -55,25 +55,28 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { private static final float MAX_ASPECT_RATIO = 2f; private static final int DEFAULT_MIN_EDGE_SIZE = 100; + /** The minimum possible size of the override min size's width or height */ + private static final int OVERRIDABLE_MIN_SIZE = 40; + private PipBoundsAlgorithm mPipBoundsAlgorithm; private DisplayInfo mDefaultDisplayInfo; - private PipBoundsState mPipBoundsState; - private PipSizeSpecHandler mPipSizeSpecHandler; + private PipBoundsState mPipBoundsState; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; @Before public void setUp() throws Exception { initializeMockResources(); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler); DisplayLayout layout = new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true); - mPipBoundsState.setDisplayLayout(layout); - mPipSizeSpecHandler.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayLayout(layout); } private void initializeMockResources() { @@ -88,6 +91,9 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { R.dimen.default_minimal_size_pip_resizable_task, DEFAULT_MIN_EDGE_SIZE); res.addOverride( + R.dimen.overridable_minimal_size_pip_resizable_task, + OVERRIDABLE_MIN_SIZE); + res.addOverride( R.string.config_defaultPictureInPictureScreenEdgeInsets, "16x16"); res.addOverride( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java index 341a451eeb43..f32000445ca9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -27,11 +27,13 @@ import android.content.ComponentName; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.testing.TestableResources; import android.util.Size; import androidx.test.filters.SmallTest; import com.android.internal.util.function.TriConsumer; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.pip.phone.PipSizeSpecHandler; @@ -52,13 +54,23 @@ public class PipBoundsStateTest extends ShellTestCase { private static final Size DEFAULT_SIZE = new Size(10, 10); private static final float DEFAULT_SNAP_FRACTION = 1.0f; + /** The minimum possible size of the override min size's width or height */ + private static final int OVERRIDABLE_MIN_SIZE = 40; + private PipBoundsState mPipBoundsState; private ComponentName mTestComponentName1; private ComponentName mTestComponentName2; @Before public void setUp() { - mPipBoundsState = new PipBoundsState(mContext, new PipSizeSpecHandler(mContext)); + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride( + R.dimen.overridable_minimal_size_pip_resizable_task, + OVERRIDABLE_MIN_SIZE); + + PipDisplayLayoutState pipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipBoundsState = new PipBoundsState(mContext, + new PipSizeSpecHandler(mContext, pipDisplayLayoutState), pipDisplayLayoutState); mTestComponentName1 = new ComponentName(mContext, "component1"); mTestComponentName2 = new ComponentName(mContext, "component2"); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index e907cd3ca0ad..15bb10ed4f2b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -88,6 +88,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { private PipTransitionState mPipTransitionState; private PipBoundsAlgorithm mPipBoundsAlgorithm; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; private ComponentName mComponent1; private ComponentName mComponent2; @@ -97,15 +98,16 @@ public class PipTaskOrganizerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mComponent1 = new ComponentName(mContext, "component1"); mComponent2 = new ComponentName(mContext, "component2"); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler); mMainExecutor = new TestShellExecutor(); mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue, - mPipTransitionState, mPipBoundsState, mPipSizeSpecHandler, + mPipTransitionState, mPipBoundsState, mPipDisplayLayoutState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, @@ -259,8 +261,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { final DisplayInfo info = new DisplayInfo(); DisplayLayout layout = new DisplayLayout(info, mContext.getResources(), true, true); - mPipBoundsState.setDisplayLayout(layout); - mPipSizeSpecHandler.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayLayout(layout); mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA); mPipTaskOrganizer.setSurfaceControlTransactionFactory( MockSurfaceControlHelper::createMockSurfaceControlTransaction); 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 4a68287a4486..108e273d75a5 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 @@ -53,12 +53,14 @@ 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.ShellExecutor; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -108,11 +110,13 @@ public class PipControllerTest extends ShellTestCase { @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper; @Mock private PipBoundsState mMockPipBoundsState; @Mock private PipSizeSpecHandler mMockPipSizeSpecHandler; + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; @Mock private TaskStackListenerImpl mMockTaskStackListener; @Mock private ShellExecutor mMockExecutor; @Mock private Optional<OneHandedController> mMockOneHandedController; @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; @Mock private DisplayInsetsController mMockDisplayInsetsController; + @Mock private TabletopModeController mMockTabletopModeController; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -130,11 +134,12 @@ public class PipControllerTest extends ShellTestCase { mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, mShellController, mMockDisplayController, mMockPipAnimationController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, - mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper, - mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, - mMockWindowManagerShellWrapper, mMockTaskStackListener, - mMockPipParamsChangedForwarder, mMockDisplayInsetsController, + mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState, + mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, + mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, + mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockTabletopModeController, mMockOneHandedController, mMockExecutor); mShellInit.init(); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); @@ -221,11 +226,12 @@ public class PipControllerTest extends ShellTestCase { assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler, mShellController, mMockDisplayController, mMockPipAnimationController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, - mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper, - mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, - mMockWindowManagerShellWrapper, mMockTaskStackListener, - mMockPipParamsChangedForwarder, mMockDisplayInsetsController, + mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState, + mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, + mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, + mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockTabletopModeController, mMockOneHandedController, mMockExecutor)); } @@ -283,8 +289,8 @@ public class PipControllerTest extends ShellTestCase { when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(1, 1)); when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_VALUE, MAX_VALUE)); when(mMockPipBoundsState.getBounds()).thenReturn(bounds); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); - when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); when(mMockPipTaskOrganizer.isInPip()).thenReturn(true); @@ -299,8 +305,8 @@ public class PipControllerTest extends ShellTestCase { final int displayId = 1; final Rect bounds = new Rect(0, 0, 10, 10); when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); - when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); when(mMockPipTaskOrganizer.isInPip()).thenReturn(false); @@ -314,7 +320,7 @@ public class PipControllerTest extends ShellTestCase { public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() { final int displayId = 1; final Rect keepClearArea = new Rect(0, 0, 10, 10); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( displayId, Set.of(keepClearArea), Set.of()); @@ -327,7 +333,7 @@ public class PipControllerTest extends ShellTestCase { mPipController.setEnablePipKeepClearAlgorithm(true); final int displayId = 1; final Rect keepClearArea = new Rect(0, 0, 10, 10); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( displayId, Set.of(keepClearArea), Set.of()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index c7b9eb3d1074..5b62a940c074 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -37,6 +37,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -87,11 +88,14 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm(); final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm = new PipKeepClearAlgorithmInterface() {}; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java index d9ff7d1f1089..390c830069eb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java @@ -33,6 +33,7 @@ import android.view.DisplayInfo; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.PipDisplayLayoutState; import org.junit.After; import org.junit.Assert; @@ -74,6 +75,7 @@ public class PipSizeSpecHandlerTest extends ShellTestCase { @Mock private Context mContext; @Mock private Resources mResources; + private PipDisplayLayoutState mPipDisplayLayoutState; private PipSizeSpecHandler mPipSizeSpecHandler; /** @@ -137,7 +139,6 @@ public class PipSizeSpecHandlerTest extends ShellTestCase { @Before public void setUp() { initExpectedSizes(); - setUpStaticSystemPropertiesSession(); when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE); when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO); @@ -148,11 +149,6 @@ public class PipSizeSpecHandlerTest extends ShellTestCase { // set up the mock context for spec handler specifically when(mContext.getResources()).thenReturn(mResources); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - - // no overridden min edge size by default - mPipSizeSpecHandler.setOverrideMinSize(null); - DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = DISPLAY_EDGE_SIZE; displayInfo.logicalHeight = DISPLAY_EDGE_SIZE; @@ -161,7 +157,14 @@ public class PipSizeSpecHandlerTest extends ShellTestCase { // this is done to avoid unnecessary mocking while allowing for custom display dimensions DisplayLayout displayLayout = new DisplayLayout(displayInfo, getContext().getResources(), false, false); - mPipSizeSpecHandler.setDisplayLayout(displayLayout); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipDisplayLayoutState.setDisplayLayout(displayLayout); + + setUpStaticSystemPropertiesSession(); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + + // no overridden min edge size by default + mPipSizeSpecHandler.setOverrideMinSize(null); } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 5c4863ff752f..d36060fd165f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -35,6 +35,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -92,6 +93,7 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipMotionHelper mMotionHelper; private PipResizeGestureHandler mPipResizeGestureHandler; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; private DisplayLayout mDisplayLayout; private Rect mInsetBounds; @@ -105,8 +107,9 @@ public class PipTouchHandlerTest extends ShellTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler); @@ -124,8 +127,7 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipTouchHandler.setPipResizeGestureHandler(mPipResizeGestureHandler); mDisplayLayout = new DisplayLayout(mContext, mContext.getDisplay()); - mPipBoundsState.setDisplayLayout(mDisplayLayout); - mPipSizeSpecHandler.setDisplayLayout(mDisplayLayout); + mPipDisplayLayoutState.setDisplayLayout(mDisplayLayout); mInsetBounds = new Rect(mPipBoundsState.getDisplayBounds().left + INSET, mPipBoundsState.getDisplayBounds().top + INSET, mPipBoundsState.getDisplayBounds().right - INSET, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java index 30096cb99a64..f9b772345b14 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertEquals; import android.view.Gravity; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.phone.PipSizeSpecHandler; @@ -47,6 +48,7 @@ public class TvPipGravityTest extends ShellTestCase { private TvPipBoundsState mTvPipBoundsState; private TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; @Before public void setUp() { @@ -54,8 +56,10 @@ public class TvPipGravityTest extends ShellTestCase { return; } MockitoAnnotations.initMocks(this); - mPipSizeSpecHandler = new PipSizeSpecHandler(mContext); - mTvPipBoundsState = new TvPipBoundsState(mContext, mPipSizeSpecHandler); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mTvPipBoundsState = new TvPipBoundsState(mContext, mPipSizeSpecHandler, + mPipDisplayLayoutState); mTvPipBoundsAlgorithm = new TvPipBoundsAlgorithm(mContext, mTvPipBoundsState, mMockPipSnapAlgorithm, mPipSizeSpecHandler); 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 3901dabcaec8..df78d92a90c8 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 @@ -35,6 +35,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_P import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -249,7 +250,7 @@ public class SplitTransitionTests extends ShellTestCase { @Test @UiThreadTest - public void testEnterRecents() { + public void testEnterRecentsAndCommit() { enterSplit(); ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() @@ -258,27 +259,65 @@ public class SplitTransitionTests extends ShellTestCase { .build(); // Create a request to bring home forward - TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, null); + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, + mock(RemoteTransition.class)); IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - - assertTrue(result.isEmpty()); + // Don't handle recents opening + assertNull(result); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); - // simulate the transition - TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, 0) - .addChange(TRANSIT_TO_FRONT, homeTask) - .addChange(TRANSIT_TO_BACK, mMainChild) - .addChange(TRANSIT_TO_BACK, mSideChild) + // simulate the start of recents transition + mMainStage.onTaskVanished(mMainChild); + mSideStage.onTaskVanished(mSideChild); + mStageCoordinator.onRecentsInSplitAnimationStart(mock(SurfaceControl.Transaction.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + + // Make sure it cleans-up if recents doesn't restore + WindowContainerTransaction commitWCT = new WindowContainerTransaction(); + mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mock(SurfaceControl.Transaction.class)); + assertFalse(mStageCoordinator.isSplitScreenVisible()); + } + + @Test + @UiThreadTest + public void testEnterRecentsAndRestore() { + enterSplit(); + + ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setActivityType(ACTIVITY_TYPE_HOME) .build(); + + // Create a request to bring home forward + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, + mock(RemoteTransition.class)); + IBinder transition = mock(IBinder.class); + WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); + // Don't handle recents opening + assertNull(result); + + // make sure we haven't made any local changes yet (need to wait until transition is ready) + assertTrue(mStageCoordinator.isSplitScreenVisible()); + + // simulate the start of recents transition mMainStage.onTaskVanished(mMainChild); mSideStage.onTaskVanished(mSideChild); - mStageCoordinator.startAnimation(transition, info, - mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class), - mock(Transitions.TransitionFinishCallback.class)); + mStageCoordinator.onRecentsInSplitAnimationStart(mock(SurfaceControl.Transaction.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + + // Make sure we remain in split after recents restores. + WindowContainerTransaction restoreWCT = new WindowContainerTransaction(); + restoreWCT.reorder(mMainChild.token, true /* toTop */); + restoreWCT.reorder(mSideChild.token, true /* toTop */); + // simulate the restoreWCT being applied: + mMainStage.onTaskAppeared(mMainChild, mock(SurfaceControl.class)); + mSideStage.onTaskAppeared(mSideChild, mock(SurfaceControl.class)); + mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mock(SurfaceControl.Transaction.class)); assertTrue(mStageCoordinator.isSplitScreenVisible()); } 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 e63bbeb05575..44f1f0147b52 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 @@ -62,6 +62,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.util.ArraySet; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; @@ -99,6 +100,7 @@ import org.junit.runner.RunWith; import org.mockito.InOrder; import java.util.ArrayList; +import java.util.function.Function; /** * Tests for the shell transitions. @@ -591,6 +593,68 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testInterleavedMerging() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + Function<Boolean, IBinder> startATransition = (doMerge) -> { + IBinder token = new Binder(); + if (doMerge) { + mDefaultHandler.setShouldMerge(token); + } + transitions.requestStartTransition(token, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(token, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + return token; + }; + + IBinder transitToken1 = startATransition.apply(false); + // merge first one + IBinder transitToken2 = startATransition.apply(true); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // don't merge next one + IBinder transitToken3 = startATransition.apply(false); + // make sure nothing happened (since it wasn't merged) + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // make a mergable + IBinder transitToken4 = startATransition.apply(true); + // make sure nothing happened since there is a non-mergable pending. + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // Queue up another mergable + IBinder transitToken5 = startATransition.apply(true); + + // Queue up a non-mergable + IBinder transitToken6 = startATransition.apply(false); + + // Our active now looks like: [playing, merged] + // and ready queue: [non-mergable, mergable, mergable, non-mergable] + // finish the playing one + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + // Now we should have the non-mergable playing now with 2 merged: + // active: [playing, merged, merged] queue: [non-mergable] + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(2, mDefaultHandler.mergeCount()); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, mDefaultHandler.mergeCount()); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + } + + @Test public void testTransitionOrderMatchesCore() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -1016,6 +1080,7 @@ public class ShellTransitionTests extends ShellTestCase { ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>(); final ArrayList<IBinder> mMerged = new ArrayList<>(); boolean mSimulateMerge = false; + final ArraySet<IBinder> mShouldMerge = new ArraySet<>(); @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -1030,7 +1095,7 @@ public class ShellTransitionTests extends ShellTestCase { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!mSimulateMerge) return; + if (!(mSimulateMerge || mShouldMerge.contains(transition))) return; mMerged.add(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); } @@ -1046,12 +1111,23 @@ public class ShellTransitionTests extends ShellTestCase { mSimulateMerge = sim; } + void setShouldMerge(IBinder toMerge) { + mShouldMerge.add(toMerge); + } + void finishAll() { final ArrayList<Transitions.TransitionFinishCallback> finishes = mFinishes; mFinishes = new ArrayList<>(); for (int i = finishes.size() - 1; i >= 0; --i) { finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); } + mShouldMerge.clear(); + } + + void finishOne() { + Transitions.TransitionFinishCallback fin = mFinishes.remove(0); + mMerged.clear(); + fin.onTransitionFinished(null /* wct */, null /* wctCB */); } int activeCount() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt index 8f66f4e7e47b..94c064bda763 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -5,13 +5,16 @@ import android.app.WindowConfiguration import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner +import android.view.Display 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.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED @@ -19,10 +22,11 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations /** @@ -51,6 +55,8 @@ class TaskPositionerTest : ShellTestCase() { private lateinit var mockDisplayController: DisplayController @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display private lateinit var taskPositioner: TaskPositioner @@ -68,6 +74,9 @@ class TaskPositionerTest : ShellTestCase() { `when`(taskToken.asBinder()).thenReturn(taskBinder) `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -78,6 +87,8 @@ class TaskPositionerTest : ShellTestCase() { displayId = DISPLAY_ID configuration.windowConfiguration.bounds = STARTING_BOUNDS } + mockWindowDecoration.mDisplay = mockDisplay + `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @Test @@ -451,6 +462,72 @@ class TaskPositionerTest : ShellTestCase() { }) } + fun testDragResize_toDisallowedBounds_freezesAtLimit() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + + // Resize the task by 10px to the right and bottom, a valid destination + val newBounds = Rect( + STARTING_BOUNDS.left, + STARTING_BOUNDS.top, + STARTING_BOUNDS.right + 10, + STARTING_BOUNDS.bottom + 10) + taskPositioner.onDragPositioningMove( + newBounds.right.toFloat(), + newBounds.bottom.toFloat() + ) + + // Resize the task by another 10px to the right (allowed) and to just in the disallowed + // area of the Y coordinate. + val newBounds2 = Rect( + newBounds.left, + newBounds.top, + newBounds.right + 10, + DISALLOWED_RESIZE_AREA.top + ) + taskPositioner.onDragPositioningMove( + newBounds2.right.toFloat(), + newBounds2.bottom.toFloat() + ) + + taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) + + // The first resize falls in the allowed area, verify there's a change for it. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds) + } + }) + // The second resize falls in the disallowed area, verify there's no change for it. + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds2) + } + }) + // Instead, there should be a change for its allowed portion (the X movement) with the Y + // staying frozen in the last valid resize position. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds( + Rect( + newBounds2.left, + newBounds2.top, + newBounds2.right, + newBounds.bottom // Stayed at the first resize destination. + ) + ) + } + }) + } + + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { + return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && + bounds == configuration.windowConfiguration.bounds + } + companion object { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 @@ -458,6 +535,19 @@ class TaskPositionerTest : ShellTestCase() { private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val DISALLOWED_RESIZE_AREA = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) } } diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index b0896daee2a1..9df6822b4867 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -91,6 +91,8 @@ bool Properties::isHighEndGfx = true; bool Properties::isLowRam = false; bool Properties::isSystemOrPersistent = false; +float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number + StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized; @@ -150,6 +152,11 @@ bool Properties::load() { enableWebViewOverlays = base::GetBoolProperty(PROPERTY_WEBVIEW_OVERLAYS_ENABLED, true); + auto hdrHeadroom = (float)atof(base::GetProperty(PROPERTY_8BIT_HDR_HEADROOM, "").c_str()); + if (hdrHeadroom >= 1.f) { + maxHdrHeadroomOn8bit = std::min(hdrHeadroom, 100.f); + } + // call isDrawingEnabled to force loading of the property isDrawingEnabled(); diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ed7175e140e4..24e206bbc3b1 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -218,6 +218,8 @@ enum DebugLevel { #define PROPERTY_MEMORY_POLICY "debug.hwui.app_memory_policy" +#define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -321,6 +323,8 @@ public: static bool isLowRam; static bool isSystemOrPersistent; + static float maxHdrHeadroomOn8bit; + static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; } diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp index 8977d3ce4da3..bfe4eaf39e21 100644 --- a/libs/hwui/effects/GainmapRenderer.cpp +++ b/libs/hwui/effects/GainmapRenderer.cpp @@ -23,21 +23,55 @@ #include "utils/Trace.h" #ifdef __ANDROID__ +#include "include/core/SkColorSpace.h" +#include "include/core/SkImage.h" +#include "include/core/SkShader.h" +#include "include/effects/SkRuntimeEffect.h" +#include "include/private/SkGainmapInfo.h" #include "renderthread/CanvasContext.h" +#include "src/core/SkColorFilterPriv.h" +#include "src/core/SkImageInfoPriv.h" +#include "src/core/SkRuntimeEffectPriv.h" #endif namespace android::uirenderer { using namespace renderthread; +static float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) { + // We should always have a known destination colorspace. If we don't we must be in some + // legacy mode where we're lost and also definitely not going to HDR + if (destColorspace == nullptr) { + return 1.f; + } + + constexpr float GenericSdrWhiteNits = 203.f; + constexpr float maxPQLux = 10000.f; + constexpr float maxHLGLux = 1000.f; + skcms_TransferFunction destTF; + destColorspace->transferFn(&destTF); + if (skcms_TransferFunction_isPQish(&destTF)) { + return maxPQLux / GenericSdrWhiteNits; + } else if (skcms_TransferFunction_isHLGish(&destTF)) { + return maxHLGLux / GenericSdrWhiteNits; + } else { +#ifdef __ANDROID__ + CanvasContext* context = CanvasContext::getActiveContext(); + return context ? context->targetSdrHdrRatio() : 1.f; +#else + return 1.f; +#endif + } +} + void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, SkCanvas::SrcRectConstraint constraint, const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) { ATRACE_CALL(); #ifdef __ANDROID__ - CanvasContext* context = CanvasContext::getActiveContext(); - float targetSdrHdrRatio = context ? context->targetSdrHdrRatio() : 1.f; + auto destColorspace = c->imageInfo().refColorSpace(); + float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get()); if (targetSdrHdrRatio > 1.f && gainmapImage) { SkPaint gainmapPaint = *paint; float sX = gainmapImage->width() / (float)image->width(); @@ -48,9 +82,9 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR gainmapSrc.fRight *= sX; gainmapSrc.fTop *= sY; gainmapSrc.fBottom *= sY; - auto shader = SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, - sampling, gainmapInfo, dst, targetSdrHdrRatio, - c->imageInfo().refColorSpace()); + auto shader = + SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling, + gainmapInfo, dst, targetSdrHdrRatio, destColorspace); gainmapPaint.setShader(shader); c->drawRect(dst, gainmapPaint); } else @@ -58,4 +92,213 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR c->drawImageRect(image.get(), src, dst, sampling, paint, constraint); } +#ifdef __ANDROID__ + +static constexpr char gGainmapSKSL[] = R"SKSL( + uniform shader base; + uniform shader gainmap; + uniform colorFilter workingSpaceToLinearSrgb; + uniform half4 logRatioMin; + uniform half4 logRatioMax; + uniform half4 gainmapGamma; + uniform half4 epsilonSdr; + uniform half4 epsilonHdr; + uniform half W; + uniform int gainmapIsAlpha; + uniform int gainmapIsRed; + uniform int singleChannel; + uniform int noGamma; + + half4 toDest(half4 working) { + half4 ls = workingSpaceToLinearSrgb.eval(working); + vec3 dest = fromLinearSrgb(ls.rgb); + return half4(dest.r, dest.g, dest.b, ls.a); + } + + half4 main(float2 coord) { + half4 S = base.eval(coord); + half4 G = gainmap.eval(coord); + if (gainmapIsAlpha == 1) { + G = half4(G.a, G.a, G.a, 1.0); + } + if (gainmapIsRed == 1) { + G = half4(G.r, G.r, G.r, 1.0); + } + if (singleChannel == 1) { + half L; + if (noGamma == 1) { + L = mix(logRatioMin.r, logRatioMax.r, G.r); + } else { + L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r)); + } + half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb; + return toDest(half4(H.r, H.g, H.b, S.a)); + } else { + half3 L; + if (noGamma == 1) { + L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb); + } else { + L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb)); + } + half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb; + return toDest(half4(H.r, H.g, H.b, S.a)); + } + } +)SKSL"; + +static sk_sp<SkRuntimeEffect> gainmap_apply_effect() { + static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* { + auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {}); + if (buildResult.effect) { + return buildResult.effect.release(); + } else { + LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str()); + } + }(); + SkASSERT(effect); + return sk_ref_sp(effect); +} + +static bool all_channels_equal(const SkColor4f& c) { + return c.fR == c.fG && c.fR == c.fB; +} + +class DeferredGainmapShader { +private: + sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()}; + SkRuntimeShaderBuilder mBuilder{mShader}; + SkGainmapInfo mGainmapInfo; + std::mutex mUniformGuard; + + void setupChildren(const sk_sp<const SkImage>& baseImage, + const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) { + sk_sp<SkColorSpace> baseColorSpace = + baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB(); + + // Determine the color space in which the gainmap math is to be applied. + sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma(); + + // Create a color filter to transform from the base image's color space to the color space + // in which the gainmap is to be applied. + auto colorXformSdrToGainmap = + 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); + + // The gainmap image shader will ignore any color space that the gainmap has. + const SkMatrix gainmapRectToDstRect = + SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()), + SkRect::MakeWH(baseImage->width(), baseImage->height())); + auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions, + &gainmapRectToDstRect); + + // Create a color filter to transform from the color space in which the gainmap is applied + // to the intermediate destination color space. + auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform( + gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear()); + + mBuilder.child("base") = std::move(baseImageShader); + mBuilder.child("gainmap") = std::move(gainmapImageShader); + mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); + } + + 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 int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f && + gainmapInfo.fGainmapGamma.fG == 1.f && + gainmapInfo.fGainmapGamma.fB == 1.f; + const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType()); + const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag; + const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag; + const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) && + all_channels_equal(gainmapInfo.fGainmapRatioMin) && + all_channels_equal(gainmapInfo.fGainmapRatioMax) && + (colorTypeFlags == kGray_SkColorChannelFlag || + colorTypeFlags == kAlpha_SkColorChannelFlag || + colorTypeFlags == kRed_SkColorChannelFlag); + mBuilder.uniform("logRatioMin") = logRatioMin; + mBuilder.uniform("logRatioMax") = logRatioMax; + mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma; + mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr; + mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr; + mBuilder.uniform("noGamma") = noGamma; + mBuilder.uniform("singleChannel") = singleChannel; + mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha; + mBuilder.uniform("gainmapIsRed") = gainmapIsRed; + } + + sk_sp<const SkData> build(float targetHdrSdrRatio) { + sk_sp<const SkData> uniforms; + { + // If we are called concurrently from multiple threads, we need to guard the call + // to writableUniforms() which mutates mUniform. This is otherwise safe because + // writeableUniforms() will make a copy if it's not unique before mutating + // This can happen if a BitmapShader is used on multiple canvas', such as a + // software + hardware canvas, which is otherwise valid as SkShader is "immutable" + std::lock_guard _lock(mUniformGuard); + const float Wunclamped = (sk_float_log(targetHdrSdrRatio) - + sk_float_log(mGainmapInfo.fDisplayRatioSdr)) / + (sk_float_log(mGainmapInfo.fDisplayRatioHdr) - + sk_float_log(mGainmapInfo.fDisplayRatioSdr)); + const float W = std::max(std::min(Wunclamped, 1.f), 0.f); + mBuilder.uniform("W") = W; + uniforms = mBuilder.uniforms(); + } + return uniforms; + } + +public: + explicit DeferredGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + mGainmapInfo = gainmapInfo; + setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling); + setupGenericUniforms(gainmapImage, gainmapInfo); + } + + static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + auto deferredHandler = std::make_shared<DeferredGainmapShader>( + image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling); + auto callback = + [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext) + -> sk_sp<const SkData> { + return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace)); + }; + return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback, + deferredHandler->mBuilder.children()); + } +}; + +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY, + sampling); +} + +#else // __ANDROID__ + +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + return nullptr; +} + +#endif // __ANDROID__ + } // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/effects/GainmapRenderer.h b/libs/hwui/effects/GainmapRenderer.h index 7c56d94d9776..4ed2445da17e 100644 --- a/libs/hwui/effects/GainmapRenderer.h +++ b/libs/hwui/effects/GainmapRenderer.h @@ -30,4 +30,9 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR SkCanvas::SrcRectConstraint constraint, const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo); +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling); + } // namespace android::uirenderer diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 8266beb4ee8c..9a06be006dca 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -498,7 +498,7 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { return result; } -SkCodec::Result ImageDecoder::extractGainmap(Bitmap* destination) { +SkCodec::Result ImageDecoder::extractGainmap(Bitmap* destination, bool isShared) { ATRACE_CALL(); SkGainmapInfo gainmapInfo; std::unique_ptr<SkStream> gainmapStream; @@ -553,9 +553,12 @@ SkCodec::Result ImageDecoder::extractGainmap(Bitmap* destination) { return SkCodec::kInternalError; } - // TODO: We don't currently parcel the gainmap, but if we should then also support - // the shared allocator - sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bm); + sk_sp<Bitmap> nativeBitmap; + if (isShared) { + nativeBitmap = Bitmap::allocateAshmemBitmap(&bm); + } else { + nativeBitmap = Bitmap::allocateHeapBitmap(&bm); + } if (!nativeBitmap) { ALOGE("OOM allocating Bitmap with dimensions %i x %i", bitmapInfo.width(), bitmapInfo.height()); diff --git a/libs/hwui/hwui/ImageDecoder.h b/libs/hwui/hwui/ImageDecoder.h index 97573e1e8207..b3781b52a418 100644 --- a/libs/hwui/hwui/ImageDecoder.h +++ b/libs/hwui/hwui/ImageDecoder.h @@ -79,7 +79,7 @@ public: // Set whether the ImageDecoder should handle RestorePrevious frames. void setHandleRestorePrevious(bool handle); - SkCodec::Result extractGainmap(Bitmap* destination); + SkCodec::Result extractGainmap(Bitmap* destination, bool isShared); private: // State machine for keeping track of how to handle RestorePrevious (RP) diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index ad80460da5a9..db1c188e425e 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -354,7 +354,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong // cost of RAM if (result == SkCodec::kSuccess && !jpostProcess && !preferRamOverQuality) { // The gainmap costs RAM to improve quality, so skip this if we're prioritizing RAM instead - result = decoder->extractGainmap(nativeBitmap.get()); + result = decoder->extractGainmap(nativeBitmap.get(), + allocator == kSharedMemory_Allocator ? true : false); jexception = get_and_clear_exception(env); } diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 75d45e5bd8aa..7eb79be6f55b 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -1,6 +1,9 @@ #undef LOG_TAG #define LOG_TAG "ShaderJNI" +#include <vector> + +#include "Gainmap.h" #include "GraphicsJNI.h" #include "SkBitmap.h" #include "SkBlendMode.h" @@ -17,10 +20,9 @@ #include "SkShader.h" #include "SkString.h" #include "SkTileMode.h" +#include "effects/GainmapRenderer.h" #include "include/effects/SkRuntimeEffect.h" -#include <vector> - using namespace android::uirenderer; /** @@ -74,7 +76,20 @@ static jlong createBitmapShaderHelper(JNIEnv* env, jobject o, jlong matrixPtr, j if (bitmapHandle) { // Only pass a valid SkBitmap object to the constructor if the Bitmap exists. Otherwise, // we'll pass an empty SkBitmap to avoid crashing/excepting for compatibility. - image = android::bitmap::toBitmap(bitmapHandle).makeImage(); + auto& bitmap = android::bitmap::toBitmap(bitmapHandle); + image = bitmap.makeImage(); + + if (!isDirectSampled && bitmap.hasGainmap()) { + sk_sp<SkShader> gainmapShader = MakeGainmapShader( + image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info, + (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); + if (gainmapShader) { + if (matrix) { + gainmapShader = gainmapShader->makeWithLocalMatrix(*matrix); + } + return reinterpret_cast<jlong>(gainmapShader.release()); + } + } } if (!image.get()) { diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index f10b2b2f0694..dd781bb85470 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -311,7 +311,7 @@ float CanvasContext::setColorMode(ColorMode mode) { } switch (mColorMode) { case ColorMode::Hdr: - return 3.f; // TODO: Refine this number + return Properties::maxHdrHeadroomOn8bit; case ColorMode::Hdr10: return 10.f; default: diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 7a7f1abdd268..0afd949cf5c9 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -135,7 +135,9 @@ void RenderThread::frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t !mFrameCallbackTaskPending) { ATRACE_NAME("queue mFrameCallbackTask"); mFrameCallbackTaskPending = true; - nsecs_t runAt = (frameTimeNanos + mDispatchFrameDelay); + + nsecs_t timeUntilDeadline = frameDeadline - frameTimeNanos; + nsecs_t runAt = (frameTimeNanos + (timeUntilDeadline * 0.25f)); queue().postAt(runAt, [=]() { dispatchFrameCallbacks(); }); } } @@ -257,7 +259,6 @@ void RenderThread::initThreadLocals() { void RenderThread::setupFrameInterval() { nsecs_t frameIntervalNanos = DeviceInfo::getVsyncPeriod(); mTimeLord.setFrameInterval(frameIntervalNanos); - mDispatchFrameDelay = static_cast<nsecs_t>(frameIntervalNanos * .25f); } void RenderThread::requireGlContext() { diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 0a89e5e944f8..c77cd4134d1e 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -235,7 +235,6 @@ private: bool mFrameCallbackTaskPending; TimeLord mTimeLord; - nsecs_t mDispatchFrameDelay = 4_ms; RenderState* mRenderState; EglManager* mEglManager; WebViewFunctorManager& mFunctorManager; diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index 24cfc9d70c9b..c3984055a278 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -63,24 +63,23 @@ MouseCursorController::~MouseCursorController() { mLocked.pointerSprite.clear(); } -bool MouseCursorController::getBounds(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const { +std::optional<FloatRect> MouseCursorController::getBounds() const { std::scoped_lock lock(mLock); - return getBoundsLocked(outMinX, outMinY, outMaxX, outMaxY); + return getBoundsLocked(); } -bool MouseCursorController::getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const REQUIRES(mLock) { +std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) { if (!mLocked.viewport.isValid()) { - return false; + return {}; } - *outMinX = mLocked.viewport.logicalLeft; - *outMinY = mLocked.viewport.logicalTop; - *outMaxX = mLocked.viewport.logicalRight - 1; - *outMaxY = mLocked.viewport.logicalBottom - 1; - return true; + return FloatRect{ + static_cast<float>(mLocked.viewport.logicalLeft), + static_cast<float>(mLocked.viewport.logicalTop), + static_cast<float>(mLocked.viewport.logicalRight - 1), + static_cast<float>(mLocked.viewport.logicalBottom - 1), + }; } void MouseCursorController::move(float deltaX, float deltaY) { @@ -121,31 +120,19 @@ void MouseCursorController::setPosition(float x, float y) { } void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) { - float minX, minY, maxX, maxY; - if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) { - if (x <= minX) { - mLocked.pointerX = minX; - } else if (x >= maxX) { - mLocked.pointerX = maxX; - } else { - mLocked.pointerX = x; - } - if (y <= minY) { - mLocked.pointerY = minY; - } else if (y >= maxY) { - mLocked.pointerY = maxY; - } else { - mLocked.pointerY = y; - } - updatePointerLocked(); - } + const auto bounds = getBoundsLocked(); + if (!bounds) return; + + mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x)); + mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y)); + + updatePointerLocked(); } -void MouseCursorController::getPosition(float* outX, float* outY) const { +FloatPoint MouseCursorController::getPosition() const { std::scoped_lock lock(mLock); - *outX = mLocked.pointerX; - *outY = mLocked.pointerY; + return {mLocked.pointerX, mLocked.pointerY}; } int32_t MouseCursorController::getDisplayId() const { @@ -235,10 +222,9 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport, // Reset cursor position to center if size or display changed. if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth || oldDisplayHeight != newDisplayHeight) { - float minX, minY, maxX, maxY; - if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) { - mLocked.pointerX = (minX + maxX) * 0.5f; - mLocked.pointerY = (minY + maxY) * 0.5f; + if (const auto bounds = getBoundsLocked(); bounds) { + mLocked.pointerX = (bounds->left + bounds->right) * 0.5f; + mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f; // Reload icon resources for density may be changed. loadResourcesLocked(getAdditionalMouseResources); } else { diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index db0ab56429b2..26be2a858c4e 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -43,12 +43,12 @@ public: MouseCursorController(PointerControllerContext& context); ~MouseCursorController(); - bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; + std::optional<FloatRect> getBounds() const; void move(float deltaX, float deltaY); void setButtonState(int32_t buttonState); int32_t getButtonState() const; void setPosition(float x, float y); - void getPosition(float* outX, float* outY) const; + FloatPoint getPosition() const; int32_t getDisplayId() const; void fade(PointerControllerInterface::Transition transition); void unfade(PointerControllerInterface::Transition transition); @@ -102,7 +102,7 @@ private: } mLocked GUARDED_BY(mLock); - bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; + std::optional<FloatRect> getBoundsLocked() const; void setPositionLocked(float x, float y); void updatePointerLocked(); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index fedf58d7c6d0..544edc2a716f 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -114,16 +114,15 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::~PointerController() { mDisplayInfoListener->onPointerControllerDestroyed(); mUnregisterWindowInfosListener(mDisplayInfoListener); - mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0); + mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0}); } std::mutex& PointerController::getLock() const { return mDisplayInfoListener->mLock; } -bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const { - return mCursorController.getBounds(outMinX, outMinY, outMaxX, outMaxY); +std::optional<FloatRect> PointerController::getBounds() const { + return mCursorController.getBounds(); } void PointerController::move(float deltaX, float deltaY) { @@ -156,15 +155,13 @@ void PointerController::setPosition(float x, float y) { mCursorController.setPosition(transformed.x, transformed.y); } -void PointerController::getPosition(float* outX, float* outY) const { +FloatPoint PointerController::getPosition() const { const int32_t displayId = mCursorController.getDisplayId(); - mCursorController.getPosition(outX, outY); + const auto p = mCursorController.getPosition(); { std::scoped_lock lock(getLock()); const auto& transform = getTransformForDisplayLocked(displayId); - const auto xy = transform.inverse().transform(*outX, *outY); - *outX = xy.x; - *outY = xy.y; + return FloatPoint{transform.inverse().transform(p.x, p.y)}; } } @@ -262,19 +259,31 @@ void PointerController::reloadPointerResources() { } void PointerController::setDisplayViewport(const DisplayViewport& viewport) { - std::scoped_lock lock(getLock()); + struct PointerDisplayChangeArgs { + int32_t displayId; + FloatPoint cursorPosition; + }; + std::optional<PointerDisplayChangeArgs> pointerDisplayChanged; - bool getAdditionalMouseResources = false; - if (mLocked.presentation == PointerController::Presentation::POINTER || - mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) { - getAdditionalMouseResources = true; - } - mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); - if (viewport.displayId != mLocked.pointerDisplayId) { - float xPos, yPos; - mCursorController.getPosition(&xPos, &yPos); - mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos); - mLocked.pointerDisplayId = viewport.displayId; + { // acquire lock + std::scoped_lock lock(getLock()); + + bool getAdditionalMouseResources = false; + if (mLocked.presentation == PointerController::Presentation::POINTER || + mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) { + getAdditionalMouseResources = true; + } + 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); } } diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index 48d5a5756a69..6d3557c89cc7 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -50,12 +50,12 @@ public: ~PointerController() override; - virtual bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; + virtual std::optional<FloatRect> getBounds() const; virtual void move(float deltaX, float deltaY); virtual void setButtonState(int32_t buttonState); virtual int32_t getButtonState() const; virtual void setPosition(float x, float y); - virtual void getPosition(float* outX, float* outY) const; + virtual FloatPoint getPosition() const; virtual int32_t getDisplayId() const; virtual void fade(Transition transition); virtual void unfade(Transition transition); diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index 96d83a5f0d15..f6f5d3bc51bd 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -81,7 +81,7 @@ public: virtual PointerIconStyle getDefaultPointerIconId() = 0; virtual PointerIconStyle getDefaultStylusIconId() = 0; virtual PointerIconStyle getCustomPointerIconId() = 0; - virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0; + virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0; }; /* diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index c820d0007a4b..2378d42793a1 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -60,7 +60,7 @@ public: virtual PointerIconStyle getDefaultPointerIconId() override; virtual PointerIconStyle getDefaultStylusIconId() override; virtual PointerIconStyle getCustomPointerIconId() override; - virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override; + virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); @@ -143,8 +143,8 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic } void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, - float /*xPos*/, - float /*yPos*/) { + const FloatPoint& /*position*/ +) { latestPointerDisplayId = displayId; } |