diff options
Diffstat (limited to 'libs')
45 files changed, 1250 insertions, 201 deletions
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 310300d2f32a..d66c925de376 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -206,6 +206,8 @@ android_robolectric_test {      srcs: [          "multivalentTests/src/**/*.kt",      ], +    // TODO(b/323188766): Include BubbleStackViewTest once the robolectric issue is fixed. +    exclude_srcs: ["multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"],      static_libs: [          "junit",          "androidx.test.runner", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 5825bbfbfefa..9cd14fca6a9d 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -466,6 +466,26 @@ class BubblePositionerTest {              .isEqualTo(expectedExpandedViewY)      } +    @Test +    fun testGetTaskViewContentWidth_onLeft() { +        positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) +        val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) +        val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */, +                false /* isOverflow */) +        assertThat(taskViewWidth).isEqualTo( +                positioner.screenRect.width() - paddings[0] - paddings[2]) +    } + +    @Test +    fun testGetTaskViewContentWidth_onRight() { +        positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) +        val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) +        val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */, +                false /* isOverflow */) +        assertThat(taskViewWidth).isEqualTo( +                positioner.screenRect.width() - paddings[0] - paddings[2]) +    } +      private val defaultYPosition: Float          /**           * Calculates the Y position bubbles should be placed based on the config. Based on the diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt new file mode 100644 index 000000000000..8989fc543044 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.view.IWindowManager +import android.view.WindowManager +import android.view.WindowManagerGlobal +import androidx.test.annotation.UiThreadTest +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.common.ProtoLog +import com.android.launcher3.icons.BubbleIconFactory +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** Unit tests for [BubbleStackView]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleStackViewTest { + +    private val context = ApplicationProvider.getApplicationContext<Context>() +    private lateinit var positioner: BubblePositioner +    private lateinit var iconFactory: BubbleIconFactory +    private lateinit var expandedViewManager: FakeBubbleExpandedViewManager +    private lateinit var bubbleStackView: BubbleStackView +    private lateinit var shellExecutor: ShellExecutor +    private lateinit var windowManager: IWindowManager +    private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory +    private lateinit var bubbleData: BubbleData + +    @Before +    fun setUp() { +        // Disable protolog tool when running the tests from studio +        ProtoLog.REQUIRE_PROTOLOGTOOL = false +        windowManager = WindowManagerGlobal.getWindowManagerService()!! +        shellExecutor = TestShellExecutor() +        val windowManager = context.getSystemService(WindowManager::class.java) +        iconFactory = +            BubbleIconFactory( +                context, +                context.resources.getDimensionPixelSize(R.dimen.bubble_size), +                context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size), +                Color.BLACK, +                context.resources.getDimensionPixelSize( +                    com.android.internal.R.dimen.importance_ring_stroke_width +                ) +            ) +        positioner = BubblePositioner(context, windowManager) +        val bubbleStackViewManager = FakeBubbleStackViewManager() +        bubbleData = +            BubbleData( +                context, +                BubbleLogger(UiEventLoggerFake()), +                positioner, +                BubbleEducationController(context), +                shellExecutor +            ) + +        val sysuiProxy = mock<SysuiProxy>() +        expandedViewManager = FakeBubbleExpandedViewManager() +        bubbleTaskViewFactory = FakeBubbleTaskViewFactory() +        bubbleStackView = +            BubbleStackView( +                context, +                bubbleStackViewManager, +                positioner, +                bubbleData, +                null, +                FloatingContentCoordinator(), +                { sysuiProxy }, +                shellExecutor +            ) +    } + +    @UiThreadTest +    @Test +    fun addBubble() { +        val bubble = createAndInflateBubble() +        bubbleStackView.addBubble(bubble) +        assertThat(bubbleStackView.bubbleCount).isEqualTo(1) +    } + +    @UiThreadTest +    @Test +    fun tapBubbleToExpand() { +        val bubble = createAndInflateBubble() +        bubbleStackView.addBubble(bubble) +        assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + +        bubble.iconView!!.performClick() +        // we're checking the expanded state in BubbleData because that's the source of truth. This +        // will eventually propagate an update back to the stack view, but setting the entire +        // pipeline is outside the scope of a unit test. +        assertThat(bubbleData.isExpanded).isTrue() +    } + +    private fun createAndInflateBubble(): Bubble { +        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) +        val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) +        val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) +        bubble.setInflateSynchronously(true) +        bubbleData.notificationEntryUpdated(bubble, true, false) + +        val semaphore = Semaphore(0) +        val callback: BubbleViewInfoTask.Callback = +            BubbleViewInfoTask.Callback { semaphore.release() } +        bubble.inflate( +            callback, +            context, +            expandedViewManager, +            bubbleTaskViewFactory, +            positioner, +            bubbleStackView, +            null, +            iconFactory, +            false +        ) + +        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() +        assertThat(bubble.isInflated).isTrue() +        return bubble +    } + +    private class FakeBubbleStackViewManager : BubbleStackViewManager { + +        override fun onAllBubblesAnimatedOut() {} + +        override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {} + +        override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {} + +        override fun hideCurrentInputMethod() {} +    } + +    private class TestShellExecutor : ShellExecutor { + +        override fun execute(runnable: Runnable) { +            runnable.run() +        } + +        override fun executeDelayed(r: Runnable, delayMillis: Long) { +            r.run() +        } + +        override fun removeCallbacks(r: Runnable) {} + +        override fun hasCallback(r: Runnable): Boolean = false +    } + +    private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { +        override fun create(): BubbleTaskView { +            val taskViewTaskController = mock<TaskViewTaskController>() +            val taskView = TaskView(context, taskViewTaskController) +            return BubbleTaskView(taskView, shellExecutor) +        } +    } + +    private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager { + +        override val overflowBubbles: List<Bubble> +            get() = emptyList() + +        override fun setOverflowListener(listener: BubbleData.Listener) {} + +        override fun collapseStack() {} + +        override fun updateWindowFlagsForBackpress(intercept: Boolean) {} + +        override fun promoteBubbleFromOverflow(bubble: Bubble) {} + +        override fun removeBubble(key: String, reason: Int) {} + +        override fun dismissBubble(bubble: Bubble, reason: Int) {} + +        override fun setAppBubbleTaskId(key: String, taskId: Int) {} + +        override fun isStackExpanded(): Boolean = false + +        override fun isShowingAsBubbleBar(): Boolean = false +    } +} diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index e04ab817215c..34f03c2f226b 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -23,7 +23,8 @@      <com.android.wm.shell.bubbles.bar.BubbleBarHandleView          android:id="@+id/bubble_bar_handle_view" -        android:layout_height="wrap_content" -        android:layout_width="wrap_content" /> +        android:layout_height="@dimen/bubble_bar_expanded_view_caption_height" +        android:layout_width="@dimen/bubble_bar_expanded_view_caption_width" +        android:layout_gravity="top|center_horizontal" />  </com.android.wm.shell.bubbles.bar.BubbleBarExpandedView> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f73775becac9..cbfa74e9332b 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -244,6 +244,8 @@      <dimen name="bubble_popup_padding">24dp</dimen>      <!-- The size of the caption bar inset at the top of bubble bar expanded view. -->      <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen> +    <!-- The width of the caption bar at the top of bubble bar expanded view. --> +    <dimen name="bubble_bar_expanded_view_caption_width">128dp</dimen>      <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. -->      <dimen name="bubble_bar_expanded_view_caption_dot_size">4dp</dimen>      <!-- The spacing between the dots for the caption menu in the bubble bar expanded view.. --> @@ -501,6 +503,17 @@      fullscreen if dragged until the top bound of the task is within the area. -->      <dimen name="desktop_mode_transition_area_height">16dp</dimen> +    <!-- The width of the area where a desktop task will transition to fullscreen. --> +    <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen> + +    <!-- The height of the area where a desktop task will transition to fullscreen. --> +    <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen> + +    <!-- The height on the screen where drag to the left or right edge will result in a +    desktop task snapping to split size. The empty space between this and the top is to allow +    for corner drags without transition. --> +    <dimen name="desktop_mode_split_from_desktop_height">100dp</dimen> +      <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->      <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>      <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index fe65fdd30e48..d8d0d876b4f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -643,19 +643,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements          }      } -    /** Helper to set int metadata on the Surface corresponding to the task id. */ -    public void setSurfaceMetadata(int taskId, int key, int value) { -        synchronized (mLock) { -            final TaskAppearedInfo info = mTasks.get(taskId); -            if (info == null || info.getLeash() == null) { -                return; -            } -            SurfaceControl.Transaction t = new SurfaceControl.Transaction(); -            t.setMetadata(info.getLeash(), key, value); -            t.apply(); -        } -    } -      private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash,              TaskListener oldListener, TaskListener newListener) {          if (oldListener == newListener) return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 2ec9e8b12fc6..19963675ff86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -71,6 +71,13 @@ public class Interpolators {       */      public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(              0.05f, 0.7f, 0.1f, 1f); + +    /** +     * The standard decelerating interpolator that should be used on every regular movement of +     * content that is appearing e.g. when coming from off screen. +     */ +    public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(0f, 0f, 0f, 1f); +      /**       * Interpolator to be used when animating a move based on a click. Pair with enough duration.       */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index e7f6f0d61847..34be9b097e81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -404,10 +404,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont      @VisibleForTesting      void onPilferPointers() { -        mCurrentTracker.updateStartLocation();          // Dispatch onBackStarted, only to app callbacks.          // System callbacks will receive onBackStarted when the remote animation starts.          if (!shouldDispatchToAnimator() && mActiveCallback != null) { +            mCurrentTracker.updateStartLocation();              tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));          }      } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java index 55982dca79b3..d6f7c367f772 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java @@ -40,7 +40,6 @@ import android.view.IRemoteAnimationFinishedCallback;  import android.view.IRemoteAnimationRunner;  import android.view.RemoteAnimationTarget;  import android.view.SurfaceControl; -import android.view.animation.DecelerateInterpolator;  import android.view.animation.Interpolator;  import android.window.BackEvent;  import android.window.BackMotionEvent; @@ -51,6 +50,7 @@ import com.android.internal.dynamicanimation.animation.SpringAnimation;  import com.android.internal.dynamicanimation.animation.SpringForce;  import com.android.internal.policy.ScreenDecorationsUtils;  import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.animation.Interpolators;  import com.android.wm.shell.common.annotations.ShellMainThread;  import javax.inject.Inject; @@ -65,7 +65,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation {      /** Duration of post animation after gesture committed. */      private static final int POST_ANIMATION_DURATION = 350; -    private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); +    private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;      private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =              new FloatProperty<>("enter-alpha") {                  @Override @@ -285,7 +285,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation {          ValueAnimator valueAnimator =                  ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION); -        valueAnimator.setInterpolator(new DecelerateInterpolator()); +        valueAnimator.setInterpolator(INTERPOLATOR);          valueAnimator.addUpdateListener(animation -> {              float progress = animation.getAnimatedFraction();              updatePostCommitEnteringAnimation(progress); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index adc78391f033..4b3154190910 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -91,7 +91,8 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {      private final PointF mInitialTouchPos = new PointF();      private final Interpolator mPostAnimationInterpolator = Interpolators.EMPHASIZED; -    private final Interpolator mProgressInterpolator = new DecelerateInterpolator(); +    private final Interpolator mProgressInterpolator = Interpolators.STANDARD_DECELERATE; +    private final Interpolator mVerticalMoveInterpolator = new DecelerateInterpolator();      private final Matrix mTransformMatrix = new Matrix();      private final float[] mTmpFloat9 = new float[9]; @@ -169,7 +170,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {          float yDirection = rawYDelta < 0 ? -1 : 1;          // limit yDelta interpretation to 1/2 of screen height in either direction          float deltaYRatio = Math.min(height / 2f, Math.abs(rawYDelta)) / (height / 2f); -        float interpolatedYRatio = mProgressInterpolator.getInterpolation(deltaYRatio); +        float interpolatedYRatio = mVerticalMoveInterpolator.getInterpolation(deltaYRatio);          // limit y-shift so surface never passes 8dp screen margin          float deltaY = yDirection * interpolatedYRatio * Math.max(0f,                  (height - scaledHeight) / 2f - mVerticalMargin); 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 5c6f73f0a4a2..e0f0556d03f0 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 @@ -481,7 +481,12 @@ public class BubbleController implements ConfigurationChangeListener,                  });          mOneHandedOptional.ifPresent(this::registerOneHandedState); -        mDragAndDropController.addListener(this::collapseStack); +        mDragAndDropController.addListener(new DragAndDropController.DragAndDropListener() { +            @Override +            public void onDragStarted() { +                collapseStack(); +            } +        });          // Clear out any persisted bubbles on disk that no longer have a valid user.          List<UserInfo> users = mUserManager.getAliveUsers(); @@ -1838,7 +1843,7 @@ public class BubbleController implements ConfigurationChangeListener,                      + " expanded=%b selectionChanged=%b selected=%s"                      + " suppressed=%s unsupressed=%s shouldShowEducation=%b",                      update.addedBubble != null ? update.addedBubble.getKey() : "null", -                    update.removedBubbles.isEmpty(), +                    !update.removedBubbles.isEmpty(),                      update.updatedBubble != null ? update.updatedBubble.getKey() : "null",                      update.orderChanged, update.expandedChanged, update.expanded,                      update.selectionChanged, 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 cda29c95281d..a5853d621cb5 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 @@ -395,7 +395,7 @@ public class BubblePositioner {      public int getTaskViewContentWidth(boolean onLeft) {          int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false);          int pointerOffset = showBubblesVertically() ? getPointerSize() : 0; -        return mPositionRect.width() - paddings[0] - paddings[2] - pointerOffset; +        return mScreenRect.width() - paddings[0] - paddings[2] - pointerOffset;      }      /** Gets the y position of the expanded view if it was top-aligned. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index eddd43f263d9..271fb9abce6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -143,6 +143,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView                  outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCurrentCornerRadius);              }          }); +        // Set a touch sink to ensure that clicks on the caption area do not propagate to the parent +        setOnTouchListener((v, event) -> true);      }      @Override @@ -245,12 +247,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          super.onMeasure(widthMeasureSpec, heightMeasureSpec); -        int height = MeasureSpec.getSize(heightMeasureSpec); -        int menuViewHeight = Math.min(mCaptionHeight, height); -        measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight, -                MeasureSpec.getMode(heightMeasureSpec))); -          if (mTaskView != null) { +            int height = MeasureSpec.getSize(heightMeasureSpec);              measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height,                      MeasureSpec.getMode(heightMeasureSpec)));          } @@ -259,14 +257,11 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView      @Override      protected void onLayout(boolean changed, int l, int t, int r, int b) {          super.onLayout(changed, l, t, r, b); -        final int captionBottom = t + mCaptionHeight;          if (mTaskView != null) {              mTaskView.layout(l, t, r,                      t + mTaskView.getMeasuredHeight());              mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0));          } -        // Handle draws on top of task view in the caption area. -        mHandleView.layout(l, t, r, captionBottom);      }      @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 62f2726ad9bd..78a41f759d96 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -231,6 +231,7 @@ public class BubbleBarLayerView extends FrameLayout              // Touch delegate for the menu              BubbleBarHandleView view = mExpandedView.getHandleView();              view.getBoundsOnScreen(mHandleTouchBounds); +            // Move top value up to ensure touch target is large enough              mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop();              mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds,                      mExpandedView.getHandleView()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java index a9f687fc9b2d..6ffeb97f50fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java @@ -125,11 +125,15 @@ public class PipBoundsAlgorithm {      public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {          final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState(); -        final Rect destinationBounds = reentryState != null -                ? getDefaultBounds(reentryState.getSnapFraction(), reentryState.getSize()) -                : getDefaultBounds(); +        final Rect destinationBounds = getDefaultBounds(); +        if (reentryState != null) { +            final Size scaledBounds = new Size( +                    Math.round(mPipBoundsState.getMaxSize().x * reentryState.getBoundsScale()), +                    Math.round(mPipBoundsState.getMaxSize().y * reentryState.getBoundsScale())); +            destinationBounds.set(getDefaultBounds(reentryState.getSnapFraction(), scaledBounds)); +        } -        final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null; +        final boolean useCurrentSize = reentryState != null;          Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds,                  mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,                  useCurrentSize); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index df589df8b227..b57e2d2c6ac2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -298,8 +298,8 @@ public class PipBoundsState {      }      /** Save the reentry state to restore to when re-entering PIP mode. */ -    public void saveReentryState(Size size, float fraction) { -        mPipReentryState = new PipReentryState(size, fraction); +    public void saveReentryState(float fraction) { +        mPipReentryState = new PipReentryState(mBoundsScale, fraction);      }      /** Returns the saved reentry state. */ @@ -601,17 +601,16 @@ public class PipBoundsState {      public static final class PipReentryState {          private static final String TAG = PipReentryState.class.getSimpleName(); -        private final @Nullable Size mSize;          private final float mSnapFraction; +        private final float mBoundsScale; -        PipReentryState(@Nullable Size size, float snapFraction) { -            mSize = size; +        PipReentryState(float boundsScale, float snapFraction) { +            mBoundsScale = boundsScale;              mSnapFraction = snapFraction;          } -        @Nullable -        public Size getSize() { -            return mSize; +        public float getBoundsScale() { +            return mBoundsScale;          }          public float getSnapFraction() { @@ -621,7 +620,7 @@ public class PipBoundsState {          void dump(PrintWriter pw, String prefix) {              final String innerPrefix = prefix + "  ";              pw.println(prefix + TAG); -            pw.println(innerPrefix + "mSize=" + mSize); +            pw.println(innerPrefix + "mBoundsScale=" + mBoundsScale);              pw.println(innerPrefix + "mSnapFraction=" + mSnapFraction);          }      } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java index 81d13999e73c..7c280994042b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -20,6 +20,7 @@ import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;  import android.annotation.NonNull;  import android.annotation.Nullable; +import android.app.AppCompatTaskInfo;  import android.app.TaskInfo;  import android.content.Context;  import android.content.Intent; @@ -88,7 +89,8 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract          mShellExecutor = shellExecutor;          mUserAspectRatioButtonShownChecker = userAspectRatioButtonStateChecker;          mUserAspectRatioButtonStateConsumer = userAspectRatioButtonShownConsumer; -        mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); +        mHasUserAspectRatioSettingsButton = shouldShowUserAspectRatioSettingsButton( +                taskInfo.appCompatTaskInfo, taskInfo.baseIntent);          mCompatUIHintsState = compatUIHintsState;          mOnButtonClicked = onButtonClicked;          mDisappearTimeSupplier = disappearTimeSupplier; @@ -134,7 +136,8 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract      public boolean updateCompatInfo(@NonNull TaskInfo taskInfo,              @NonNull ShellTaskOrganizer.TaskListener taskListener, boolean canShow) {          final boolean prevHasUserAspectRatioSettingsButton = mHasUserAspectRatioSettingsButton; -        mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); +        mHasUserAspectRatioSettingsButton = shouldShowUserAspectRatioSettingsButton( +                taskInfo.appCompatTaskInfo, taskInfo.baseIntent);          if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {              return false; @@ -227,12 +230,21 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract          return SystemClock.uptimeMillis() + hideDelay;      } -    private boolean getHasUserAspectRatioSettingsButton(@NonNull TaskInfo taskInfo) { -        final Intent intent = taskInfo.baseIntent; -        return taskInfo.appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton -                && (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed -                    || taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled) -                && !taskInfo.appCompatTaskInfo.isSystemFullscreenOverrideEnabled +    private boolean shouldShowUserAspectRatioSettingsButton(@NonNull AppCompatTaskInfo taskInfo, +            @NonNull Intent intent) { +        final Rect stableBounds = getTaskStableBounds(); +        final int letterboxHeight = taskInfo.topActivityLetterboxHeight; +        final int letterboxWidth = taskInfo.topActivityLetterboxWidth; +        // App is not visibly letterboxed if it covers status bar/bottom insets or matches the +        // stable bounds, so don't show the button +        if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth) { +            return false; +        } + +        return taskInfo.topActivityEligibleForUserAspectRatioButton +                && (taskInfo.topActivityBoundsLetterboxed +                    || taskInfo.isUserFullscreenOverrideEnabled) +                && !taskInfo.isSystemFullscreenOverrideEnabled                  && Intent.ACTION_MAIN.equals(intent.getAction())                  && intent.hasCategory(Intent.CATEGORY_LAUNCHER)                  && (!mUserAspectRatioButtonShownChecker.get() || isShowingButton()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 4fe79c13e722..f757e1c88cb8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -65,7 +65,7 @@ import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;  import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;  import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;  import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.draganddrop.UnhandledDragController; +import com.android.wm.shell.draganddrop.GlobalDragListener;  import com.android.wm.shell.freeform.FreeformComponents;  import com.android.wm.shell.freeform.FreeformTaskListener;  import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; @@ -498,6 +498,7 @@ public abstract class WMShellModule {              ShellTaskOrganizer shellTaskOrganizer,              SyncTransactionQueue syncQueue,              RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, +            DragAndDropController dragAndDropController,              Transitions transitions,              EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,              ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, @@ -506,14 +507,15 @@ public abstract class WMShellModule {              @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,              LaunchAdjacentController launchAdjacentController,              RecentsTransitionHandler recentsTransitionHandler, +            MultiInstanceHelper multiInstanceHelper,              @ShellMainThread ShellExecutor mainExecutor      ) {          return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,                  displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, -                transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, -                toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, -                desktopModeTaskRepository, launchAdjacentController, recentsTransitionHandler, -                mainExecutor); +                dragAndDropController, transitions, enterDesktopTransitionHandler, +                exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, +                dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController, +                recentsTransitionHandler, multiInstanceHelper, mainExecutor);      }      @WMSingleton @@ -562,10 +564,10 @@ public abstract class WMShellModule {      @WMSingleton      @Provides -    static UnhandledDragController provideUnhandledDragController( +    static GlobalDragListener provideGlobalDragListener(              IWindowManager wmService,              @ShellMainThread ShellExecutor mainExecutor) { -        return new UnhandledDragController(wmService, mainExecutor); +        return new GlobalDragListener(wmService, mainExecutor);      }      @WMSingleton @@ -577,9 +579,12 @@ public abstract class WMShellModule {              DisplayController displayController,              UiEventLogger uiEventLogger,              IconProvider iconProvider, +            GlobalDragListener globalDragListener, +            Transitions transitions,              @ShellMainThread ShellExecutor mainExecutor) { -        return new DragAndDropController(context, shellInit, shellController, -                shellCommandHandler, displayController, uiEventLogger, iconProvider, mainExecutor); +        return new DragAndDropController(context, shellInit, shellController, shellCommandHandler, +                displayController, uiEventLogger, iconProvider, globalDragListener, transitions, +                mainExecutor);      }      // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 8eecf1c58db0..458ea05e620d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -74,13 +74,18 @@ public abstract class Pip2Module {              ShellController shellController,              DisplayController displayController,              DisplayInsetsController displayInsetsController, -            PipDisplayLayoutState pipDisplayLayoutState) { +            PipBoundsState pipBoundsState, +            PipBoundsAlgorithm pipBoundsAlgorithm, +            PipDisplayLayoutState pipDisplayLayoutState, +            PipScheduler pipScheduler, +            @ShellMainThread ShellExecutor mainExecutor) {          if (!PipUtils.isPip2ExperimentEnabled()) {              return Optional.empty();          } else {              return Optional.ofNullable(PipController.create(                      context, shellInit, shellController, displayController, displayInsetsController, -                    pipDisplayLayoutState)); +                    pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, +                    mainExecutor));          }      } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index e732a0354806..8305fa6b0fbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -47,4 +47,8 @@ public interface DesktopMode {      default void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,              Executor callbackExecutor) { } + +    /** Called when requested to go to desktop mode from the current focused app. */ +    void enterDesktop(int displayId); +  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 6250fc5820aa..405341803a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -19,6 +19,8 @@ package com.android.wm.shell.desktopmode;  import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;  import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;  import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;  import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; @@ -28,11 +30,13 @@ import android.animation.RectEvaluator;  import android.animation.ValueAnimator;  import android.annotation.NonNull;  import android.app.ActivityManager; +import android.app.WindowConfiguration;  import android.content.Context;  import android.content.res.Resources;  import android.graphics.PixelFormat;  import android.graphics.PointF;  import android.graphics.Rect; +import android.graphics.Region;  import android.util.DisplayMetrics;  import android.view.SurfaceControl;  import android.view.SurfaceControlViewHost; @@ -41,6 +45,8 @@ import android.view.WindowManager;  import android.view.WindowlessWindowManager;  import android.view.animation.DecelerateInterpolator; +import androidx.annotation.VisibleForTesting; +  import com.android.wm.shell.R;  import com.android.wm.shell.RootTaskDisplayAreaOrganizer;  import com.android.wm.shell.common.DisplayController; @@ -93,28 +99,114 @@ public class DesktopModeVisualIndicator {      /**       * Based on the coordinates of the current drag event, determine which indicator type we should       * display, including no visible indicator. -     * TODO(b/280828642): Update drag zones per starting windowing mode.       */ -    IndicatorType updateIndicatorType(PointF inputCoordinates) { +    IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) {          final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);          // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. -        IndicatorType result = mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM -                ? IndicatorType.NO_INDICATOR : IndicatorType.TO_DESKTOP_INDICATOR; -        int transitionAreaHeight = mContext.getResources().getDimensionPixelSize( -                com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); -        int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( +        IndicatorType result = IndicatorType.NO_INDICATOR; +        final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(                  com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); -        if (inputCoordinates.y <= transitionAreaHeight) { +        // Because drags in freeform use task position for indicator calculation, we need to +        // account for the possibility of the task going off the top of the screen by captionHeight +        final int captionHeight = mContext.getResources().getDimensionPixelSize( +                com.android.wm.shell.R.dimen.desktop_mode_freeform_decor_caption_height); +        final Region fullscreenRegion = calculateFullscreenRegion(layout, windowingMode, +                captionHeight); +        final Region splitLeftRegion = calculateSplitLeftRegion(layout, windowingMode, +                transitionAreaWidth, captionHeight); +        final Region splitRightRegion = calculateSplitRightRegion(layout, windowingMode, +                transitionAreaWidth, captionHeight); +        final Region toDesktopRegion = calculateToDesktopRegion(layout, windowingMode, +                splitLeftRegion, splitRightRegion, fullscreenRegion); +        if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {              result = IndicatorType.TO_FULLSCREEN_INDICATOR; -        } else if (inputCoordinates.x <= transitionAreaWidth) { +        } +        if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {              result = IndicatorType.TO_SPLIT_LEFT_INDICATOR; -        } else if (inputCoordinates.x >= layout.width() - transitionAreaWidth) { +        } +        if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {              result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR;          } +        if (toDesktopRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { +            result = IndicatorType.TO_DESKTOP_INDICATOR; +        }          transitionIndicator(result);          return result;      } +    @VisibleForTesting +    Region calculateFullscreenRegion(DisplayLayout layout, +            @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { +        final Region region = new Region(); +        int edgeTransitionHeight = mContext.getResources().getDimensionPixelSize( +                com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); +        // A thin, short Rect at the top of the screen. +        if (windowingMode == WINDOWING_MODE_FREEFORM) { +            int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( +                    com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); +            int fromFreeformHeight = mContext.getResources().getDimensionPixelSize( +                    com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height); +            region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), +                    -captionHeight, +                    (layout.width() / 2) + (fromFreeformWidth / 2), +                    fromFreeformHeight)); +        } +        // A screen-wide, shorter Rect if the task is in fullscreen or split. +        if (windowingMode == WINDOWING_MODE_FULLSCREEN +                || windowingMode == WINDOWING_MODE_MULTI_WINDOW) { +            region.union(new Rect(0, +                    -captionHeight, +                    layout.width(), +                    edgeTransitionHeight)); +        } +        return region; +    } + +    @VisibleForTesting +    Region calculateToDesktopRegion(DisplayLayout layout, +            @WindowConfiguration.WindowingMode int windowingMode, +            Region splitLeftRegion, Region splitRightRegion, +            Region toFullscreenRegion) { +        final Region region = new Region(); +        // If in desktop, we need no region. Otherwise it's the same for all windowing modes. +        if (windowingMode != WINDOWING_MODE_FREEFORM) { +            region.union(new Rect(0, 0, layout.width(), layout.height())); +            region.op(splitLeftRegion, Region.Op.DIFFERENCE); +            region.op(splitRightRegion, Region.Op.DIFFERENCE); +            region.op(toFullscreenRegion, Region.Op.DIFFERENCE); +        } +        return region; +    } + +    @VisibleForTesting +    Region calculateSplitLeftRegion(DisplayLayout layout, +            @WindowConfiguration.WindowingMode int windowingMode, +            int transitionEdgeWidth, int captionHeight) { +        final Region region = new Region(); +        // In freeform, keep the top corners clear. +        int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM +                ? mContext.getResources().getDimensionPixelSize( +                        com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : +                -captionHeight; +        region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height())); +        return region; +    } + +    @VisibleForTesting +    Region calculateSplitRightRegion(DisplayLayout layout, +            @WindowConfiguration.WindowingMode int windowingMode, +            int transitionEdgeWidth, int captionHeight) { +        final Region region = new Region(); +        // In freeform, keep the top corners clear. +        int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM +                ? mContext.getResources().getDimensionPixelSize( +                com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : +                -captionHeight; +        region.union(new Rect(layout.width() - transitionEdgeWidth, transitionHeight, +                layout.width(), layout.height())); +        return region; +    } +      /**       * Create a fullscreen indicator with no animation       */ @@ -175,7 +267,6 @@ public class DesktopModeVisualIndicator {                          mDisplayController.getDisplayLayout(mTaskInfo.displayId));          animator.start();          mCurrentType = IndicatorType.NO_INDICATOR; -      }      /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 11304ec587e7..98f9988eaabb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -16,14 +16,19 @@  package com.android.wm.shell.desktopmode +import android.app.ActivityManager  import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityOptions +import android.app.PendingIntent  import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME  import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD  import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM  import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW  import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED  import android.app.WindowConfiguration.WindowingMode  import android.content.Context +import android.content.Intent  import android.graphics.Point  import android.graphics.PointF  import android.graphics.Rect @@ -31,6 +36,7 @@ import android.graphics.Region  import android.os.IBinder  import android.os.SystemProperties  import android.util.DisplayMetrics.DENSITY_DEFAULT +import android.view.Display.DEFAULT_DISPLAY  import android.view.SurfaceControl  import android.view.WindowManager.TRANSIT_CHANGE  import android.view.WindowManager.TRANSIT_NONE @@ -48,6 +54,8 @@ import com.android.wm.shell.common.DisplayController  import com.android.wm.shell.common.ExecutorUtils  import com.android.wm.shell.common.ExternalInterfaceBinder  import com.android.wm.shell.common.LaunchAdjacentController +import com.android.wm.shell.common.MultiInstanceHelper +import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent  import com.android.wm.shell.common.RemoteCallable  import com.android.wm.shell.common.ShellExecutor  import com.android.wm.shell.common.SingleInstanceRemoteListener @@ -58,7 +66,9 @@ import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOT  import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT  import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener  import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener +import com.android.wm.shell.draganddrop.DragAndDropController  import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.recents.RecentTasksController  import com.android.wm.shell.recents.RecentsTransitionHandler  import com.android.wm.shell.recents.RecentsTransitionStateListener  import com.android.wm.shell.splitscreen.SplitScreenController @@ -75,6 +85,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener  import java.io.PrintWriter  import java.util.concurrent.Executor  import java.util.function.Consumer +import java.util.function.Function  /** Handles moving tasks in and out of desktop */  class DesktopTasksController( @@ -86,6 +97,7 @@ class DesktopTasksController(          private val shellTaskOrganizer: ShellTaskOrganizer,          private val syncQueue: SyncTransactionQueue,          private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, +        private val dragAndDropController: DragAndDropController,          private val transitions: Transitions,          private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,          private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, @@ -95,8 +107,10 @@ class DesktopTasksController(          private val desktopModeTaskRepository: DesktopModeTaskRepository,          private val launchAdjacentController: LaunchAdjacentController,          private val recentsTransitionHandler: RecentsTransitionHandler, +        private val multiInstanceHelper: MultiInstanceHelper,          @ShellMainThread private val mainExecutor: ShellExecutor -) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { +) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, +    DragAndDropController.DragAndDropListener {      private val desktopMode: DesktopModeImpl      private var visualIndicator: DesktopModeVisualIndicator? = null @@ -173,6 +187,7 @@ class DesktopTasksController(                  }              }          ) +        dragAndDropController.addListener(this)      }      fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { @@ -240,6 +255,42 @@ class DesktopTasksController(          return desktopModeTaskRepository.getVisibleTaskCount(displayId)      } +    /** Enter desktop by using the focused task in given `displayId` */ +    fun enterDesktop(displayId: Int) { +        val allFocusedTasks = +            shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> +                taskInfo.isFocused && +                        (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || +                                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && +                        taskInfo.activityType != ACTIVITY_TYPE_HOME +            } +        if (allFocusedTasks.isNotEmpty()) { +            when (allFocusedTasks.size) { +                2 -> { +                    // Split-screen case where there are two focused tasks, then we find the child +                    // task to move to desktop. +                    val splitFocusedTask = +                        if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) +                            allFocusedTasks[1] +                        else allFocusedTasks[0] +                    moveToDesktop(splitFocusedTask) +                } +                1 -> { +                    // Fullscreen case where we move the current focused task. +                    moveToDesktop(allFocusedTasks[0].taskId) +                } +                else -> { +                    KtProtoLog.w( +                        WM_SHELL_DESKTOP_MODE, +                        "DesktopTasksController: Cannot enter desktop, expected less " + +                                "than 3 focused tasks but found %d", +                        allFocusedTasks.size +                    ) +                } +            } +        } +    } +      /** Move a task with given `taskId` to desktop */      fun moveToDesktop(              taskId: Int, @@ -893,7 +944,7 @@ class DesktopTasksController(          }          // Then, update the indicator type.          val indicator = visualIndicator ?: return -        indicator.updateIndicatorType(PointF(inputX, taskTop)) +        indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode)      }      /** @@ -986,6 +1037,50 @@ class DesktopTasksController(          desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor)      } +    override fun onUnhandledDrag( +        launchIntent: PendingIntent, +        dragSurface: SurfaceControl, +        onFinishCallback: Consumer<Boolean> +    ): Boolean { +        // TODO(b/320797628): Pass through which display we are dropping onto +        val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY) +        if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { +            // Not currently in desktop mode, ignore the drop +            return false +        } + +        val launchComponent = getComponent(launchIntent) +        if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) { +            // TODO(b/320797628): Should only return early if there is an existing running task, and +            //                    notify the user as well. But for now, just ignore the drop. +            KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "Dropped intent does not support multi-instance") +            return false +        } + +        // Start a new transition to launch the app +        val opts = ActivityOptions.makeBasic().apply { +            launchWindowingMode = WINDOWING_MODE_FREEFORM +            pendingIntentLaunchFlags = +                Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK +            setPendingIntentBackgroundActivityStartMode( +                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) +            isPendingIntentBackgroundActivityLaunchAllowedByPermission = true +        } +        val wct = WindowContainerTransaction() +        wct.sendPendingIntent(launchIntent, null, opts.toBundle()) +        transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) + +        // Report that this is handled by the listener +        onFinishCallback.accept(true) + +        // We've assumed responsibility of cleaning up the drag surface, so do that now +        // TODO(b/320797628): Do an actual animation here for the drag surface +        val t = SurfaceControl.Transaction() +        t.remove(dragSurface) +        t.apply() +        return true +    } +      private fun dump(pw: PrintWriter, prefix: String) {          val innerPrefix = "$prefix  "          pw.println("${prefix}DesktopTasksController") @@ -1012,6 +1107,12 @@ class DesktopTasksController(                  this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor)              }          } + +        override fun enterDesktop(displayId: Int) { +            mainExecutor.execute { +                this@DesktopTasksController.enterDesktop(displayId) +            } +        }      }      /** The interface for calls from outside the host process. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 269c3699ac0a..1afbdf90eac0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -35,7 +35,9 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;  import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;  import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; +import android.app.ActivityManager;  import android.app.ActivityTaskManager; +import android.app.PendingIntent;  import android.content.ClipDescription;  import android.content.ComponentCallbacks2;  import android.content.Context; @@ -51,6 +53,7 @@ import android.view.View;  import android.view.ViewGroup;  import android.view.WindowManager;  import android.widget.FrameLayout; +import android.window.WindowContainerTransaction;  import androidx.annotation.BinderThread;  import androidx.annotation.NonNull; @@ -71,14 +74,18 @@ import com.android.wm.shell.splitscreen.SplitScreenController;  import com.android.wm.shell.sysui.ShellCommandHandler;  import com.android.wm.shell.sysui.ShellController;  import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions;  import java.io.PrintWriter;  import java.util.ArrayList; +import java.util.function.Consumer; +import java.util.function.Function;  /**   * Handles the global drag and drop handling for the Shell.   */  public class DragAndDropController implements RemoteCallable<DragAndDropController>, +        GlobalDragListener.GlobalDragListenerCallback,          DisplayController.OnDisplaysChangedListener,          View.OnDragListener, ComponentCallbacks2 { @@ -90,6 +97,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll      private final DisplayController mDisplayController;      private final DragAndDropEventLogger mLogger;      private final IconProvider mIconProvider; +    private final GlobalDragListener mGlobalDragListener; +    private final Transitions mTransitions;      private SplitScreenController mSplitScreen;      private ShellExecutor mMainExecutor;      private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); @@ -97,12 +106,29 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll      // Map of displayId -> per-display info      private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); +    // The current display if a drag is in progress +    private int mActiveDragDisplay = -1; +      /** -     * Listener called during drag events, currently just onDragStarted. +     * Listener called during drag events.       */      public interface DragAndDropListener {          /** Called when a drag has started. */ -        void onDragStarted(); +        default void onDragStarted() {} + +        /** Called when a drag has ended. */ +        default void onDragEnded() {} + +        /** +         * Called when an unhandled drag has occurred. The impl must return true if it decides to +         * handled the unhandled drag, and it must also call `onFinishCallback` to complete the +         * drag. +         */ +        default boolean onUnhandledDrag(@NonNull PendingIntent launchIntent, +                @NonNull SurfaceControl dragSurface, +                @NonNull Consumer<Boolean> onFinishCallback) { +            return false; +        }      }      public DragAndDropController(Context context, @@ -112,6 +138,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll              DisplayController displayController,              UiEventLogger uiEventLogger,              IconProvider iconProvider, +            GlobalDragListener globalDragListener, +            Transitions transitions,              ShellExecutor mainExecutor) {          mContext = context;          mShellController = shellController; @@ -119,6 +147,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll          mDisplayController = displayController;          mLogger = new DragAndDropEventLogger(uiEventLogger);          mIconProvider = iconProvider; +        mGlobalDragListener = globalDragListener; +        mTransitions = transitions;          mMainExecutor = mainExecutor;          shellInit.addInitCallback(this::onInit, this);      } @@ -136,6 +166,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll          mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP,                  this::createExternalInterface, this);          mShellCommandHandler.addDumpCallback(this::dump, this); +        mGlobalDragListener.setListener(this);      }      private ExternalInterfaceBinder createExternalInterface() { @@ -169,10 +200,18 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll          mListeners.remove(listener);      } -    private void notifyDragStarted() { +    /** +     * Notifies all listeners and returns whether any listener handled the callback. +     */ +    private boolean notifyListeners(Function<DragAndDropListener, Boolean> callback) {          for (int i = 0; i < mListeners.size(); i++) { -            mListeners.get(i).onDragStarted(); +            boolean handled = callback.apply(mListeners.get(i)); +            if (handled) { +                // Return once the callback reports it has handled it +                return true; +            }          } +        return false;      }      @Override @@ -258,6 +297,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll          }          if (event.getAction() == ACTION_DRAG_STARTED) { +            mActiveDragDisplay = displayId;              pd.isHandlingDrag = DragUtils.canHandleDrag(event);              ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,                      "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", @@ -283,7 +323,11 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll                  pd.dragSession.update();                  pd.dragLayout.prepare(pd.dragSession, loggerSessionId);                  setDropTargetWindowVisibility(pd, View.VISIBLE); -                notifyDragStarted(); +                notifyListeners(l -> { +                    l.onDragStarted(); +                    // Return false to continue dispatch to next listener +                    return false; +                });                  break;              case ACTION_DRAG_ENTERED:                  pd.dragLayout.show(); @@ -317,11 +361,43 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll                      });                  }                  mLogger.logEnd(); +                mActiveDragDisplay = -1; +                notifyListeners(l -> { +                    l.onDragEnded(); +                    // Return false to continue dispatch to next listener +                    return false; +                });                  break;          }          return true;      } +    @Override +    public void onCrossWindowDrop(@NonNull ActivityManager.RunningTaskInfo taskInfo) { +        // Bring the task forward when an item is dropped on it +        final WindowContainerTransaction wct = new WindowContainerTransaction(); +        wct.reorder(taskInfo.token, true /* onTop */); +        mTransitions.startTransition(WindowManager.TRANSIT_TO_FRONT, wct, null); +    } + +    @Override +    public void onUnhandledDrop(@NonNull DragEvent dragEvent, +            @NonNull Consumer<Boolean> onFinishCallback) { +        final PendingIntent launchIntent = DragUtils.getLaunchIntent(dragEvent); +        if (launchIntent == null) { +            // No intent to launch, report that this is unhandled by the listener +            onFinishCallback.accept(false); +            return; +        } + +        final boolean handled = notifyListeners( +                l -> l.onUnhandledDrag(launchIntent, dragEvent.getDragSurface(), onFinishCallback)); +        if (!handled) { +            // Nobody handled this, we still have to notify WM +            onFinishCallback.accept(false); +        } +    } +      /**       * Handles dropping on the drop target.       */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index 7c0883d2538f..f7bcc9477aa1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -20,9 +20,14 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;  import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;  import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import android.app.PendingIntent; +import android.content.ClipData;  import android.content.ClipDescription;  import android.view.DragEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +  /** Collection of utility classes for handling drag and drop. */  public class DragUtils {      private static final String TAG = "DragUtils"; @@ -45,6 +50,31 @@ public class DragUtils {      }      /** +     * Returns a launchable intent in the given `DragEvent` or `null` if there is none. +     */ +    @Nullable +    public static PendingIntent getLaunchIntent(@NonNull DragEvent dragEvent) { +        return getLaunchIntent(dragEvent.getClipData()); +    } + +    /** +     * Returns a launchable intent in the given `ClipData` or `null` if there is none. +     */ +    @Nullable +    public static PendingIntent getLaunchIntent(@NonNull ClipData data) { +        for (int i = 0; i < data.getItemCount(); i++) { +            final ClipData.Item item = data.getItemAt(i); +            if (item.getIntentSender() != null) { +                final PendingIntent intent = new PendingIntent(item.getIntentSender().getTarget()); +                if (intent != null && intent.isActivity()) { +                    return intent; +                } +            } +        } +        return null; +    } + +    /**       * Returns a list of the mime types provided in the clip description.       */      public static String getMimeTypesConcatenated(ClipDescription description) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt index ccf48d0de9ed..8826141fb406 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt @@ -15,12 +15,13 @@   */  package com.android.wm.shell.draganddrop +import android.app.ActivityManager  import android.os.RemoteException  import android.util.Log  import android.view.DragEvent  import android.view.IWindowManager +import android.window.IGlobalDragListener  import android.window.IUnhandledDragCallback -import android.window.IUnhandledDragListener  import androidx.annotation.VisibleForTesting  import com.android.internal.protolog.common.ProtoLog  import com.android.wm.shell.common.ShellExecutor @@ -29,26 +30,38 @@ import java.util.function.Consumer  /**   * Manages the listener and callbacks for unhandled global drags. + * This is only used by DragAndDropController and should not be used directly by other classes.   */ -class UnhandledDragController( -    val wmService: IWindowManager, -    mainExecutor: ShellExecutor +class GlobalDragListener( +    private val wmService: IWindowManager, +    private val mainExecutor: ShellExecutor  ) { -    private var callback: UnhandledDragAndDropCallback? = null +    private var callback: GlobalDragListenerCallback? = null + +    private val globalDragListener: IGlobalDragListener = +        object : IGlobalDragListener.Stub() { +            override fun onCrossWindowDrop(taskInfo: ActivityManager.RunningTaskInfo) { +                mainExecutor.execute() { +                    this@GlobalDragListener.onCrossWindowDrop(taskInfo) +                } +            } -    private val unhandledDragListener: IUnhandledDragListener = -        object : IUnhandledDragListener.Stub() {              override fun onUnhandledDrop(event: DragEvent, callback: IUnhandledDragCallback) {                  mainExecutor.execute() { -                    this@UnhandledDragController.onUnhandledDrop(event, callback) +                    this@GlobalDragListener.onUnhandledDrop(event, callback)                  }              }          }      /** -     * Listener called when an unhandled drag is started. +     * Callbacks for global drag events.       */ -    interface UnhandledDragAndDropCallback { +    interface GlobalDragListenerCallback { +        /** +         * Called when a global drag is successfully handled by another window. +         */ +        fun onCrossWindowDrop(taskInfo: ActivityManager.RunningTaskInfo) {} +          /**           * Called when a global drag is unhandled (ie. dropped outside of all visible windows, or           * dropped on a window that does not want to handle it). @@ -62,7 +75,7 @@ class UnhandledDragController(      /**       * Sets a listener for callbacks when an unhandled drag happens.       */ -    fun setListener(listener: UnhandledDragAndDropCallback?) { +    fun setListener(listener: GlobalDragListenerCallback?) {          val updateWm = (callback == null && listener != null)                  || (callback != null && listener == null)          callback = listener @@ -71,8 +84,8 @@ class UnhandledDragController(                  ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,                      "%s unhandled drag listener",                      if (callback != null) "Registering" else "Unregistering") -                wmService.setUnhandledDragListener( -                    if (callback != null) unhandledDragListener else null) +                wmService.setGlobalDragListener( +                    if (callback != null) globalDragListener else null)              } catch (e: RemoteException) {                  Log.e(TAG, "Failed to set unhandled drag listener")              } @@ -80,11 +93,19 @@ class UnhandledDragController(      }      @VisibleForTesting +    fun onCrossWindowDrop(taskInfo: ActivityManager.RunningTaskInfo) { +        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, +            "onCrossWindowDrop: %s", taskInfo) +        callback?.onCrossWindowDrop(taskInfo) +    } + +    @VisibleForTesting      fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) {          ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,              "onUnhandledDrop: %s", dragEvent)          if (callback == null) {              wmCallback.notifyUnhandledDropComplete(false) +            return          }          callback?.onUnhandledDrop(dragEvent) { @@ -95,6 +116,6 @@ class UnhandledDragController(      }      companion object { -        private val TAG = UnhandledDragController::class.java.simpleName +        private val TAG = GlobalDragListener::class.java.simpleName      }  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index d17317522694..32442f740a52 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -191,7 +191,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH              try {                  ActivityTaskManager.getService().onPictureInPictureUiStateChanged(                          new PictureInPictureUiState.Builder() -                                .setEnteringPip(true) +                                .setTransitioningToPip(true)                                  .build());              } catch (RemoteException | IllegalStateException e) {                  ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -210,7 +210,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH              try {                  ActivityTaskManager.getService().onPictureInPictureUiStateChanged(                          new PictureInPictureUiState.Builder() -                                .setEnteringPip(false) +                                .setTransitioningToPip(false)                                  .build());              } catch (RemoteException | IllegalStateException e) {                  ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 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 4b12134cb377..46840773cfc6 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 @@ -48,7 +48,6 @@ import android.graphics.Rect;  import android.os.RemoteException;  import android.os.SystemProperties;  import android.util.Pair; -import android.util.Size;  import android.view.DisplayInfo;  import android.view.InsetsState;  import android.view.SurfaceControl; @@ -1042,22 +1041,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb      /** Save the state to restore to on re-entry. */      public void saveReentryState(Rect pipBounds) {          float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds); - -        if (!mPipBoundsState.hasUserResizedPip()) { -            mPipBoundsState.saveReentryState(null /* bounds */, snapFraction); -            return; -        } - -        Size reentrySize = new Size(pipBounds.width(), pipBounds.height()); - -        // TODO: b/279937014 Investigate why userResizeBounds are empty with shell transitions on -        // fallback to using the userResizeBounds if userResizeBounds are not empty -        if (!mTouchHandler.getUserResizeBounds().isEmpty()) { -            Rect userResizeBounds = mTouchHandler.getUserResizeBounds(); -            reentrySize = new Size(userResizeBounds.width(), userResizeBounds.height()); -        } - -        mPipBoundsState.saveReentryState(reentrySize, snapFraction); +        mPipBoundsState.saveReentryState(snapFraction);      }      @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 186cb615f4ec..e73a85003881 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -18,14 +18,31 @@ package com.android.wm.shell.pip2.phone;  import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; + +import android.app.PictureInPictureParams; +import android.content.ComponentName;  import android.content.Context; +import android.content.pm.ActivityInfo;  import android.content.res.Configuration; +import android.graphics.Rect;  import android.view.InsetsState; +import android.view.SurfaceControl; + +import androidx.annotation.BinderThread;  import com.android.internal.protolog.common.ProtoLog;  import com.android.wm.shell.common.DisplayController;  import com.android.wm.shell.common.DisplayInsetsController;  import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ExternalInterfaceBinder; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.IPip; +import com.android.wm.shell.common.pip.IPipAnimationListener; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState;  import com.android.wm.shell.common.pip.PipDisplayLayoutState;  import com.android.wm.shell.common.pip.PipUtils;  import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -37,32 +54,54 @@ import com.android.wm.shell.sysui.ShellInit;   * Manages the picture-in-picture (PIP) UI and states for Phones.   */  public class PipController implements ConfigurationChangeListener, -        DisplayController.OnDisplaysChangedListener { +        DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> {      private static final String TAG = PipController.class.getSimpleName();      private Context mContext;      private ShellController mShellController;      private DisplayController mDisplayController;      private DisplayInsetsController mDisplayInsetsController; +    private PipBoundsState mPipBoundsState; +    private PipBoundsAlgorithm mPipBoundsAlgorithm;      private PipDisplayLayoutState mPipDisplayLayoutState; +    private PipScheduler mPipScheduler; +    private ShellExecutor mMainExecutor;      private PipController(Context context,              ShellInit shellInit,              ShellController shellController,              DisplayController displayController,              DisplayInsetsController displayInsetsController, -            PipDisplayLayoutState pipDisplayLayoutState) { +            PipBoundsState pipBoundsState, +            PipBoundsAlgorithm pipBoundsAlgorithm, +            PipDisplayLayoutState pipDisplayLayoutState, +            PipScheduler pipScheduler, +            ShellExecutor mainExecutor) {          mContext = context;          mShellController = shellController;          mDisplayController = displayController;          mDisplayInsetsController = displayInsetsController; +        mPipBoundsState = pipBoundsState; +        mPipBoundsAlgorithm = pipBoundsAlgorithm;          mPipDisplayLayoutState = pipDisplayLayoutState; +        mPipScheduler = pipScheduler; +        mMainExecutor = mainExecutor;          if (PipUtils.isPip2ExperimentEnabled()) {              shellInit.addInitCallback(this::onInit, this);          }      } +    @Override +    public Context getContext() { +        return mContext; +    } + +    @Override +    public ShellExecutor getRemoteCallExecutor() { +        return mMainExecutor; +    } +      private void onInit() {          // Ensure that we have the display info in case we get calls to update the bounds before the          // listener calls back @@ -80,6 +119,10 @@ public class PipController implements ConfigurationChangeListener,                                          .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));                      }                  }); + +        // Allow other outside processes to bind to PiP controller using the key below. +        mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, +                this::createExternalInterface, this);      }      /** @@ -90,16 +133,24 @@ public class PipController implements ConfigurationChangeListener,              ShellController shellController,              DisplayController displayController,              DisplayInsetsController displayInsetsController, -            PipDisplayLayoutState pipDisplayLayoutState) { +            PipBoundsState pipBoundsState, +            PipBoundsAlgorithm pipBoundsAlgorithm, +            PipDisplayLayoutState pipDisplayLayoutState, +            PipScheduler pipScheduler, +            ShellExecutor mainExecutor) {          if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {              ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,                      "%s: Device doesn't support Pip feature", TAG);              return null;          }          return new PipController(context, shellInit, shellController, displayController, -                displayInsetsController, pipDisplayLayoutState); +                displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, +                pipScheduler, mainExecutor);      } +    private ExternalInterfaceBinder createExternalInterface() { +        return new IPipImpl(this); +    }      @Override      public void onConfigurationChanged(Configuration newConfiguration) { @@ -130,4 +181,86 @@ public class PipController implements ConfigurationChangeListener,      private void onDisplayChanged(DisplayLayout layout) {          mPipDisplayLayoutState.setDisplayLayout(layout);      } + +    private Rect getSwipePipToHomeBounds(ComponentName componentName, ActivityInfo activityInfo, +            PictureInPictureParams pictureInPictureParams, +            int launcherRotation, Rect hotseatKeepClearArea) { +        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, +                "getSwipePipToHomeBounds: %s", componentName); +        mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams, +                mPipBoundsAlgorithm); +        return mPipBoundsAlgorithm.getEntryDestinationBounds(); +    } + +    private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, +            Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { +        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, +                "onSwipePipToHomeAnimationStart: %s", componentName); +        mPipScheduler.setInSwipePipToHomeTransition(true); +        // TODO: cache the overlay if provided for reparenting later. +    } + +    /** +     * The interface for calls from outside the host process. +     */ +    @BinderThread +    private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder { +        private PipController mController; + +        IPipImpl(PipController controller) { +            mController = controller; +        } + +        /** +         * Invalidates this instance, preventing future calls from updating the controller. +         */ +        @Override +        public void invalidate() { +            mController = null; +        } + +        @Override +        public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, +                PictureInPictureParams pictureInPictureParams, int launcherRotation, +                Rect keepClearArea) { +            Rect[] result = new Rect[1]; +            executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome", +                    (controller) -> { +                        result[0] = controller.getSwipePipToHomeBounds(componentName, activityInfo, +                                pictureInPictureParams, launcherRotation, keepClearArea); +                    }, true /* blocking */); +            return result[0]; +        } + +        @Override +        public void stopSwipePipToHome(int taskId, ComponentName componentName, +                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { +            if (overlay != null) { +                overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); +            } +            executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", +                    (controller) -> controller.onSwipePipToHomeAnimationStart( +                            taskId, componentName, destinationBounds, overlay, appBounds)); +        } + +        @Override +        public void abortSwipePipToHome(int taskId, ComponentName componentName) {} + +        @Override +        public void setShelfHeight(boolean visible, int height) {} + +        @Override +        public void setLauncherKeepClearAreaHeight(boolean visible, int height) {} + +        @Override +        public void setLauncherAppIconSize(int iconSizePx) {} + +        @Override +        public void setPipAnimationListener(IPipAnimationListener listener) { +            // TODO: set a proper animation listener to update the Launcher state as needed. +        } + +        @Override +        public void setPipAnimationTypeToAlpha() {} +    }  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 57b73b3019f4..895c793007a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -63,6 +63,9 @@ public class PipScheduler {      @Nullable      private SurfaceControl mPinnedTaskLeash; +    // true if Launcher has started swipe PiP to home animation +    private boolean mInSwipePipToHomeTransition; +      /**       * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell.       * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -168,6 +171,14 @@ public class PipScheduler {          mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback);      } +    void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { +        mInSwipePipToHomeTransition = true; +    } + +    boolean isInSwipePipToHomeTransition() { +        return mInSwipePipToHomeTransition; +    } +      void onExitPip() {          mPipTaskToken = null;          mPinnedTaskLeash = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index fbf4d13a0c19..dfb04758c851 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -152,6 +152,12 @@ public class PipTransition extends PipTransitionController {              @NonNull Transitions.TransitionFinishCallback finishCallback) {          if (transition == mEnterTransition) {              mEnterTransition = null; +            if (mPipScheduler.isInSwipePipToHomeTransition()) { +                // If this is the second transition as a part of swipe PiP to home cuj, +                // handle this transition as a special case with no-op animation. +                return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction, +                        finishCallback); +            }              if (isLegacyEnter(info)) {                  // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause),                  // then we should run an ALPHA type (cross-fade) animation. @@ -207,6 +213,25 @@ public class PipTransition extends PipTransitionController {          return true;      } +    private boolean handleSwipePipToHomeTransition(@NonNull TransitionInfo info, +            @NonNull SurfaceControl.Transaction startTransaction, +            @NonNull SurfaceControl.Transaction finishTransaction, +            @NonNull Transitions.TransitionFinishCallback finishCallback) { +        TransitionInfo.Change pipChange = getPipChange(info); +        if (pipChange == null) { +            return false; +        } +        mPipScheduler.setInSwipePipToHomeTransition(false); +        mPipTaskToken = pipChange.getContainer(); + +        // cache the PiP task token and leash +        mPipScheduler.setPipTaskToken(mPipTaskToken); + +        startTransaction.apply(); +        finishCallback.onTransitionFinished(null); +        return true; +    } +      private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,              @NonNull SurfaceControl.Transaction startTransaction,              @NonNull SurfaceControl.Transaction finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 5e79681e060b..b8a0f6703b97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -493,11 +493,9 @@ public class Transitions implements RemoteCallable<Transitions>,              final SurfaceControl leash = change.getLeash();              final int mode = info.getChanges().get(i).getMode(); -            if (mode == TRANSIT_TO_FRONT -                    && ((change.getStartAbsBounds().height() != change.getEndAbsBounds().height() -                    || change.getStartAbsBounds().width() != change.getEndAbsBounds().width()))) { -                // When the window is moved to front with a different size, make sure the crop is -                // updated to prevent it from using the old crop. +            if (mode == TRANSIT_TO_FRONT) { +                // When the window is moved to front, make sure the crop is updated to prevent it +                // from using the old crop.                  t.setWindowCrop(leash, change.getEndAbsBounds().width(),                          change.getEndAbsBounds().height());              } @@ -1170,7 +1168,11 @@ public class Transitions implements RemoteCallable<Transitions>,          mPendingTransitions.add(0, active);      } -    /** Start a new transition directly. */ +    /** +     * Start a new transition directly. +     * @param handler if null, the transition will be dispatched to the registered set of transition +     *                handlers to be handled +     */      public IBinder startTransition(@WindowManager.TransitionType int type,              @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {          ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition " diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index c713a2ed24b0..a2d962d065c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -728,7 +728,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {                      mTransitionDragActive = false;                      final int statusBarHeight = getStatusBarHeight(                              relevantDecor.mTaskInfo.displayId); -                    if (ev.getY() > 2 * statusBarHeight) { +                    if (ev.getRawY() > 2 * statusBarHeight) {                          if (DesktopModeStatus.isEnabled()) {                              animateToDesktop(relevantDecor, ev);                          } @@ -753,10 +753,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {                      mDesktopTasksController.ifPresent(                              c -> c.updateVisualIndicator(                                      relevantDecor.mTaskInfo, -                                    relevantDecor.mTaskSurface, ev.getX(), ev.getY())); +                                    relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()));                      final int statusBarHeight = getStatusBarHeight(                              relevantDecor.mTaskInfo.displayId); -                    if (ev.getY() > statusBarHeight) { +                    if (ev.getRawY() > statusBarHeight) {                          if (mMoveToDesktopAnimator == null) {                              mMoveToDesktopAnimator = new MoveToDesktopAnimator(                                      mContext, mDragToDesktopAnimationStartBounds, diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt new file mode 100644 index 000000000000..8c0a524cda1e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.LegacyFlickerTest +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test auto entering pip using a source rect hint. + * + * To run this test: `atest AutoEnterPipWithSourceRectHintTest` + * + * Actions: + * ``` + *     Launch an app in full screen + *     Select "Auto-enter PiP" radio button + *     Press "Set SourceRectHint" to create a temporary view that is used as the source rect hint + *     Press Home button or swipe up to go Home and put [pipApp] in pip mode + * ``` + * + * Notes: + * ``` + *     1. All assertions are inherited from [AutoEnterPipOnGoToHomeTest] + *     2. Part of the test setup occurs automatically via + *        [android.tools.device.flicker.legacy.runner.TransitionRunner], + *        including configuring navigation mode, initial orientation and ensuring no + *        apps are running before setup + * ``` + */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AutoEnterPipWithSourceRectHintTest(flicker: LegacyFlickerTest) : +    AutoEnterPipOnGoToHomeTest(flicker) { +    override val defaultEnterPip: FlickerBuilder.() -> Unit = { +        setup { +            pipApp.launchViaIntent(wmHelper) +            pipApp.setSourceRectHint() +            pipApp.enableAutoEnterForPipActivity() +        } +    } + +    @Presubmit +    @Test +    fun pipOverlayNotShown() { +        val overlay = ComponentNameMatcher.PIP_CONTENT_OVERLAY +        flicker.assertLayers { +            this.notContains(overlay) +        } +    } +    @Presubmit +    @Test +    override fun pipOverlayLayerAppearThenDisappear() { +        // we don't use overlay when entering with sourceRectHint +    } + +    @Presubmit +    @Test +    override fun pipLayerOrOverlayRemainInsideVisibleBounds() { +        // TODO (b/323511194): Looks like there is some bounciness with pip when using +        // auto enter and sourceRectHint that causes the app to move outside of the display +        // bounds during the transition. +    } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml index 47a116be1b66..2ef425cf3d41 100644 --- a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml @@ -21,6 +21,7 @@      <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />      <uses-permission android:name="android.permission.VIBRATE"/> +    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>      <application android:debuggable="true" android:largeHeap="true">          <uses-library android:name="android.test.mock" /> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 9fe2cb11e804..81ba4b37d13b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -113,8 +113,22 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {          mExecutor = new TestShellExecutor();          mTaskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */                  false, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER); +        final DisplayInfo displayInfo = new DisplayInfo(); +        final int displayWidth = 1000; +        final int displayHeight = 1200; +        displayInfo.logicalWidth = displayWidth; +        displayInfo.logicalHeight = displayHeight; +        final DisplayLayout displayLayout = new DisplayLayout(displayInfo, +                mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false); +        InsetsState insetsState = new InsetsState(); +        insetsState.setDisplayFrame(new Rect(0, 0, displayWidth, displayHeight)); +        InsetsSource insetsSource = new InsetsSource( +                InsetsSource.createId(null, 0, navigationBars()), navigationBars()); +        insetsSource.setFrame(0, displayHeight - 200, displayWidth, displayHeight); +        insetsState.addSource(insetsSource); +        displayLayout.setInsets(mContext.getResources(), insetsState);          mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, -                mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), +                mSyncTransactionQueue, mTaskListener, displayLayout, new CompatUIHintsState(),                  mOnUserAspectRatioSettingsButtonClicked, mExecutor, flags -> 0,                  mUserAspectRatioButtonShownChecker, s -> {});          spyOn(mWindowManager); @@ -253,6 +267,31 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {      }      @Test +    public void testEligibleButtonHiddenIfLetterboxBoundsEqualToStableBounds() { +        TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ +                true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER); + +        final Rect stableBounds = mWindowManager.getTaskStableBounds(); +        final int stableHeight = stableBounds.height(); + +        // Letterboxed activity bounds equal to stable bounds, layout shouldn't be inflated +        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableHeight; +        taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width(); + +        mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + +        verify(mWindowManager, never()).inflateLayout(); + +        // Letterboxed activity bounds smaller than stable bounds, layout should be inflated +        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableHeight - 100; + +        clearInvocations(mWindowManager); +        mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + +        verify(mWindowManager).inflateLayout(); +    } + +    @Test      public void testUpdateDisplayLayout() {          final DisplayInfo displayInfo = new DisplayInfo();          displayInfo.logicalWidth = 1000; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt new file mode 100644 index 000000000000..f8ce4ee8e1ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.graphics.Rect +import android.graphics.Region +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.SyncTransactionQueue +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeVisualIndicatorTest : ShellTestCase() { +    @Mock private lateinit var taskInfo: RunningTaskInfo +    @Mock private lateinit var syncQueue: SyncTransactionQueue +    @Mock private lateinit var displayController: DisplayController +    @Mock private lateinit var taskSurface: SurfaceControl +    @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +    @Mock private lateinit var displayLayout: DisplayLayout + +    private lateinit var visualIndicator: DesktopModeVisualIndicator + +    @Before +    fun setUp() { +        visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController, +            context, taskSurface, taskDisplayAreaOrganizer) +        whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) +        whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) +    } + +    @Test +    fun testFullscreenRegionCalculation() { +        val transitionHeight = context.resources.getDimensionPixelSize( +            R.dimen.desktop_mode_transition_area_height) +        val fromFreeformWidth = mContext.resources.getDimensionPixelSize( +            R.dimen.desktop_mode_fullscreen_from_desktop_width +        ) +        val fromFreeformHeight = mContext.resources.getDimensionPixelSize( +            R.dimen.desktop_mode_fullscreen_from_desktop_height +        ) +        var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) +        testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, +            WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect( +            DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, +            -50, +            DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, +            fromFreeformHeight)) +        testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, +            WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) +    } + +    @Test +    fun testSplitLeftRegionCalculation() { +        val transitionHeight = context.resources.getDimensionPixelSize( +            R.dimen.desktop_mode_split_from_desktop_height) +        var testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) +        testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, +            WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(0, transitionHeight, 32, 1600)) +        testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, +            WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) +    } + +    @Test +    fun testSplitRightRegionCalculation() { +        val transitionHeight = context.resources.getDimensionPixelSize( +            R.dimen.desktop_mode_split_from_desktop_height) +        var testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) +        testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, +            WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(2368, transitionHeight, 2400, 1600)) +        testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, +            WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) +    } + +    @Test +    fun testToDesktopRegionCalculation() { +        val fullscreenRegion = visualIndicator.calculateFullscreenRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) +        val splitLeftRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        val splitRightRegion = visualIndicator.calculateSplitRightRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) +        val desktopRegion = visualIndicator.calculateToDesktopRegion(displayLayout, +            WINDOWING_MODE_FULLSCREEN, splitLeftRegion, splitRightRegion, fullscreenRegion) +        var testRegion = Region() +        testRegion.union(DISPLAY_BOUNDS) +        testRegion.op(splitLeftRegion, Region.Op.DIFFERENCE) +        testRegion.op(splitRightRegion, Region.Op.DIFFERENCE) +        testRegion.op(fullscreenRegion, Region.Op.DIFFERENCE) +        assertThat(desktopRegion).isEqualTo(testRegion) +    } + +    companion object { +        private const val TRANSITION_AREA_WIDTH = 32 +        private const val CAPTION_HEIGHT = 50 +        private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) +    } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 63618f4d0673..383621beca22 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -48,12 +48,14 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder  import com.android.wm.shell.TestShellExecutor  import com.android.wm.shell.common.DisplayController  import com.android.wm.shell.common.LaunchAdjacentController +import com.android.wm.shell.common.MultiInstanceHelper  import com.android.wm.shell.common.ShellExecutor  import com.android.wm.shell.common.SyncTransactionQueue  import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask  import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask  import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask  import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask +import com.android.wm.shell.draganddrop.DragAndDropController  import com.android.wm.shell.recents.RecentsTransitionHandler  import com.android.wm.shell.recents.RecentsTransitionStateListener  import com.android.wm.shell.splitscreen.SplitScreenController @@ -106,6 +108,8 @@ class DesktopTasksControllerTest : ShellTestCase() {      @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration      @Mock lateinit var splitScreenController: SplitScreenController      @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler +    @Mock lateinit var dragAndDropController: DragAndDropController +    @Mock lateinit var multiInstanceHelper: MultiInstanceHelper      private lateinit var mockitoSession: StaticMockitoSession      private lateinit var controller: DesktopTasksController @@ -148,6 +152,7 @@ class DesktopTasksControllerTest : ShellTestCase() {              shellTaskOrganizer,              syncQueue,              rootTaskDisplayAreaOrganizer, +            dragAndDropController,              transitions,              enterDesktopTransitionHandler,              exitDesktopTransitionHandler, @@ -156,6 +161,7 @@ class DesktopTasksControllerTest : ShellTestCase() {              desktopModeTaskRepository,              launchAdjacentController,              recentsTransitionHandler, +            multiInstanceHelper,              shellExecutor          )      } @@ -734,6 +740,46 @@ class DesktopTasksControllerTest : ShellTestCase() {          shellExecutor.flushAll()          verify(launchAdjacentController).launchAdjacentEnabled = true      } +    @Test +    fun enterDesktop_fullscreenTaskIsMovedToDesktop() { +        val task1 = setUpFullscreenTask() +        val task2 = setUpFullscreenTask() +        val task3 = setUpFullscreenTask() + +        task1.isFocused = true +        task2.isFocused = false +        task3.isFocused = false + +        controller.enterDesktop(DEFAULT_DISPLAY) + +        val wct = getLatestMoveToDesktopWct() +        assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) +                .isEqualTo(WINDOWING_MODE_FREEFORM) +    } + +    @Test +    fun enterDesktop_splitScreenTaskIsMovedToDesktop() { +        val task1 = setUpSplitScreenTask() +        val task2 = setUpFullscreenTask() +        val task3 = setUpFullscreenTask() +        val task4 = setUpSplitScreenTask() + +        task1.isFocused = true +        task2.isFocused = false +        task3.isFocused = false +        task4.isFocused = true + +        task4.parentTaskId = task1.taskId + +        controller.enterDesktop(DEFAULT_DISPLAY) + +        val wct = getLatestMoveToDesktopWct() +        assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) +                .isEqualTo(WINDOWING_MODE_FREEFORM) +        verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(), +            eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) +        ) +    }      private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {          val task = createFreeformTask(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 54f36f61859d..a64ebd301c00 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -45,12 +45,14 @@ import androidx.test.filters.SmallTest;  import com.android.internal.logging.UiEventLogger;  import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.ShellTaskOrganizer;  import com.android.wm.shell.ShellTestCase;  import com.android.wm.shell.common.DisplayController;  import com.android.wm.shell.common.ShellExecutor;  import com.android.wm.shell.sysui.ShellCommandHandler;  import com.android.wm.shell.sysui.ShellController;  import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions;  import org.junit.Before;  import org.junit.Test; @@ -84,7 +86,9 @@ public class DragAndDropControllerTest extends ShellTestCase {      @Mock      private ShellExecutor mMainExecutor;      @Mock -    private WindowManager mWindowManager; +    private Transitions mTransitions; +    @Mock +    private GlobalDragListener mGlobalDragListener;      private DragAndDropController mController; @@ -93,7 +97,7 @@ public class DragAndDropControllerTest extends ShellTestCase {          MockitoAnnotations.initMocks(this);          mController = new DragAndDropController(mContext, mShellInit, mShellController,                  mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider, -                mMainExecutor); +                mGlobalDragListener, mTransitions, mMainExecutor);          mController.onInit();      } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt index 522f05233f3a..e731b06c0947 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt @@ -15,6 +15,7 @@   */  package com.android.wm.shell.draganddrop +import android.app.ActivityManager.RunningTaskInfo  import android.os.RemoteException  import android.view.DragEvent  import android.view.DragEvent.ACTION_DROP @@ -25,19 +26,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4  import androidx.test.filters.SmallTest  import com.android.wm.shell.ShellTestCase  import com.android.wm.shell.common.ShellExecutor -import com.android.wm.shell.draganddrop.UnhandledDragController.UnhandledDragAndDropCallback +import com.android.wm.shell.draganddrop.GlobalDragListener.GlobalDragListenerCallback  import java.util.function.Consumer  import junit.framework.Assert.assertEquals  import org.junit.Before  import org.junit.Test  import org.junit.runner.RunWith  import org.mockito.ArgumentMatchers -import org.mockito.Mock  import org.mockito.Mockito -import org.mockito.Mockito.mock -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify  /**   * Tests for the unhandled drag controller. @@ -45,35 +44,31 @@ import org.mockito.MockitoAnnotations  @SmallTest  @RunWith(AndroidJUnit4::class)  class UnhandledDragControllerTest : ShellTestCase() { -    @Mock -    private lateinit var mIWindowManager: IWindowManager +    private val mIWindowManager = mock<IWindowManager>() +    private val mMainExecutor = mock<ShellExecutor>() -    @Mock -    private lateinit var mMainExecutor: ShellExecutor - -    private lateinit var mController: UnhandledDragController +    private lateinit var mController: GlobalDragListener      @Before      @Throws(RemoteException::class)      fun setUp() { -        MockitoAnnotations.initMocks(this) -        mController = UnhandledDragController(mIWindowManager, mMainExecutor) +        mController = GlobalDragListener(mIWindowManager, mMainExecutor)      }      @Test      fun setListener_registersUnregistersWithWM() { -        mController.setListener(object : UnhandledDragAndDropCallback {}) -        mController.setListener(object : UnhandledDragAndDropCallback {}) -        mController.setListener(object : UnhandledDragAndDropCallback {}) +        mController.setListener(object : GlobalDragListenerCallback {}) +        mController.setListener(object : GlobalDragListenerCallback {}) +        mController.setListener(object : GlobalDragListenerCallback {})          verify(mIWindowManager, Mockito.times(1)) -                .setUnhandledDragListener(ArgumentMatchers.any()) +                .setGlobalDragListener(ArgumentMatchers.any())          reset(mIWindowManager)          mController.setListener(null)          mController.setListener(null)          mController.setListener(null)          verify(mIWindowManager, Mockito.times(1)) -                .setUnhandledDragListener(ArgumentMatchers.isNull()) +                .setGlobalDragListener(ArgumentMatchers.isNull())      }      @Test @@ -81,7 +76,7 @@ class UnhandledDragControllerTest : ShellTestCase() {          // Simulate an unhandled drop          val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,              null, null, false) -        val wmCallback = mock(IUnhandledDragCallback::class.java) +        val wmCallback = mock<IUnhandledDragCallback>()          mController.onUnhandledDrop(dropEvent, wmCallback)          verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(false)) @@ -92,7 +87,7 @@ class UnhandledDragControllerTest : ShellTestCase() {          val lastDragEvent = arrayOfNulls<DragEvent>(1)          // Set a listener to listen for unhandled drops -        mController.setListener(object : UnhandledDragAndDropCallback { +        mController.setListener(object : GlobalDragListenerCallback {              override fun onUnhandledDrop(dragEvent: DragEvent,                  onFinishedCallback: Consumer<Boolean>) {                  lastDragEvent[0] = dragEvent @@ -102,14 +97,31 @@ class UnhandledDragControllerTest : ShellTestCase() {          })          // Simulate an unhandled drop -        val dragSurface = mock(SurfaceControl::class.java) +        val dragSurface = mock<SurfaceControl>()          val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,              dragSurface, null, false) -        val wmCallback = mock(IUnhandledDragCallback::class.java) +        val wmCallback = mock<IUnhandledDragCallback>()          mController.onUnhandledDrop(dropEvent, wmCallback)          verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(true))          verify(dragSurface).release()          assertEquals(lastDragEvent.get(0), dropEvent)      } + +    @Test +    fun onCrossWindowDrop() { +        val lastTaskInfo = arrayOfNulls<RunningTaskInfo>(1) + +        // Set a listener to listen for unhandled drops +        mController.setListener(object : GlobalDragListenerCallback { +            override fun onCrossWindowDrop(taskInfo: RunningTaskInfo) { +                lastTaskInfo[0] = taskInfo +            } +        }) + +        // Simulate a cross-window drop +        val taskInfo = mock<RunningTaskInfo>() +        mController.onCrossWindowDrop(taskInfo) +        assertEquals(lastTaskInfo.get(0), taskInfo) +    }  } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index 46259a8b177f..080b0ae006ea 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -354,14 +354,17 @@ public class PipBoundsAlgorithmTest extends ShellTestCase {      }      @Test -    public void getEntryDestinationBounds_reentryStateExists_restoreLastSize() { +    public void getEntryDestinationBounds_reentryStateExists_restoreProportionalSize() {          mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); +        final Size maxSize = mSizeSpecSource.getMaxSize(DEFAULT_ASPECT_RATIO); +        mPipBoundsState.setMaxSize(maxSize.getWidth(), maxSize.getHeight());          final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();          reentryBounds.scale(1.25f); +        mPipBoundsState.setBounds(reentryBounds); // this updates the bounds scale used in reentry +          final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); -        mPipBoundsState.saveReentryState( -                new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); +        mPipBoundsState.saveReentryState(reentrySnapFraction);          final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();          assertEquals(reentryBounds.width(), destinationBounds.width()); @@ -375,8 +378,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase {          reentryBounds.offset(0, -100);          final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); -        mPipBoundsState.saveReentryState( -                new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); +        mPipBoundsState.saveReentryState(reentrySnapFraction);          final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java index db98abbbcbf1..304da75f870c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -114,22 +114,19 @@ public class PipBoundsStateTest extends ShellTestCase {      @Test      public void testSetReentryState() { -        final Size size = new Size(100, 100);          final float snapFraction = 0.5f; -        mPipBoundsState.saveReentryState(size, snapFraction); +        mPipBoundsState.saveReentryState(snapFraction);          final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); -        assertEquals(size, state.getSize());          assertEquals(snapFraction, state.getSnapFraction(), 0.01);      }      @Test      public void testClearReentryState() { -        final Size size = new Size(100, 100);          final float snapFraction = 0.5f; -        mPipBoundsState.saveReentryState(size, snapFraction); +        mPipBoundsState.saveReentryState(snapFraction);          mPipBoundsState.clearReentryState();          assertNull(mPipBoundsState.getReentryState()); @@ -138,20 +135,19 @@ public class PipBoundsStateTest extends ShellTestCase {      @Test      public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() {          mPipBoundsState.setLastPipComponentName(mTestComponentName1); -        mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); +        mPipBoundsState.saveReentryState(DEFAULT_SNAP_FRACTION);          mPipBoundsState.setLastPipComponentName(mTestComponentName1);          final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState();          assertNotNull(state); -        assertEquals(DEFAULT_SIZE, state.getSize());          assertEquals(DEFAULT_SNAP_FRACTION, state.getSnapFraction(), 0.01);      }      @Test      public void testSetLastPipComponentName_changed_clearReentryState() {          mPipBoundsState.setLastPipComponentName(mTestComponentName1); -        mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); +        mPipBoundsState.saveReentryState(DEFAULT_SNAP_FRACTION);          mPipBoundsState.setLastPipComponentName(mTestComponentName2); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 4eb519334e12..5d968d3360ab 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -45,7 +45,6 @@ import android.os.RemoteException;  import android.test.suitebuilder.annotation.SmallTest;  import android.testing.AndroidTestingRunner;  import android.testing.TestableLooper; -import android.util.Size;  import com.android.wm.shell.ShellTestCase;  import com.android.wm.shell.WindowManagerShellWrapper; @@ -256,40 +255,13 @@ public class PipControllerTest extends ShellTestCase {      }      @Test -    public void saveReentryState_noUserResize_doesNotSaveSize() { +    public void saveReentryState_savesPipBoundsState() {          final Rect bounds = new Rect(0, 0, 10, 10);          when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); -        when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(false);          mPipController.saveReentryState(bounds); -        verify(mMockPipBoundsState).saveReentryState(null, 1.0f); -    } - -    @Test -    public void saveReentryState_nonEmptyUserResizeBounds_savesSize() { -        final Rect bounds = new Rect(0, 0, 10, 10); -        final Rect resizedBounds = new Rect(0, 0, 30, 30); -        when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); -        when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds); -        when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true); - -        mPipController.saveReentryState(bounds); - -        verify(mMockPipBoundsState).saveReentryState(new Size(30, 30), 1.0f); -    } - -    @Test -    public void saveReentryState_emptyUserResizeBounds_savesSize() { -        final Rect bounds = new Rect(0, 0, 10, 10); -        final Rect resizedBounds = new Rect(0, 0, 0, 0); -        when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); -        when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds); -        when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true); - -        mPipController.saveReentryState(bounds); - -        verify(mMockPipBoundsState).saveReentryState(new Size(10, 10), 1.0f); +        verify(mMockPipBoundsState).saveReentryState(1.0f);      }      @Test diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp index b667daf9c850..30e7a628f1f6 100644 --- a/libs/hwui/CanvasTransform.cpp +++ b/libs/hwui/CanvasTransform.cpp @@ -137,9 +137,10 @@ static BitmapPalette filterPalette(const SkPaint* paint, BitmapPalette palette)          return palette;      } -    SkColor color = palette == BitmapPalette::Light ? SK_ColorWHITE : SK_ColorBLACK; -    color = paint->getColorFilter()->filterColor(color); -    return paletteForColorHSV(color); +    SkColor4f color = palette == BitmapPalette::Light ? SkColors::kWhite : SkColors::kBlack; +    sk_sp<SkColorSpace> srgb = SkColorSpace::MakeSRGB(); +    color = paint->getColorFilter()->filterColor4f(color, srgb.get(), srgb.get()); +    return paletteForColorHSV(color.toSkColor());  }  bool transformPaint(ColorTransform transform, SkPaint* paint) { diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp index fd276419f5e5..28d85bd860df 100644 --- a/libs/hwui/DamageAccumulator.cpp +++ b/libs/hwui/DamageAccumulator.cpp @@ -218,7 +218,7 @@ void DamageAccumulator::applyRenderNodeTransform(DirtyStack* frame) {      }      // Perform clipping -    if (props.getClipDamageToBounds() && !frame->pendingDirty.isEmpty()) { +    if (props.getClipDamageToBounds()) {          if (!frame->pendingDirty.intersect(SkRect::MakeIWH(props.getWidth(), props.getHeight()))) {              frame->pendingDirty.setEmpty();          } diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 9c7f7cc24266..1d0330185b1c 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -1067,6 +1067,7 @@ SkRect CanvasContext::computeDirtyRect(const Frame& frame, SkRect* dirty) {      if (dirty->isEmpty()) {          dirty->setIWH(frame.width(), frame.height()); +        return *dirty;      }      // At this point dirty is the area of the window to update. However,  |