summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/Android.bp2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt20
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt215
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml5
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java113
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt105
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt)49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java141
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt86
-rw-r--r--libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java41
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt139
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt)58
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java32
-rw-r--r--libs/hwui/CanvasTransform.cpp7
-rw-r--r--libs/hwui/DamageAccumulator.cpp2
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp1
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,