diff options
Diffstat (limited to 'libs')
31 files changed, 1243 insertions, 279 deletions
diff --git a/libs/WindowManager/Shell/res/color/split_divider_background.xml b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml index 049980803ee3..9473cdd607d6 100644 --- a/libs/WindowManager/Shell/res/color/split_divider_background.xml +++ b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ 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. @@ -14,6 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +<!-- Should be the same as in packages/apps/Launcher3/res/color-night-v31/taskbar_background.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml index b3d260299106..0e165fca4fd3 100644 --- a/libs/WindowManager/Shell/res/color/taskbar_background.xml +++ b/libs/WindowManager/Shell/res/color/taskbar_background.xml @@ -16,5 +16,5 @@ --> <!-- Should be the same as in packages/apps/Launcher3/res/color-v31/taskbar_background.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> + <item android:color="@android:color/system_neutral1_500" android:lStar="95" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-night/colors.xml b/libs/WindowManager/Shell/res/values-night/colors.xml index 83c4d93982f4..5c6bb57a7f1c 100644 --- a/libs/WindowManager/Shell/res/values-night/colors.xml +++ b/libs/WindowManager/Shell/res/values-night/colors.xml @@ -15,6 +15,7 @@ --> <resources> + <color name="docked_divider_handle">#ffffff</color> <!-- Bubbles --> <color name="bubbles_icon_tint">@color/GM2_grey_200</color> <!-- Splash screen--> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 965ab1519df4..6fb70006e67f 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -17,7 +17,8 @@ */ --> <resources> - <color name="docked_divider_handle">#ffffff</color> + <color name="docked_divider_handle">#000000</color> + <color name="split_divider_background">@color/taskbar_background</color> <drawable name="forced_resizable_background">#59000000</drawable> <color name="minimize_dock_shadow_start">#60000000</color> <color name="minimize_dock_shadow_end">#00000000</color> 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 71e15c12b9c0..541c0f04b9b9 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 @@ -61,6 +61,7 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; @@ -125,6 +126,39 @@ public class BubbleController implements ConfigurationChangeListener { private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; + // TODO(b/256873975) Should use proper flag when available to shell/launcher + /** + * Whether bubbles are showing in the bubble bar from launcher. This is only available + * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used + * to check all conditions that indicate if the bubble bar is in use. + */ + 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; @@ -147,12 +181,8 @@ public class BubbleController implements ConfigurationChangeListener { // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBackgroundExecutor; - // Whether or not we should show bubbles pinned at the bottom of the screen. - private boolean mIsBubbleBarEnabled; - private BubbleLogger mLogger; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @@ -533,10 +563,10 @@ public class BubbleController implements ConfigurationChangeListener { mDataRepository.removeBubblesForUser(removedUserId, parentUserId); } - // TODO(b/256873975): Should pass this into the constructor once flags are available to shell. - /** Sets whether the bubble bar is enabled (i.e. bubbles pinned to bottom on large screens). */ - public void setBubbleBarEnabled(boolean enabled) { - mIsBubbleBarEnabled = enabled; + /** Whether bubbles are showing in the bubble bar. */ + public boolean isShowingAsBubbleBar() { + // TODO(b/269670598): should also check that we're in gesture nav + return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen(); } /** Whether this userId belongs to the current user. */ @@ -605,12 +635,6 @@ public class BubbleController implements ConfigurationChangeListener { mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } - if (mIsBubbleBarEnabled && mBubblePositioner.isLargeScreen()) { - mBubblePositioner.setUsePinnedLocation(true); - } else { - mBubblePositioner.setUsePinnedLocation(false); - } - addToWindowManagerMaybe(); } @@ -1284,6 +1308,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() { @@ -1306,7 +1382,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) { @@ -1321,9 +1398,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 @@ -1353,49 +1428,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"); @@ -1406,6 +1479,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)) { @@ -1852,13 +1938,6 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void setBubbleBarEnabled(boolean enabled) { - mMainExecutor.execute(() -> { - BubbleController.this.setBubbleBarEnabled(enabled); - }); - } - - @Override public void onNotificationPanelExpandedChanged(boolean expanded) { mMainExecutor.execute( () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 6230d22ebe12..3fd09675a245 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -283,7 +283,7 @@ public class BubbleData { } boolean isShowingOverflow() { - return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); + return mShowingOverflow && isExpanded(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 07c58527a815..5ea2450114f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -18,9 +18,6 @@ package com.android.wm.shell.bubbles; import static android.view.View.LAYOUT_DIRECTION_RTL; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -39,8 +36,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import java.lang.annotation.Retention; - /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. @@ -50,15 +45,6 @@ public class BubblePositioner { ? "BubblePositioner" : BubbleDebugConfig.TAG_BUBBLES; - @Retention(SOURCE) - @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT, - TASKBAR_POSITION_BOTTOM}) - @interface TaskbarPosition {} - public static final int TASKBAR_POSITION_NONE = -1; - public static final int TASKBAR_POSITION_RIGHT = 0; - public static final int TASKBAR_POSITION_LEFT = 1; - public static final int TASKBAR_POSITION_BOTTOM = 2; - /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; /** Indicates a bubble's height should be the maximum available space. **/ @@ -108,15 +94,9 @@ public class BubblePositioner { private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; - private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mShowingInTaskbar; - private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; - private int mTaskbarIconSize; - private int mTaskbarSize; - public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; @@ -153,27 +133,11 @@ public class BubblePositioner { + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen + " isSmallTablet: " + mIsSmallTablet - + " bounds: " + bounds - + " showingInTaskbar: " + mShowingInTaskbar); + + " bounds: " + bounds); } updateInternal(mRotation, insets, bounds); } - /** - * Updates position information to account for taskbar state. - * - * @param taskbarPosition which position the taskbar is displayed in. - * @param showingInTaskbar whether the taskbar is being shown. - */ - public void updateForTaskbar(int iconSize, - @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) { - mShowingInTaskbar = showingInTaskbar; - mTaskbarIconSize = iconSize; - mTaskbarPosition = taskbarPosition; - mTaskbarSize = taskbarSize; - update(); - } - @VisibleForTesting public void updateInternal(int rotation, Insets insets, Rect bounds) { mRotation = rotation; @@ -232,10 +196,6 @@ public class BubblePositioner { R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); - - if (mShowingInTaskbar) { - adjustForTaskbar(); - } } /** @@ -260,30 +220,6 @@ public class BubblePositioner { return mDefaultMaxBubbles; } - /** - * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should - * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space - * and insets to account for the taskbar. - */ - // TODO(b/171559950): When the insets are reported correctly we can remove this logic - private void adjustForTaskbar() { - // When bar is showing on edges... subtract that inset because we appear on top - if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) { - WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars()); - int newInsetLeft = mInsets.left; - int newInsetRight = mInsets.right; - if (mTaskbarPosition == TASKBAR_POSITION_LEFT) { - mPositionRect.left -= navBarInsets.left; - newInsetLeft -= navBarInsets.left; - } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) { - mPositionRect.right += navBarInsets.right; - newInsetRight -= navBarInsets.right; - } - mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom); - } - } /** * @return a rect of available screen space accounting for orientation, system bars and cutouts. @@ -327,14 +263,12 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mShowingInTaskbar || mIsLargeScreen; + return isLandscape() || mIsLargeScreen; } /** Size of the bubble. */ public int getBubbleSize() { - return (mShowingInTaskbar && mTaskbarIconSize > 0) - ? mTaskbarIconSize - : mBubbleSize; + return mBubbleSize; } /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ @@ -699,9 +633,6 @@ public class BubblePositioner { /** The position the bubble stack should rest at when collapsed. */ public PointF getRestingPosition() { - if (mPinLocation != null) { - return mPinLocation; - } if (mRestingStackPosition == null) { return getDefaultStartPosition(); } @@ -713,9 +644,6 @@ public class BubblePositioner { * is being shown. */ public PointF getDefaultStartPosition() { - if (mPinLocation != null) { - return mPinLocation; - } // Start on the left if we're in LTR, right otherwise. final boolean startOnLeft = mContext.getResources().getConfiguration().getLayoutDirection() @@ -730,7 +658,6 @@ public class BubblePositioner { 1 /* default starts with 1 bubble */)); } - /** * Returns the region that the stack position must stay within. This goes slightly off the left * and right sides of the screen, below the status bar/cutout and above the navigation bar. @@ -751,39 +678,6 @@ public class BubblePositioner { } /** - * @return whether the bubble stack is pinned to the taskbar. - */ - public boolean showingInTaskbar() { - return mShowingInTaskbar; - } - - /** - * @return the taskbar position if set. - */ - public int getTaskbarPosition() { - return mTaskbarPosition; - } - - public int getTaskbarSize() { - return mTaskbarSize; - } - - /** - * In some situations bubbles will be pinned to a specific onscreen location. This sets whether - * bubbles should be pinned or not. - */ - public void setUsePinnedLocation(boolean usePinnedLocation) { - if (usePinnedLocation) { - mShowingInTaskbar = true; - mPinLocation = new PointF(mPositionRect.right - mBubbleSize, - mPositionRect.bottom - mBubbleSize); - } else { - mPinLocation = null; - mShowingInTaskbar = false; - } - } - - /** * Navigation bar has an area where system gestures can be started from. * * @return {@link Rect} for system navigation bar gesture zone 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 f2afefe243bc..5ecbd6b596b6 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 @@ -680,8 +680,6 @@ public class BubbleStackView extends FrameLayout // Re-show the expanded view if we hid it. showExpandedViewIfNeeded(); - } else if (mPositioner.showingInTaskbar()) { - mStackAnimationController.snapStackBack(); } else { // Fling the stack to the edge, and save whether or not it's going to end up on // the left side of the screen. @@ -1362,16 +1360,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 df4325763a17..a5deac5a51da 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 @@ -257,11 +257,6 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); - /** - * Sets whether bubble bar should be enabled or not. - */ - void setBubbleBarEnabled(boolean enabled); - /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 0ee0ea60a1bc..5533842f2d89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -417,23 +417,9 @@ public class StackAnimationController extends } /** - * Snaps the stack back to the previous resting position. - */ - public void snapStackBack() { - if (mLayout == null) { - return; - } - PointF p = getStackPositionAlongNearestHorizontalEdge(); - springStackAfterFling(p.x, p.y); - } - - /** * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). */ public PointF getStackPositionAlongNearestHorizontalEdge() { - if (mPositioner.showingInTaskbar()) { - return mPositioner.getRestingPosition(); - } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java index 22587f4c6456..8b4ac1a8dc79 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java @@ -39,6 +39,9 @@ import java.util.List; * * Note that most of the implementation here inherits from * {@link com.android.systemui.statusbar.policy.DevicePostureController}. + * + * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, + * which is more common. */ public class DevicePostureController { @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { 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 new file mode 100644 index 000000000000..bf226283ae54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -0,0 +1,208 @@ +/* + * 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.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +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.NonNull; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.ArraySet; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. + * See also <a + * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables + * #foldable_postures">Foldable states and postures</a> for reference. + * + * Use the {@link DevicePostureController} for more detailed posture changes. + */ +public class TabletopModeController implements + DevicePostureController.OnDevicePostureChangedListener, + DisplayController.OnDisplaysChangedListener { + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + + private final Context mContext; + + private final DevicePostureController mDevicePostureController; + + private final DisplayController mDisplayController; + + private final ShellExecutor mMainExecutor; + + private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); + + private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); + + @VisibleForTesting + final Runnable mOnEnterTabletopModeCallback = () -> { + if (isInTabletopMode()) { + // We are still in tabletop mode, go ahead. + mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); + } + }; + + @DevicePostureController.DevicePostureInt + private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; + + @Surface.Rotation + private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; + + /** + * Track the last callback value for {@link OnTabletopModeChangedListener}. + * This is to avoid duplicated {@code false} callback to {@link #mListeners}. + */ + private Boolean mLastIsInTabletopModeForCallback; + + public TabletopModeController(Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mDevicePostureController = postureController; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + @VisibleForTesting + void onInit() { + mDevicePostureController.registerOnDevicePostureChangedListener(this); + mDisplayController.addDisplayWindowListener(this); + // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. + final int[] deviceTabletopRotations = mContext.getResources().getIntArray( + com.android.internal.R.array.config_deviceTabletopRotations); + if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { + ProtoLog.e(WM_SHELL_FOLDABLE, + "No valid config_deviceTabletopRotations, can not tell" + + " tabletop mode in WMShell"); + return; + } + for (int angle : deviceTabletopRotations) { + switch (angle) { + case 0: + mTabletopModeRotations.add(Surface.ROTATION_0); + break; + case 90: + mTabletopModeRotations.add(Surface.ROTATION_90); + break; + case 180: + mTabletopModeRotations.add(Surface.ROTATION_180); + break; + case 270: + mTabletopModeRotations.add(Surface.ROTATION_270); + break; + default: + ProtoLog.e(WM_SHELL_FOLDABLE, + "Invalid surface rotation angle in " + + "config_deviceTabletopRotations: %d", + angle); + break; + } + } + } + + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ + public void registerOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + if (listener == null || mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onTabletopModeChanged(isInTabletopMode()); + } + + /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ + public void unregisterOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { + if (mDevicePosture != posture) { + onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); + if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { + onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); + } + } + + private void onDevicePostureOrDisplayRotationChanged( + @DevicePostureController.DevicePostureInt int newPosture, + @Surface.Rotation int newDisplayRotation) { + final boolean wasInTabletopMode = isInTabletopMode(); + mDevicePosture = newPosture; + mDisplayRotation = newDisplayRotation; + final boolean couldBeInTabletopMode = isInTabletopMode(); + mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); + if (!wasInTabletopMode && couldBeInTabletopMode) { + // May enter tabletop mode, but we need to wait for additional time since this + // could be an intermediate state. + mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); + } else { + // Cancel entering tabletop mode if any condition's changed. + mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); + } + } + + private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { + return posture == DEVICE_POSTURE_HALF_OPENED; + } + + private boolean isInTabletopMode() { + return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); + } + + private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { + if (mLastIsInTabletopModeForCallback == null + || mLastIsInTabletopModeForCallback != isInTabletopMode) { + mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); + mLastIsInTabletopModeForCallback = isInTabletopMode; + } + } + + /** + * Listener interface for tabletop mode change. + */ + public interface OnTabletopModeChangedListener { + /** + * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. + * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. + */ + void onTabletopModeChanged(boolean isInTabletopMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index abb357c5b653..bdf0ac2ed30c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -247,11 +247,11 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Stops showing resizing hint. */ public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { - if (mScreenshot != null) { - if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { - mScreenshotAnimator.cancel(); - } + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + if (mScreenshot != null) { t.setPosition(mScreenshot, mOffsetX, mOffsetY); final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); @@ -321,6 +321,10 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mTempRect.set(mOldBounds); mTempRect.offsetTo(0, 0); mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, @@ -333,6 +337,10 @@ public class SplitDecorManager extends WindowlessWindowManager { if (screenshot == null || !screenshot.isValid()) return; if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mScreenshot = screenshot; t.reparent(screenshot, mHostLeash); t.setLayer(screenshot, Integer.MAX_VALUE - 1); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index f616e6f64750..ffc56b6f6106 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -120,6 +120,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mOrientation; private int mRotation; private int mDensity; + private int mUiMode; private final boolean mDimNonImeSide; private ValueAnimator mDividerFlingAnimator; @@ -295,10 +296,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final Rect rootBounds = configuration.windowConfiguration.getBounds(); final int orientation = configuration.orientation; final int density = configuration.densityDpi; + final int uiMode = configuration.uiMode; if (mOrientation == orientation && mRotation == rotation && mDensity == density + && mUiMode == uiMode && mRootBounds.equals(rootBounds)) { return false; } @@ -310,6 +313,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRootBounds.set(rootBounds); mRotation = rotation; mDensity = density; + mUiMode = uiMode; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); updateDividerConfig(mContext); initDividerPosition(mTempRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 72dc771ee08c..ef21c7e9ec0c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -51,6 +51,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.ShellAnimationThread; @@ -171,6 +172,18 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static TabletopModeController provideTabletopModeController( + Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new TabletopModeController( + context, shellInit, postureController, displayController, mainExecutor); + } + + @WMSingleton + @Provides static DragAndDropController provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, 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 ac13f96585b6..f9e0ca53b32d 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 @@ -247,6 +247,11 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { mLaunchRootTask = taskInfo; } + if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId + && !taskInfo.equals(mHomeTask)) { + mHomeTask = taskInfo; + } + super.onTaskInfoChanged(taskInfo); } @@ -364,6 +369,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/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 480bf93b2ddb..53bf42a3c911 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 @@ -39,6 +39,9 @@ import android.window.TaskSnapshot; * Represents the content overlay used during the entering PiP animation. */ public abstract class PipContentOverlay { + // Fixed string used in WMShellFlickerTests + protected static final String LAYER_NAME = "PipContentOverlay"; + protected SurfaceControl mLeash; /** Attaches the internal {@link #mLeash} to the given parent leash. */ @@ -86,7 +89,7 @@ public abstract class PipContentOverlay { mContext = context; mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .setColorLayer() .build(); } @@ -139,7 +142,7 @@ public abstract class PipContentOverlay { mSourceRectHint = new Rect(sourceRectHint); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } @@ -194,7 +197,7 @@ public abstract class PipContentOverlay { prepareAppIconOverlay(activityInfo); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } 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 f11836ea5bee..eb336d56b62c 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 @@ -1179,6 +1179,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) { @@ -1594,7 +1608,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // source rect hint to enter PiP use bounds animation. if (sourceHintRect == null) { if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, mTaskInfo.topActivityInfo); } else { 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 e5c0570841f4..7234b15bf6d2 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 @@ -804,7 +804,7 @@ public class PipTransition extends PipTransitionController { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, taskInfo.topActivityInfo); } else { 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 fa3efeb51bd0..0d5f1432d204 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 @@ -101,6 +101,7 @@ import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -181,14 +182,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); + } } } } @@ -874,6 +881,21 @@ 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); + Set<Rect> restrictedKeepClearAreas = new HashSet<>( + mPipBoundsState.getRestrictedKeepClearAreas()); + restrictedKeepClearAreas.add(rect); + mPipBoundsState.setKeepClearAreas(restrictedKeepClearAreas, + mPipBoundsState.getUnrestrictedKeepClearAreas()); + updatePipPositionForKeepClearAreas(); + } + } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mOnIsInPipStateChangedListener = callback; if (mOnIsInPipStateChangedListener != null) { @@ -1237,6 +1259,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/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 75f9a4c33af9..c9b3a1af6507 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; 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 7d3e7ca671e5..71ee690146f9 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; @@ -422,6 +423,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 +442,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 +707,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(); @@ -764,17 +755,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (pendingIntent2 == null) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - if (shortcutInfo1 != null) { - wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); - } else { - wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); - } - mSyncQueue.queue(wct); + // Launching a solo intent or shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1, + options1, adapter, wct); return; } @@ -797,13 +780,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (taskId == INVALID_TASK_ID) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - mSyncQueue.queue(wct); + // Launching a solo intent as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1, + adapter, wct); return; } @@ -822,13 +801,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (taskId == INVALID_TASK_ID) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); - mSyncQueue.queue(wct); + // Launching a solo shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct); return; } @@ -838,6 +812,49 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, instanceId); } + private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent, + @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, + @Nullable Bundle options, RemoteAnimationAdapter adapter, + WindowContainerTransaction wct) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + t.apply(); + + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + addActivityOptions(options, null /* launchTarget */); + if (shortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options); + } else if (pendingIntent != null) { + wct.sendPendingIntent(pendingIntent, fillInIntent, options); + } else { + Slog.e(TAG, "Pending intent and shortcut are null is invalid case."); + } + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + private void startWithLegacyTransition(WindowContainerTransaction wct, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, @@ -894,23 +911,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (options == null) options = new Bundle(); addActivityOptions(options, mMainStage); - options = wrapAsSplitRemoteAnimation(adapter, options); updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); // TODO(b/268008375): Merge APIs to start a split pair into one. if (mainTaskId != INVALID_TASK_ID) { + options = wrapAsSplitRemoteAnimation(adapter, options); wct.startTask(mainTaskId, options); - } else if (mainShortcutInfo != null) { - wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + mSyncQueue.queue(wct); } else { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + if (mainShortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + } else { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + } + mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct); } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); }); @@ -967,6 +986,54 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return activityOptions.toBundle(); } + private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation( + RemoteAnimationAdapter adapter) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + // Wrap the divider bar into non-apps target to animate together. + nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, + getDividerBarLegacyTarget()); + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + // Reset the surface position of the opening app to prevent offset. + t.setPosition(apps[i].leash, 0, 0); + } + } + t.apply(); + + IRemoteAnimationFinishedCallback wrapCallback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() throws RemoteException { + onRemoteAnimationFinished(apps); + finishedCallback.onAnimationFinished(); + } + }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, wrapCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + return transition; + } + private void setEnterInstanceId(InstanceId instanceId) { if (instanceId != null) { mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); @@ -993,6 +1060,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSplitRequest = null; + // If any stage has no child after finished animation, that side of the split will display + // nothing. This might happen if starting the same app on the both sides while not + // supporting multi-instance. Exit the split screen and expand that app to full screen. + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); + return; + } + + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct); + mSyncQueue.queue(evictWct); + } + + /** * Collects all the current child tasks of a specific split and prepares transaction to evict * them to display. @@ -1247,7 +1335,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 { @@ -1279,7 +1367,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 -> { @@ -1391,7 +1479,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) { @@ -1595,6 +1683,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRootTaskInfo = null; mRootTaskLeash = null; + mIsRootTranslucent = false; } @@ -1613,7 +1702,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); @@ -1637,7 +1726,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 -> { @@ -1661,6 +1750,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. @@ -1687,10 +1783,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); @@ -1822,7 +1919,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); 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/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java new file mode 100644 index 000000000000..96d202ce3a85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java @@ -0,0 +1,270 @@ +/* + * 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.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link TabletopModeController}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TabletopModeControllerTest extends ShellTestCase { + // It's considered tabletop mode if the display rotation angle matches what's in this array. + // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. + private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { + 90 /* Surface.ROTATION_90 */, + 270 /* Surface.ROTATION_270 */ + }; + + private TestShellExecutor mMainExecutor; + + private Configuration mConfiguration; + + private TabletopModeController mPipTabletopController; + + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private Resources mResources; + + @Mock + private DevicePostureController mDevicePostureController; + + @Mock + private DisplayController mDisplayController; + + @Mock + private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) + .thenReturn(TABLETOP_MODE_ROTATIONS); + when(mContext.getResources()).thenReturn(mResources); + mMainExecutor = new TestShellExecutor(); + mConfiguration = new Configuration(); + mPipTabletopController = new TabletopModeController(mContext, mShellInit, + mDevicePostureController, mDisplayController, mMainExecutor); + mPipTabletopController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(false); + } + + @Test + public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(true); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + clearInvocations(mOnTabletopModeChangedListener); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + verifyZeroInteractions(mOnTabletopModeChangedListener); + } + + // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) + @Test + public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) + @Test + public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } +} 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/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java index 5ecec4ddd1ad..3125f088c72b 100644 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java @@ -72,6 +72,7 @@ public final class LowLightDreamManager { public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; private final DreamManager mDreamManager; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; @Nullable private final ComponentName mLowLightDreamComponent; @@ -81,8 +82,10 @@ public final class LowLightDreamManager { @Inject public LowLightDreamManager( DreamManager dreamManager, + LowLightTransitionCoordinator lowLightTransitionCoordinator, @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { mDreamManager = dreamManager; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mLowLightDreamComponent = lowLightDreamComponent; } @@ -111,7 +114,9 @@ public final class LowLightDreamManager { mAmbientLightMode = ambientLightMode; - mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT - ? mLowLightDreamComponent : null); + boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; + mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, + () -> mDreamManager.setSystemDreamComponent( + shouldEnterLowLight ? mLowLightDreamComponent : null)); } } diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java new file mode 100644 index 000000000000..874a2d5af75e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java @@ -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.dream.lowlight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +public class LowLightTransitionCoordinator { + /** + * Listener that is notified before low light entry. + */ + public interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + Animator onBeforeEnterLowLight(); + } + + /** + * Listener that is notified before low light exit. + */ + public interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + Animator onBeforeExitLowLight(); + } + + private LowLightEnterListener mLowLightEnterListener; + private LowLightExitListener mLowLightExitListener; + + @Inject + public LowLightTransitionCoordinator() { + } + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { + mLowLightEnterListener = lowLightEnterListener; + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { + mLowLightExitListener = lowLightExitListener; + } + + /** + * Notifies listeners that the device is about to enter or exit low light. + * + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + * @param callback callback that will be run after listeners complete. + */ + void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { + Animator animator = null; + + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener.onBeforeEnterLowLight(); + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener.onBeforeExitLowLight(); + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + callback.run(); + } + }); + } else { + callback.run(); + } + } +} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java index 91a170f7ae14..4b95d8c84bac 100644 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java @@ -21,7 +21,10 @@ import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -44,44 +47,52 @@ public class LowLightDreamManagerTest { private DreamManager mDreamManager; @Mock + private LowLightTransitionCoordinator mTransitionCoordinator; + + @Mock private ComponentName mDreamComponent; + LowLightDreamManager mLowLightDreamManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); + + // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(1)).run(); + return null; + }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), + any(Runnable.class)); + + mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, + mDreamComponent); } @Test public void setAmbientLightMode_lowLight_setSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); verify(mDreamManager).setSystemDreamComponent(mDreamComponent); } @Test public void setAmbientLightMode_regularLight_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); verify(mDreamManager).setSystemDreamComponent(null); } @Test public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - // Set to low light first. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); clearInvocations(mDreamManager); // Return to default unknown mode. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); verify(mDreamManager).setSystemDreamComponent(null); } @@ -89,7 +100,7 @@ public class LowLightDreamManagerTest { @Test public void setAmbientLightMode_dreamComponentNotSet_doNothing() { final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - null /*dream component*/); + mTransitionCoordinator, null /*dream component*/); lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java new file mode 100644 index 000000000000..81e1e33d6220 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java @@ -0,0 +1,113 @@ +/* + * 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.dream.lowlight; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightTransitionCoordinatorTest { + @Mock + private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; + + @Mock + private LowLightTransitionCoordinator.LowLightExitListener mExitListener; + + @Mock + private Animator mAnimator; + + @Captor + private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; + + @Mock + private Runnable mRunnable; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void onEnterCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verify(mEnterListener).onBeforeEnterLowLight(); + verify(mRunnable).run(); + } + + @Test + public void onExitCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightExitListener(mExitListener); + + coordinator.notifyBeforeLowLightTransition(false, mRunnable); + + verify(mExitListener).onBeforeExitLowLight(); + verify(mRunnable).run(); + } + + @Test + public void listenerNotCalledAfterRemoval() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + coordinator.setLowLightEnterListener(null); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verifyZeroInteractions(mEnterListener); + verify(mRunnable).run(); + } + + @Test + public void runnableCalledAfterAnimationEnds() { + when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); + + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); + verifyZeroInteractions(mRunnable); + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.getValue().onAnimationEnd(null); + verify(mRunnable).run(); + } +} |