summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml (renamed from libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml)0
-rw-r--r--libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt171
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java159
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java)67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java103
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt285
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt294
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java75
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt14
-rw-r--r--libs/androidfw/Idmap.cpp153
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h51
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h2
-rw-r--r--libs/androidfw/tests/data/overlay/overlay.idmapbin732 -> 732 bytes
-rw-r--r--libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp67
-rw-r--r--libs/hwui/pipeline/skia/BackdropFilterDrawable.h15
-rw-r--r--libs/hwui/tests/common/scenes/BackdropBlur.cpp67
-rw-r--r--libs/hwui/tests/unit/RenderNodeDrawableTests.cpp2
50 files changed, 1549 insertions, 495 deletions
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 3b739c3d5817..1260796810c2 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+ <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
<application>
<activity
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
index 2b2e9df07dce..2b2e9df07dce 100644
--- a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
diff --git a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
index 8ff382bbc7b4..b5bceda9a623 100644
--- a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
+++ b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
@@ -111,7 +111,7 @@
</RadioGroup>
<Button
- android:id="@+id/open_by_default_settings_dialog_dismiss_button"
+ android:id="@+id/open_by_default_settings_dialog_confirm_button"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/open_by_default_dialog_dismiss_button_text"
@@ -122,7 +122,7 @@
android:textSize="14sp"
android:textFontWeight="500"
android:textColor="?androidprv:attr/materialColorOnPrimary"
- android:background="@drawable/open_by_default_settings_dialog_dismiss_button_background"/>
+ android:background="@drawable/open_by_default_settings_dialog_confirm_button_background"/>
</LinearLayout>
</ScrollView>
</FrameLayout>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
index 26aae2d2aa78..02a799189fa1 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
@@ -26,5 +26,11 @@ public interface FocusTransitionListener {
/**
* Called when a transition changes the top, focused display.
*/
- void onFocusedDisplayChanged(int displayId);
+ default void onFocusedDisplayChanged(int displayId) {}
+
+ /**
+ * Called when the per-app or system-wide focus state has changed for a task.
+ */
+ default void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
+ boolean isFocusedGlobally) {}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 647a555ad169..0150bcdbd412 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -19,7 +19,7 @@ package com.android.wm.shell.shared.desktopmode;
import android.annotation.NonNull;
import android.content.Context;
import android.os.SystemProperties;
-import android.window.flags.DesktopModeFlags;
+import android.window.DesktopModeFlags;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 71bcb590ae23..65132fe89063 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -22,7 +22,13 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.content.pm.verify.domain.DomainVerificationUserState
import android.net.Uri
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+
+private const val TAG = "AppToWebUtils"
private val GenericBrowserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
@@ -58,4 +64,25 @@ fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? {
val component = intent.resolveActivity(packageManager) ?: return null
intent.setComponent(component)
return intent
-} \ No newline at end of file
+}
+
+/**
+ * Returns the [DomainVerificationUserState] of the user associated with the given
+ * [DomainVerificationManager] and the given package.
+ */
+fun getDomainVerificationUserState(
+ manager: DomainVerificationManager,
+ packageName: String
+): DomainVerificationUserState? {
+ try {
+ return manager.getDomainVerificationUserState(packageName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ ProtoLog.w(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "%s: Failed to get domain verification user state: %s",
+ TAG,
+ e.message!!
+ )
+ return null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
index 4926cbdbe9fb..a727b54b3a3f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.apptoweb
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.view.LayoutInflater
@@ -30,6 +31,7 @@ import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
import android.view.WindowlessWindowManager
import android.widget.ImageView
+import android.widget.RadioButton
import android.widget.TextView
import android.window.TaskConstants
import com.android.wm.shell.R
@@ -58,8 +60,17 @@ internal class OpenByDefaultDialog(
private lateinit var appIconView: ImageView
private lateinit var appNameView: TextView
+ private lateinit var openInAppButton: RadioButton
+ private lateinit var openInBrowserButton: RadioButton
+
+ private val domainVerificationManager =
+ context.getSystemService(DomainVerificationManager::class.java)!!
+ private val packageName = taskInfo.baseActivity?.packageName!!
+
+
init {
createDialog()
+ initializeRadioButtons()
bindAppInfo(appIconBitmap, appName)
}
@@ -111,9 +122,30 @@ internal class OpenByDefaultDialog(
closeMenu()
}
+ dialog.setConfirmButtonClickListener {
+ setDefaultLinkHandlingSetting()
+ closeMenu()
+ }
+
listener.onDialogCreated()
}
+ private fun initializeRadioButtons() {
+ openInAppButton = dialog.requireViewById(R.id.open_in_app_button)
+ openInBrowserButton = dialog.requireViewById(R.id.open_in_browser_button)
+
+ val userState =
+ getDomainVerificationUserState(domainVerificationManager, packageName) ?: return
+ val openInApp = userState.isLinkHandlingAllowed
+ openInAppButton.isChecked = openInApp
+ openInBrowserButton.isChecked = !openInApp
+ }
+
+ private fun setDefaultLinkHandlingSetting() {
+ domainVerificationManager.setDomainVerificationLinkHandlingAllowed(
+ packageName, openInAppButton.isChecked)
+ }
+
private fun closeMenu() {
dialogContainer?.releaseView()
dialogContainer = null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
index d03a38e8699a..1b914f419d94 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
@@ -36,9 +36,6 @@ class OpenByDefaultDialogView @JvmOverloads constructor(
private lateinit var backgroundDim: Drawable
fun setDismissOnClickListener(callback: (View) -> Unit) {
- val dismissButton = dialogContainer.requireViewById<Button>(
- R.id.open_by_default_settings_dialog_dismiss_button)
- dismissButton.setOnClickListener(callback)
// Clicks on the background dim should also dismiss the dialog.
setOnClickListener(callback)
// We add a no-op on-click listener to the dialog container so that clicks on it won't
@@ -46,6 +43,13 @@ class OpenByDefaultDialogView @JvmOverloads constructor(
dialogContainer.setOnClickListener { }
}
+ fun setConfirmButtonClickListener(callback: (View) -> Unit) {
+ val dismissButton = dialogContainer.requireViewById<Button>(
+ R.id.open_by_default_settings_dialog_confirm_button
+ )
+ dismissButton.setOnClickListener(callback)
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
dialogContainer = requireViewById(R.id.open_by_default_dialog_container)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
index c88a58be1461..1abe11998500 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
@@ -36,6 +36,8 @@ public class BubbleLogger {
@VisibleForTesting
public enum Event implements UiEventLogger.UiEventEnum {
+ // region bubble events
+
@UiEvent(doc = "User dismissed the bubble via gesture, add bubble to overflow.")
BUBBLE_OVERFLOW_ADD_USER_GESTURE(483),
@@ -64,7 +66,89 @@ public class BubbleLogger {
BUBBLE_OVERFLOW_SELECTED(600),
@UiEvent(doc = "Restore bubble to overflow after phone reboot.")
- BUBBLE_OVERFLOW_RECOVER(691);
+ BUBBLE_OVERFLOW_RECOVER(691),
+
+ // endregion
+
+ // region bubble bar events
+
+ @UiEvent(doc = "new bubble posted")
+ BUBBLE_BAR_BUBBLE_POSTED(1927),
+
+ @UiEvent(doc = "existing bubble updated")
+ BUBBLE_BAR_BUBBLE_UPDATED(1928),
+
+ @UiEvent(doc = "expanded a bubble from bubble bar")
+ BUBBLE_BAR_EXPANDED(1929),
+
+ @UiEvent(doc = "bubble bar collapsed")
+ BUBBLE_BAR_COLLAPSED(1930),
+
+ @UiEvent(doc = "dismissed single bubble from bubble bar by dragging it to dismiss target")
+ BUBBLE_BAR_BUBBLE_DISMISSED_DRAG_BUBBLE(1931),
+
+ @UiEvent(doc = "dismissed single bubble from bubble bar by dragging the expanded view to "
+ + "dismiss target")
+ BUBBLE_BAR_BUBBLE_DISMISSED_DRAG_EXP_VIEW(1932),
+
+ @UiEvent(doc = "dismiss bubble from app handle menu")
+ BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU(1933),
+
+ @UiEvent(doc = "bubble is dismissed due to app finishing the bubble activity")
+ BUBBLE_BAR_BUBBLE_ACTIVITY_FINISH(1934),
+
+ @UiEvent(doc = "dismissed the bubble bar by dragging it to dismiss target")
+ BUBBLE_BAR_DISMISSED_DRAG_BAR(1935),
+
+ @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging from the "
+ + "expanded view")
+ BUBBLE_BAR_MOVED_LEFT_DRAG_EXP_VIEW(1936),
+
+ @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging from a single"
+ + " bubble")
+ BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE(1937),
+
+ @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging the bubble bar")
+ BUBBLE_BAR_MOVED_LEFT_DRAG_BAR(1938),
+
+ @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging from the "
+ + "expanded view")
+ BUBBLE_BAR_MOVED_RIGHT_DRAG_EXP_VIEW(1939),
+
+ @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging from a "
+ + "single bubble")
+ BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE(1940),
+
+ @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging the bubble "
+ + "bar")
+ BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR(1941),
+
+ @UiEvent(doc = "stop bubbling conversation from app handle menu")
+ BUBBLE_BAR_APP_MENU_OPT_OUT(1942),
+
+ @UiEvent(doc = "open app settings from app handle menu")
+ BUBBLE_BAR_APP_MENU_GO_TO_SETTINGS(1943),
+
+ @UiEvent(doc = "flyout shown for a bubble")
+ BUBBLE_BAR_FLYOUT(1944),
+
+ @UiEvent(doc = "notification for the bubble was canceled")
+ BUBBLE_BAR_BUBBLE_REMOVED_CANCELED(1945),
+
+ @UiEvent(doc = "user turned off bubbles from settings")
+ BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED(1946),
+
+ @UiEvent(doc = "bubble bar overflow opened")
+ BUBBLE_BAR_OVERFLOW_SELECTED(1947),
+
+ @UiEvent(doc = "max number of bubbles was reached in bubble bar, move bubble to overflow")
+ BUBBLE_BAR_OVERFLOW_ADD_AGED(1948),
+
+ @UiEvent(doc = "bubble promoted from overflow back to bubble bar")
+ BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR(1949),
+
+ // endregion
+ ;
private final int mId;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index b3491baa629d..b83b5f341dda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -177,26 +177,84 @@ object PipUtils {
}
/**
+ * Calculates the transform to apply on a UNTRANSFORMED (config-at-end) Activity surface in
+ * order for it's hint-rect to occupy the same task-relative position/dimensions as it would
+ * have at the end of the transition (post-configuration).
+ *
+ * This is intended to be used in tandem with [calcStartTransform] below applied to the parent
+ * task. Applying both transforms simultaneously should result in the appearance of nothing
+ * having happened yet.
+ *
+ * Only the task should be animated (into it's identity state) and then WMCore will reset the
+ * activity transform in sync with its new configuration upon finish.
+ *
+ * Usage example:
+ * calcEndTransform(pipActivity, pipTask, scale, pos);
+ * t.setScale(pipActivity.getLeash(), scale.x, scale.y);
+ * t.setPosition(pipActivity.getLeash(), pos.x, pos.y);
+ *
+ * @see calcStartTransform
+ */
+ @JvmStatic
+ fun calcEndTransform(pipActivity: TransitionInfo.Change, pipTask: TransitionInfo.Change,
+ outScale: PointF, outPos: PointF) {
+ val actStartBounds = pipActivity.startAbsBounds
+ val actEndBounds = pipActivity.endAbsBounds
+ val taskEndBounds = pipTask.endAbsBounds
+
+ var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
+ if (hintRect == null) {
+ hintRect = Rect(actStartBounds)
+ hintRect.offsetTo(0, 0)
+ }
+
+ // FA = final activity bounds (absolute)
+ // FT = final task bounds (absolute)
+ // SA = start activity bounds (absolute)
+ // H = source hint (relative to start activity bounds)
+ // We want to transform the activity so that when the task is at FT, H overlaps with FA
+
+ // This scales the activity such that the hint rect has the same dimensions
+ // as the final activity bounds.
+ val hintToEndScaleX = (actEndBounds.width().toFloat()) / (hintRect.width().toFloat())
+ val hintToEndScaleY = (actEndBounds.height().toFloat()) / (hintRect.height().toFloat())
+ // top-left needs to be (FA.tl - FT.tl) - H.tl * hintToEnd . H is relative to the
+ // activity; so, for example, if shrinking H to FA (hintToEnd < 1), then the tl of the
+ // shrunk SA is closer to H than expected, so we need to reduce how much we offset SA
+ // to get H.tl to match.
+ val startActPosInTaskEndX =
+ (actEndBounds.left - taskEndBounds.left) - hintRect.left * hintToEndScaleX
+ val startActPosInTaskEndY =
+ (actEndBounds.top - taskEndBounds.top) - hintRect.top * hintToEndScaleY
+ outScale.set(hintToEndScaleX, hintToEndScaleY)
+ outPos.set(startActPosInTaskEndX, startActPosInTaskEndY)
+ }
+
+ /**
* Calculates the transform and crop to apply on a Task surface in order for the config-at-end
* activity inside it (original-size activity transformed to match it's hint rect to the final
* Task bounds) to occupy the same world-space position/dimensions as it had before the
* transition.
*
+ * Intended to be used in tandem with [calcEndTransform].
+ *
* Usage example:
- * calcStartTransform(pipChange, scale, pos, crop);
- * t.setScale(pipChange.getLeash(), scale.x, scale.y);
- * t.setPosition(pipChange.getLeash(), pos.x, pos.y);
- * t.setCrop(pipChange.getLeash(), crop);
+ * calcStartTransform(pipTask, scale, pos, crop);
+ * t.setScale(pipTask.getLeash(), scale.x, scale.y);
+ * t.setPosition(pipTask.getLeash(), pos.x, pos.y);
+ * t.setCrop(pipTask.getLeash(), crop);
+ *
+ * @see calcEndTransform
*/
@JvmStatic
- fun calcStartTransform(pipChange: TransitionInfo.Change, outScale: PointF,
+ fun calcStartTransform(pipTask: TransitionInfo.Change, outScale: PointF,
outPos: PointF, outCrop: Rect) {
- val startBounds = pipChange.startAbsBounds
- val taskEndBounds = pipChange.endAbsBounds
+ val startBounds = pipTask.startAbsBounds
+ val taskEndBounds = pipTask.endAbsBounds
// For now, pip activity bounds always matches task bounds. If this ever changes, we'll
// need to get the activity offset.
val endBounds = taskEndBounds
- var hintRect = pipChange.taskInfo?.pictureInPictureParams?.sourceRectHint
+ var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
if (hintRect == null) {
hintRect = Rect(startBounds)
hintRect.offsetTo(0, 0)
@@ -226,8 +284,8 @@ object PipUtils {
+ startBounds.left + hintRect.left)
val endTaskPosForStartY = (-(endBounds.top - taskEndBounds.top) * endToHintScaleY
+ startBounds.top + hintRect.top)
- outScale[endToHintScaleX] = endToHintScaleY
- outPos[endTaskPosForStartX] = endTaskPosForStartY
+ outScale.set(endToHintScaleX, endToHintScaleY)
+ outPos.set(endTaskPosForStartX, endTaskPosForStartY)
// now need to set crop to reveal the non-hint stuff. Again, hintrect is relative, so
// we must apply outsets to reveal the *activity* content which is *inside* the task
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 4d15605c756a..2128cbc144b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -27,7 +27,7 @@ import android.graphics.Rect;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
-import android.window.flags.DesktopModeFlags;
+import android.window.DesktopModeFlags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags.Flags;
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 79c31e01e365..ff32c5e53403 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
@@ -16,8 +16,8 @@
package com.android.wm.shell.dagger;
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import android.annotation.Nullable;
import android.app.KeyguardManager;
@@ -71,10 +71,10 @@ import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopRepository;
+import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
-import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
@@ -92,9 +92,9 @@ import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
-import com.android.wm.shell.freeform.TaskChangeListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarterInitializer;
+import com.android.wm.shell.freeform.TaskChangeListener;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.PipTransitionController;
@@ -111,6 +111,7 @@ import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.MixedTransitionHandler;
import com.android.wm.shell.transition.Transitions;
@@ -391,10 +392,11 @@ public abstract class WMShellModule {
Transitions transitions,
Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
WindowDecorViewModel windowDecorViewModel,
- Optional<TaskChangeListener> taskChangeListener) {
+ Optional<TaskChangeListener> taskChangeListener,
+ FocusTransitionObserver focusTransitionObserver) {
return new FreeformTaskTransitionObserver(
context, shellInit, transitions, desktopImmersiveTransitionHandler,
- windowDecorViewModel, taskChangeListener);
+ windowDecorViewModel, taskChangeListener, focusTransitionObserver);
}
@WMSingleton
@@ -693,10 +695,16 @@ public abstract class WMShellModule {
static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
Context context,
Transitions transitions,
- @DynamicOverride DesktopRepository desktopRepository) {
+ @DynamicOverride DesktopRepository desktopRepository,
+ DisplayController displayController,
+ ShellTaskOrganizer shellTaskOrganizer) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(
- new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+ new DesktopFullImmersiveTransitionHandler(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer));
}
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
index f749aa1edd92..679179a7ff68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -27,8 +27,12 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
-import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -41,16 +45,29 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
class DesktopFullImmersiveTransitionHandler(
private val transitions: Transitions,
private val desktopRepository: DesktopRepository,
+ private val displayController: DisplayController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
private val transactionSupplier: () -> SurfaceControl.Transaction,
) : TransitionHandler {
constructor(
transitions: Transitions,
desktopRepository: DesktopRepository,
- ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+ displayController: DisplayController,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ ) : this(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer,
+ { SurfaceControl.Transaction() }
+ )
private var state: TransitionState? = null
+ @VisibleForTesting
+ val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()
+
/** Whether there is an immersive transition that hasn't completed yet. */
private val inProgress: Boolean
get() = state != null
@@ -61,15 +78,15 @@ class DesktopFullImmersiveTransitionHandler(
var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
/** Starts a transition to enter full immersive state inside the desktop. */
- fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "FullImmersive: cannot start entry because transition already in progress."
- )
+ logV("Cannot start entry because transition already in progress.")
return
}
-
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, Rect())
+ }
+ logV("Moving task ${taskInfo.taskId} into immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -79,15 +96,18 @@ class DesktopFullImmersiveTransitionHandler(
)
}
- fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "$TAG: cannot start exit because transition already in progress."
- )
+ logV("Cannot start exit because transition already in progress.")
return
}
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val destinationBounds = calculateMaximizeBounds(displayLayout, taskInfo)
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, destinationBounds)
+ }
+ logV("Moving task ${taskInfo.taskId} out of immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -97,6 +117,82 @@ class DesktopFullImmersiveTransitionHandler(
)
}
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param transition that will apply this transaction
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ */
+ fun exitImmersiveIfApplicable(
+ transition: IBinder,
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ) {
+ if (!Flags.enableFullyImmersiveInDesktop()) return
+ exitImmersiveIfApplicable(wct, displayId)?.invoke(transition)
+ }
+
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ val displayLayout = displayController.getDisplayLayout(displayId) ?: return null
+ val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
+ val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
+ logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) }
+ }
+
+ /**
+ * Bring the given [taskInfo] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param taskInfo of the task that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+ // A full immersive task is being minimized, make sure the immersive state is broken
+ // (i.e. resize back to max bounds).
+ displayController.getDisplayLayout(taskInfo.displayId)?.let { displayLayout ->
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ logV("Appending immersive exit for task: ${taskInfo.taskId}")
+ return { transition ->
+ addPendingImmersiveExit(
+ taskId = taskInfo.taskId,
+ displayId = taskInfo.displayId,
+ transition = transition
+ )
+ }
+ }
+ }
+ return null
+ }
+
+ private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
+ pendingExternalExitTransitions.add(
+ ExternalPendingExit(
+ taskId = taskId,
+ displayId = displayId,
+ transition = transition
+ )
+ )
+ }
+
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
@@ -190,15 +286,31 @@ class DesktopFullImmersiveTransitionHandler(
* Called when any transition in the system is ready to play. This is needed to update the
* repository state before window decorations are drawn (which happens immediately after
* |onTransitionReady|, before this transition actually animates) because drawing decorations
- * depends in whether the task is in full immersive state or not.
+ * depends on whether the task is in full immersive state or not.
*/
- fun onTransitionReady(transition: IBinder) {
+ fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+ // Check if this is a pending external exit transition.
+ val pendingExit = pendingExternalExitTransitions
+ .firstOrNull { pendingExit -> pendingExit.transition == transition }
+ if (pendingExit != null) {
+ pendingExternalExitTransitions.remove(pendingExit)
+ if (info.hasTaskChange(taskId = pendingExit.taskId)) {
+ if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
+ logV("Pending external exit for task ${pendingExit.taskId} verified")
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = pendingExit.displayId,
+ taskId = pendingExit.taskId,
+ immersive = false
+ )
+ }
+ }
+ return
+ }
+
+ // Check if this is a direct immersive enter/exit transition.
val state = this.state ?: return
- // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
- // immersive, which isn't realistic. The app could crash, the user could dismiss it from
- // overview, etc. This (or its caller) should search all transitions to look for any
- // immersive task exiting that state to keep the repository properly updated.
if (transition == state.transition) {
+ logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
when (state.direction) {
Direction.ENTER -> {
desktopRepository.setTaskInFullImmersiveState(
@@ -225,6 +337,9 @@ class DesktopFullImmersiveTransitionHandler(
private fun requireState(): TransitionState =
state ?: error("Expected non-null transition state")
+ private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean =
+ changes.any { c -> c.taskInfo?.taskId == taskId }
+
/** The state of the currently running transition. */
private data class TransitionState(
val transition: IBinder,
@@ -233,12 +348,28 @@ class DesktopFullImmersiveTransitionHandler(
val direction: Direction
)
+ /**
+ * Tracks state of a transition involving an immersive exit that is external to this class' own
+ * transitions. This usually means transitions that exit immersive mode as a side-effect and
+ * not the primary action (for example, minimizing the immersive task or launching a new task
+ * on top of the immersive task).
+ */
+ data class ExternalPendingExit(
+ val taskId: Int,
+ val displayId: Int,
+ val transition: IBinder,
+ )
+
private enum class Direction {
ENTER, EXIT
}
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
private companion object {
- private const val TAG = "FullImmersiveHandler"
+ private const val TAG = "DesktopImmersive"
private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index bd6172226cf2..6d4792250be2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -123,6 +123,29 @@ fun calculateInitialBounds(
}
/**
+ * Calculates the maximized bounds of a task given in the given [DisplayLayout], taking
+ * resizability into consideration.
+ */
+fun calculateMaximizeBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo,
+): Rect {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ if (taskInfo.isResizeable) {
+ // if resizable then expand to entire stable bounds (full display minus insets)
+ return Rect(stableBounds)
+ } else {
+ // if non-resizable then calculate max bounds according to aspect ratio
+ val activityAspectRatio = calculateAspectRatio(taskInfo)
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+ Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+ return centerInArea(
+ newSize, stableBounds, stableBounds.left, stableBounds.top)
+ }
+}
+
+/**
* Calculates the largest size that can fit in a given area while maintaining a specific aspect
* ratio.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index c175133dd37b..5ac4ef5cf049 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -328,6 +328,10 @@ class DesktopRepository (
return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
}
+ /** Returns the task that is currently in immersive mode in this display, or null. */
+ fun getTaskInFullImmersiveState(displayId: Int): Int? =
+ desktopTaskDataByDisplayId.getOrCreate(displayId).fullImmersiveTaskId
+
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
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 fa8b6e6427a7..3f6dc94d6a3f 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
@@ -48,14 +48,14 @@ import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
import android.window.RemoteTransition
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
-import android.window.flags.DesktopModeFlags
-import android.window.flags.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
-import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
-import android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
import androidx.annotation.BinderThread
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
@@ -91,6 +91,7 @@ import com.android.wm.shell.shared.ShellSharedConstants
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
@@ -190,6 +191,7 @@ class DesktopTasksController(
private var recentsAnimationRunning = false
private lateinit var splitScreenController: SplitScreenController
+ lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
@@ -354,6 +356,8 @@ class DesktopTasksController(
// TODO(342378842): Instead of using default display, support multiple displays
val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -363,6 +367,7 @@ class DesktopTasksController(
// TODO(343149901): Add DPI changes for task launch
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
return true
}
@@ -379,6 +384,7 @@ class DesktopTasksController(
}
logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
exitSplitIfApplicable(wct, task)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId)
// Bring other apps to front first
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -386,6 +392,7 @@ class DesktopTasksController(
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -422,8 +429,13 @@ class DesktopTasksController(
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
addMoveToDesktopChanges(wct, taskInfo)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
- transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
+ transition?.let {
+ addPendingMinimizeTransition(it, taskToMinimize)
+ runOnTransit?.invoke(transition)
+ }
}
/**
@@ -455,18 +467,28 @@ class DesktopTasksController(
taskRepository.addClosingTask(displayId, taskId)
}
- /**
- * Perform clean up of the desktop wallpaper activity if the minimized window task is the last
- * active task.
- *
- * @param wct transaction to modify if the last active task is minimized
- * @param taskId task id of the window that's being minimized
- */
- fun onDesktopWindowMinimize(wct: WindowContainerTransaction, taskId: Int) {
+ fun minimizeTask(taskInfo: RunningTaskInfo) {
+ val taskId = taskInfo.taskId
+ val displayId = taskInfo.displayId
+ val wct = WindowContainerTransaction()
if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
+ // Perform clean up of the desktop wallpaper activity if the minimized window task is
+ // the last active task.
removeWallpaperActivity(wct)
}
- // Do not call taskRepository.minimizeTask because it will be called by DekstopTasksLimiter.
+ // Notify immersive handler as it might need to exit immersive state.
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+
+ wct.reorder(taskInfo.token, false)
+ val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
+ desktopTasksLimiter.ifPresent {
+ it.addPendingMinimizeChange(
+ transition = transition,
+ displayId = displayId,
+ taskId = taskId
+ )
+ }
+ runOnTransit?.invoke(transition)
}
/** Move a task with given `taskId` to fullscreen */
@@ -552,6 +574,8 @@ class DesktopTasksController(
// TODO: b/342378842 - Instead of using default display, support multiple displays
val taskToMinimize: RunningTaskInfo? =
addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -560,6 +584,7 @@ class DesktopTasksController(
)
val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/** Move a task to the front */
@@ -567,11 +592,14 @@ class DesktopTasksController(
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -643,22 +671,12 @@ class DesktopTasksController(
private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, Rect())
- }
- immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
}
private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
- val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
- val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
-
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, destinationBounds)
- }
- immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
}
/**
@@ -697,7 +715,7 @@ class DesktopTasksController(
// and toggle to the stable bounds.
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
- destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
+ destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
@@ -1285,8 +1303,10 @@ class DesktopTasksController(
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
}
- // Desktop Mode is showing and we're launching a new Task - we might need to minimize
- // a Task.
+ // Desktop Mode is showing and we're launching a new Task:
+ // 1) Exit immersive if needed.
+ immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
+ // 2) minimize a Task if needed.
val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
if (taskToMinimize != null) {
addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1316,6 +1336,9 @@ class DesktopTasksController(
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
addPendingMinimizeTransition(transition, taskToMinimize)
+ immersiveTransitionHandler.exitImmersiveIfApplicable(
+ transition, wct, task.displayId
+ )
}
}
return null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 37bec21730a6..d6b721253abf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -24,7 +24,7 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
-import android.window.flags.DesktopModeFlags
+import android.window.DesktopModeFlags
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
import com.android.internal.jank.InteractionJankMonitor
@@ -39,7 +39,7 @@ import com.android.wm.shell.transition.Transitions.TransitionObserver
* Limits the number of tasks shown in Desktop Mode.
*
* This class should only be used if
- * [android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT]
+ * [android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT]
* is enabled and [maxTasksLimit] is strictly greater than 0.
*/
class DesktopTasksLimiter (
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 64ae35b620dc..a4bc2fe9460b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -24,8 +24,8 @@ import android.view.WindowManager
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
-import android.window.flags.DesktopModeFlags
-import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 1090a4690a5d..86351e364cdd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -51,5 +51,5 @@ interface IDesktopMode {
void moveToDesktop(int taskId, in DesktopModeTransitionSource transitionSource);
/** Remove desktop on the given display */
- void removeDesktop(int displayId);
+ oneway void removeDesktop(int displayId);
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 92e645d493ec..a16446fffa15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -24,7 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
-import android.window.flags.DesktopModeFlags;
+import android.window.DesktopModeFlags;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -126,6 +126,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
|| repository.isClosingTask(taskInfo.taskId)) {
// A task that's vanishing should be removed:
// - If it's closed by the X button which means it's marked as a closing task.
+ repository.removeClosingTask(taskInfo.taskId);
repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
} else {
repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false);
@@ -150,8 +151,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
mDesktopRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
repository.addActiveTask(taskInfo.displayId, taskInfo.taskId);
- } else if (repository.isClosingTask(taskInfo.taskId)) {
- repository.removeClosingTask(taskInfo.taskId);
}
repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId,
taskInfo.isVisible);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index c9eccc3ff534..771573d48e45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -30,6 +30,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.window.flags.Flags;
import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -50,6 +51,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
private final WindowDecorViewModel mWindowDecorViewModel;
private final Optional<TaskChangeListener> mTaskChangeListener;
+ private final FocusTransitionObserver mFocusTransitionObserver;
private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
new HashMap<>();
@@ -60,11 +62,13 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
Transitions transitions,
Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
WindowDecorViewModel windowDecorViewModel,
- Optional<TaskChangeListener> taskChangeListener) {
+ Optional<TaskChangeListener> taskChangeListener,
+ FocusTransitionObserver focusTransitionObserver) {
mTransitions = transitions;
mImmersiveTransitionHandler = immersiveTransitionHandler;
mWindowDecorViewModel = windowDecorViewModel;
mTaskChangeListener = taskChangeListener;
+ mFocusTransitionObserver = focusTransitionObserver;
if (FreeformComponents.isFreeformEnabled(context)) {
shellInit.addInitCallback(this::onInit, this);
}
@@ -85,8 +89,11 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
// TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
// is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
// Otherwise window decoration relayout won't run with the immersive state up to date.
- mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+ mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info));
}
+ // Update focus state first to ensure the correct state can be queried from listeners.
+ // TODO(371503964): Remove this once the unified task repository is ready.
+ mFocusTransitionObserver.updateFocusState(info);
final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>();
final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
new file mode 100644
index 000000000000..f40a87c39aef
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.animation;
+
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.shared.animation.Interpolators;
+
+/**
+ * Animator that handles bounds animations for entering PIP.
+ */
+public class PipEnterAnimator extends ValueAnimator
+ implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
+ @NonNull private final SurfaceControl mLeash;
+ private final SurfaceControl.Transaction mStartTransaction;
+ private final SurfaceControl.Transaction mFinishTransaction;
+
+ // Bounds updated by the evaluator as animator is running.
+ private final Rect mAnimatedRect = new Rect();
+
+ private final RectEvaluator mRectEvaluator;
+ private final Rect mEndBounds = new Rect();
+ @Nullable private final Rect mSourceRectHint;
+ private final @Surface.Rotation int mRotation;
+ @Nullable private Runnable mAnimationStartCallback;
+ @Nullable private Runnable mAnimationEndCallback;
+
+ private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+
+ // Internal state representing initial transform - cached to avoid recalculation.
+ private final PointF mInitScale = new PointF();
+ private final PointF mInitPos = new PointF();
+ private final Rect mInitCrop = new Rect();
+
+ public PipEnterAnimator(Context context,
+ @NonNull SurfaceControl leash,
+ SurfaceControl.Transaction startTransaction,
+ SurfaceControl.Transaction finishTransaction,
+ @NonNull Rect endBounds,
+ @Nullable Rect sourceRectHint,
+ @Surface.Rotation int rotation) {
+ mLeash = leash;
+ mStartTransaction = startTransaction;
+ mFinishTransaction = finishTransaction;
+ mRectEvaluator = new RectEvaluator(mAnimatedRect);
+ mEndBounds.set(endBounds);
+ mSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : null;
+ mRotation = rotation;
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+
+ final int enterAnimationDuration = context.getResources()
+ .getInteger(R.integer.config_pipEnterAnimationDuration);
+ setDuration(enterAnimationDuration);
+ setFloatValues(0f, 1f);
+ setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ addListener(this);
+ addUpdateListener(this);
+ }
+
+ public void setAnimationStartCallback(@NonNull Runnable runnable) {
+ mAnimationStartCallback = runnable;
+ }
+
+ public void setAnimationEndCallback(@NonNull Runnable runnable) {
+ mAnimationEndCallback = runnable;
+ }
+
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ if (mAnimationStartCallback != null) {
+ mAnimationStartCallback.run();
+ }
+ if (mStartTransaction != null) {
+ onEnterAnimationUpdate(mInitScale, mInitPos, mInitCrop,
+ 0f /* fraction */, mStartTransaction);
+ mStartTransaction.apply();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ if (mAnimationEndCallback != null) {
+ mAnimationEndCallback.run();
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+ final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ final float fraction = getAnimatedFraction();
+ onEnterAnimationUpdate(mInitScale, mInitPos, mInitCrop, fraction, tx);
+ tx.apply();
+ }
+
+ private void onEnterAnimationUpdate(PointF initScale, PointF initPos, Rect initCrop,
+ float fraction, SurfaceControl.Transaction tx) {
+ float scaleX = 1 + (initScale.x - 1) * (1 - fraction);
+ float scaleY = 1 + (initScale.y - 1) * (1 - fraction);
+ tx.setScale(mLeash, scaleX, scaleY);
+
+ float posX = initPos.x + (mEndBounds.left - initPos.x) * fraction;
+ float posY = initPos.y + (mEndBounds.top - initPos.y) * fraction;
+ tx.setPosition(mLeash, posX, posY);
+
+ Rect endCrop = new Rect(mEndBounds);
+ endCrop.offsetTo(0, 0);
+ mRectEvaluator.evaluate(fraction, initCrop, endCrop);
+ tx.setCrop(mLeash, mAnimatedRect);
+ }
+
+ // no-ops
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {}
+
+ /**
+ * Caches the initial transform relevant values for the bounds enter animation.
+ *
+ * Since enter PiP makes use of a config-at-end transition, initial transform needs to be
+ * calculated differently from generic transitions.
+ * @param pipChange PiP change received as a transition target.
+ */
+ public void setEnterStartState(@NonNull TransitionInfo.Change pipChange) {
+ PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
index 8ebdc96c21a3..8fa5aa933929 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
@@ -19,7 +19,6 @@ package com.android.wm.shell.pip2.animation;
import android.animation.Animator;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
-import android.annotation.IntDef;
import android.content.Context;
import android.graphics.Rect;
import android.view.Surface;
@@ -30,35 +29,22 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.R;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import com.android.wm.shell.shared.animation.Interpolators;
/**
- * Animator that handles bounds animations for entering / exiting PIP.
+ * Animator that handles bounds animations for exit-via-expanding PIP.
*/
-public class PipEnterExitAnimator extends ValueAnimator
+public class PipExpandAnimator extends ValueAnimator
implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
- @IntDef(prefix = {"BOUNDS_"}, value = {
- BOUNDS_ENTER,
- BOUNDS_EXIT
- })
-
- @Retention(RetentionPolicy.SOURCE)
- public @interface BOUNDS {}
-
- public static final int BOUNDS_ENTER = 0;
- public static final int BOUNDS_EXIT = 1;
-
- @NonNull private final SurfaceControl mLeash;
+ @NonNull
+ private final SurfaceControl mLeash;
private final SurfaceControl.Transaction mStartTransaction;
private final SurfaceControl.Transaction mFinishTransaction;
- private final int mEnterExitAnimationDuration;
- private final @BOUNDS int mDirection;
private final @Surface.Rotation int mRotation;
// optional callbacks for tracking animation start and end
- @Nullable private Runnable mAnimationStartCallback;
+ @Nullable
+ private Runnable mAnimationStartCallback;
@Nullable private Runnable mAnimationEndCallback;
private final Rect mBaseBounds = new Rect();
@@ -78,7 +64,7 @@ public class PipEnterExitAnimator extends ValueAnimator
private final RectEvaluator mInsetEvaluator;
private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
- public PipEnterExitAnimator(Context context,
+ public PipExpandAnimator(Context context,
@NonNull SurfaceControl leash,
SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction,
@@ -86,7 +72,6 @@ public class PipEnterExitAnimator extends ValueAnimator
@NonNull Rect startBounds,
@NonNull Rect endBounds,
@Nullable Rect sourceRectHint,
- @BOUNDS int direction,
@Surface.Rotation int rotation) {
mLeash = leash;
mStartTransaction = startTransaction;
@@ -98,7 +83,6 @@ public class PipEnterExitAnimator extends ValueAnimator
mRectEvaluator = new RectEvaluator(mAnimatedRect);
mInsetEvaluator = new RectEvaluator(new Rect());
mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context);
- mDirection = direction;
mRotation = rotation;
mSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : null;
@@ -113,12 +97,14 @@ public class PipEnterExitAnimator extends ValueAnimator
mSurfaceControlTransactionFactory =
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
- mEnterExitAnimationDuration = context.getResources()
+
+ final int enterAnimationDuration = context.getResources()
.getInteger(R.integer.config_pipEnterAnimationDuration);
+ setDuration(enterAnimationDuration);
setObjectValues(startBounds, endBounds);
- setDuration(mEnterExitAnimationDuration);
setEvaluator(mRectEvaluator);
+ setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
addListener(this);
addUpdateListener(this);
}
@@ -147,9 +133,10 @@ public class PipEnterExitAnimator extends ValueAnimator
// finishTransaction might override some state (eg. corner radii) so we want to
// manually set the state to the end of the animation
mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint,
- mBaseBounds, mAnimatedRect, getInsets(1f), isInPipDirection(), 1f)
- .round(mFinishTransaction, mLeash, isInPipDirection())
- .shadow(mFinishTransaction, mLeash, isInPipDirection());
+ mBaseBounds, mAnimatedRect, getInsets(1f),
+ false /* isInPipDirection */, 1f)
+ .round(mFinishTransaction, mLeash, false /* applyCornerRadius */)
+ .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */);
}
if (mAnimationEndCallback != null) {
mAnimationEndCallback.run();
@@ -160,32 +147,22 @@ public class PipEnterExitAnimator extends ValueAnimator
public void onAnimationUpdate(@NonNull ValueAnimator animation) {
final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
final float fraction = getAnimatedFraction();
- Rect insets = getInsets(fraction);
// TODO (b/350801661): implement fixed rotation
+ Rect insets = getInsets(fraction);
mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint,
- mBaseBounds, mAnimatedRect, insets, isInPipDirection(), fraction)
- .round(tx, mLeash, isInPipDirection())
- .shadow(tx, mLeash, isInPipDirection());
+ mBaseBounds, mAnimatedRect, insets, false /* isInPipDirection */, fraction)
+ .round(tx, mLeash, false /* applyCornerRadius */)
+ .shadow(tx, mLeash, false /* applyCornerRadius */);
tx.apply();
}
-
private Rect getInsets(float fraction) {
- Rect startInsets = isInPipDirection() ? mZeroInsets : mSourceRectHintInsets;
- Rect endInsets = isInPipDirection() ? mSourceRectHintInsets : mZeroInsets;
-
+ final Rect startInsets = mSourceRectHintInsets;
+ final Rect endInsets = mZeroInsets;
return mInsetEvaluator.evaluate(fraction, startInsets, endInsets);
}
- private boolean isInPipDirection() {
- return mDirection == BOUNDS_ENTER;
- }
-
- private boolean isOutPipDirection() {
- return mDirection == BOUNDS_EXIT;
- }
-
// no-ops
@Override
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 62a60fab6603..b57f51aff176 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
@@ -56,7 +56,8 @@ import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
-import com.android.wm.shell.pip2.animation.PipEnterExitAnimator;
+import com.android.wm.shell.pip2.animation.PipEnterAnimator;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.shared.pip.PipContentOverlay;
import com.android.wm.shell.sysui.ShellInit;
@@ -218,6 +219,7 @@ public class PipTransition extends PipTransitionController implements
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ mFinishCallback = finishCallback;
if (transition == mEnterTransition || info.getType() == TRANSIT_PIP) {
mEnterTransition = null;
// If we are in swipe PiP to Home transition we are ENTERING_PIP as a jumpcut transition
@@ -258,6 +260,7 @@ public class PipTransition extends PipTransitionController implements
if (isRemovePipTransition(info)) {
return removePipImmediately(info, startTransaction, finishTransaction, finishCallback);
}
+ mFinishCallback = null;
return false;
}
@@ -297,7 +300,6 @@ public class PipTransition extends PipTransitionController implements
mBoundsChangeDuration = BOUNDS_CHANGE_JUMPCUT_DURATION;
}
- mFinishCallback = finishCallback;
mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra);
return true;
}
@@ -349,7 +351,6 @@ public class PipTransition extends PipTransitionController implements
startTransaction.setMatrix(pipLeash, transformTensor, matrixTmp);
}
startTransaction.apply();
- finishCallback.onTransitionFinished(null /* finishWct */);
finishInner();
return true;
}
@@ -386,14 +387,6 @@ public class PipTransition extends PipTransitionController implements
return false;
}
- WindowContainerToken pipTaskToken = pipChange.getContainer();
- if (pipTaskToken == null) {
- return false;
- }
-
- WindowContainerTransaction finishWct = new WindowContainerTransaction();
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-
Rect startBounds = pipChange.getStartAbsBounds();
Rect endBounds = pipChange.getEndAbsBounds();
SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
@@ -405,29 +398,22 @@ public class PipTransition extends PipTransitionController implements
sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
}
- // For opening type transitions, if there is a non-pip change of mode TO_FRONT/OPEN,
+ // For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
// make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
// by the Transitions framework to simplify Task opening transitions.
if (TransitionUtil.isOpeningType(info.getType())) {
for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getLeash() == null || change == pipChange) continue;
+ if (change.getLeash() == null) continue;
if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
startTransaction.setAlpha(change.getLeash(), 1f);
}
}
}
- PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, startBounds, startBounds, endBounds,
- sourceRectHint, PipEnterExitAnimator.BOUNDS_ENTER, Surface.ROTATION_0);
-
- tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
- this::finishInner);
- finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
-
- animator.setAnimationEndCallback(() ->
- finishCallback.onTransitionFinished(finishWct));
-
+ PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
+ startTransaction, finishTransaction, endBounds, sourceRectHint, Surface.ROTATION_0);
+ animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange));
+ animator.setAnimationEndCallback(this::finishInner);
animator.start();
return true;
}
@@ -452,11 +438,8 @@ public class PipTransition extends PipTransitionController implements
PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipLeash, startTransaction,
PipAlphaAnimator.FADE_IN);
- animator.setAnimationEndCallback(() -> {
- finishCallback.onTransitionFinished(null);
- // This should update the pip transition state accordingly after we stop playing.
- finishInner();
- });
+ // This should update the pip transition state accordingly after we stop playing.
+ animator.setAnimationEndCallback(this::finishInner);
animator.start();
return true;
@@ -510,9 +493,9 @@ public class PipTransition extends PipTransitionController implements
sourceRectHint = mPipTaskListener.getPictureInPictureParams().getSourceRectHint();
}
- PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
+ PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash,
startTransaction, finishTransaction, endBounds, startBounds, endBounds,
- sourceRectHint, PipEnterExitAnimator.BOUNDS_EXIT, Surface.ROTATION_0);
+ sourceRectHint, Surface.ROTATION_0);
animator.setAnimationEndCallback(() -> {
mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
@@ -631,6 +614,7 @@ public class PipTransition extends PipTransitionController implements
//
private void finishInner() {
+ finishTransition(null /* tx */);
if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
startOverlayFadeoutAnimation();
} else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
@@ -652,6 +636,7 @@ public class PipTransition extends PipTransitionController implements
}
if (mFinishCallback != null) {
mFinishCallback.onTransitionFinished(wct);
+ mFinishCallback = null;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index a0b7a29cee98..6086801491e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -38,8 +38,8 @@ import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.window.DesktopModeFlags;
import android.window.WindowContainerToken;
-import android.window.flags.DesktopModeFlags;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
index 0cbbb715cde6..1af99f974a28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -21,7 +21,7 @@ import android.os.IBinder
import android.util.ArrayMap
import android.view.SurfaceControl
import android.window.TransitionInfo
-import android.window.flags.DesktopModeFlags
+import android.window.DesktopModeFlags
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
index 399e39a920fc..6d01e247b48d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -16,7 +16,8 @@
package com.android.wm.shell.transition;
-import static android.view.Display.INVALID_DISPLAY;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
@@ -24,10 +25,11 @@ import static com.android.window.flags.Flags.enableDisplayFocusInShellTransition
import static com.android.wm.shell.transition.Transitions.TransitionObserver;
import android.annotation.NonNull;
-import android.os.IBinder;
+import android.app.ActivityManager.RunningTaskInfo;
import android.os.RemoteException;
+import android.util.ArraySet;
import android.util.Slog;
-import android.view.SurfaceControl;
+import android.util.SparseArray;
import android.window.TransitionInfo;
import com.android.wm.shell.shared.FocusTransitionListener;
@@ -43,44 +45,64 @@ import java.util.concurrent.Executor;
* It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
* and callers within the process via {@link FocusTransitionListener}.
*/
-public class FocusTransitionObserver implements TransitionObserver {
+public class FocusTransitionObserver {
private static final String TAG = FocusTransitionObserver.class.getSimpleName();
private IFocusTransitionListener mRemoteListener;
private final Map<FocusTransitionListener, Executor> mLocalListeners =
new HashMap<>();
- private int mFocusedDisplayId = INVALID_DISPLAY;
+ private int mFocusedDisplayId = DEFAULT_DISPLAY;
+ private final SparseArray<RunningTaskInfo> mFocusedTaskOnDisplay = new SparseArray<>();
+
+ private final ArraySet<RunningTaskInfo> mTmpTasksToBeNotified = new ArraySet<>();
public FocusTransitionObserver() {}
- @Override
- public void onTransitionReady(@NonNull IBinder transition,
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
+ /**
+ * Update display/window focus state from the given transition info and notifies changes if any.
+ */
+ public void updateFocusState(@NonNull TransitionInfo info) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
final List<TransitionInfo.Change> changes = info.getChanges();
for (int i = changes.size() - 1; i >= 0; i--) {
final TransitionInfo.Change change = changes.get(i);
+
+ final RunningTaskInfo task = change.getTaskInfo();
+ if (task != null
+ && (change.hasFlags(FLAG_MOVED_TO_TOP) || change.getMode() == TRANSIT_OPEN)) {
+ final RunningTaskInfo lastFocusedTaskOnDisplay =
+ mFocusedTaskOnDisplay.get(task.displayId);
+ if (lastFocusedTaskOnDisplay != null) {
+ mTmpTasksToBeNotified.add(lastFocusedTaskOnDisplay);
+ }
+ mTmpTasksToBeNotified.add(task);
+ mFocusedTaskOnDisplay.put(task.displayId, task);
+ }
+
if (change.hasFlags(FLAG_IS_DISPLAY) && change.hasFlags(FLAG_MOVED_TO_TOP)) {
if (mFocusedDisplayId != change.getEndDisplayId()) {
+ final RunningTaskInfo lastGloballyFocusedTask =
+ mFocusedTaskOnDisplay.get(mFocusedDisplayId);
+ if (lastGloballyFocusedTask != null) {
+ mTmpTasksToBeNotified.add(lastGloballyFocusedTask);
+ }
mFocusedDisplayId = change.getEndDisplayId();
notifyFocusedDisplayChanged();
+ final RunningTaskInfo currentGloballyFocusedTask =
+ mFocusedTaskOnDisplay.get(mFocusedDisplayId);
+ if (currentGloballyFocusedTask != null) {
+ mTmpTasksToBeNotified.add(currentGloballyFocusedTask);
+ }
}
- return;
}
}
+ mTmpTasksToBeNotified.forEach(this::notifyTaskFocusChanged);
+ mTmpTasksToBeNotified.clear();
}
- @Override
- public void onTransitionStarting(@NonNull IBinder transition) {}
-
- @Override
- public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
-
- @Override
- public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
-
/**
* Sets the focus transition listener that receives any transitions resulting in focus switch.
* This is for calls from outside the Shell, within the host process.
@@ -92,7 +114,10 @@ public class FocusTransitionObserver implements TransitionObserver {
return;
}
mLocalListeners.put(listener, executor);
- executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId));
+ executor.execute(() -> {
+ listener.onFocusedDisplayChanged(mFocusedDisplayId);
+ mTmpTasksToBeNotified.forEach(this::notifyTaskFocusChanged);
+ });
}
/**
@@ -120,13 +145,20 @@ public class FocusTransitionObserver implements TransitionObserver {
notifyFocusedDisplayChangedToRemote();
}
- /**
- * Notifies the listener that display focus has changed.
- */
- public void notifyFocusedDisplayChanged() {
+ private void notifyTaskFocusChanged(RunningTaskInfo task) {
+ final boolean isFocusedOnDisplay = isFocusedOnDisplay(task);
+ final boolean isFocusedGlobally = hasGlobalFocus(task);
+ mLocalListeners.forEach((listener, executor) ->
+ executor.execute(() -> listener.onFocusedTaskChanged(task.taskId,
+ isFocusedOnDisplay, isFocusedGlobally)));
+ }
+
+ private void notifyFocusedDisplayChanged() {
notifyFocusedDisplayChangedToRemote();
mLocalListeners.forEach((listener, executor) ->
- executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)));
+ executor.execute(() -> {
+ listener.onFocusedDisplayChanged(mFocusedDisplayId);
+ }));
}
private void notifyFocusedDisplayChangedToRemote() {
@@ -138,4 +170,23 @@ public class FocusTransitionObserver implements TransitionObserver {
}
}
}
+
+ private boolean isFocusedOnDisplay(@NonNull RunningTaskInfo task) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return task.isFocused;
+ }
+ final RunningTaskInfo focusedTaskOnDisplay = mFocusedTaskOnDisplay.get(task.displayId);
+ return focusedTaskOnDisplay != null && focusedTaskOnDisplay.taskId == task.taskId;
+ }
+
+ /**
+ * Checks whether the given task has focused globally on the system.
+ * (Note {@link RunningTaskInfo#isFocused} represents per-display focus.)
+ */
+ public boolean hasGlobalFocus(@NonNull RunningTaskInfo task) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return task.isFocused;
+ }
+ return task.displayId == mFocusedDisplayId && isFocusedOnDisplay(task);
+ }
}
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 d5e92e6a0e62..346f21b86e65 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
@@ -392,8 +392,6 @@ public class Transitions implements RemoteCallable<Transitions>,
mShellCommandHandler.addCommandCallback("transitions", this, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
-
- registerObserver(mFocusTransitionObserver);
}
public boolean isRegistered() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 839973fcbdd5..576c911d4459 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -16,7 +16,7 @@
package com.android.wm.shell.windowdecor;
-import static android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING;
+import static android.window.DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
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 bcf48d9ec2eb..c7feac51d2ab 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
@@ -56,7 +56,6 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -77,10 +76,10 @@ import android.view.SurfaceControl.Transaction;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Toast;
+import android.window.DesktopModeFlags;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
-import android.window.flags.DesktopModeFlags;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
@@ -103,8 +102,8 @@ 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.DesktopActivityOrientationChangeHandler;
-import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
@@ -133,14 +132,14 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import kotlin.Pair;
import kotlin.Unit;
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
/**
* View model for the window decoration with a caption and shadows. Works with
* {@link DesktopModeWindowDecoration}.
@@ -407,6 +406,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
@Override
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
+ mDesktopTasksController.setFreeformTaskTransitionStarter(transitionStarter);
}
@Override
@@ -774,11 +774,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
}
} else if (id == R.id.minimize_window) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
- final IBinder transition = mTaskOperations.minimizeTask(mTaskToken, wct);
- mDesktopTasksLimiter.ifPresent(limiter ->
- limiter.addPendingMinimizeChange(transition, mDisplayId, mTaskId));
+ mDesktopTasksController.minimizeTask(decoration.mTaskInfo);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 25d37fce7270..a78fb9b5e245 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -24,8 +24,8 @@ import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
-import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
-import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
+import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
+import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
@@ -72,9 +72,9 @@ import android.view.ViewConfiguration;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.ImageButton;
+import android.window.DesktopModeFlags;
import android.window.TaskSnapshot;
import android.window.WindowContainerTransaction;
-import android.window.flags.DesktopModeFlags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 38f9cfaca7ae..60c922293d80 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -27,7 +27,7 @@ import android.graphics.PointF;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
-import android.window.flags.DesktopModeFlags;
+import android.window.DesktopModeFlags;
import androidx.annotation.NonNull;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index d726f5083eb6..33d1c260cb84 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -18,7 +18,7 @@ package com.android.wm.shell.windowdecor;
import static android.view.InputDevice.SOURCE_MOUSE;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
-import static android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_EDGE_DRAG_RESIZE;
+import static android.window.DesktopModeFlags.ENABLE_WINDOWING_EDGE_DRAG_RESIZE;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
index 68a58ee0a370..376cd2a78baf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -33,7 +33,7 @@ import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.content.ContextCompat
import com.android.wm.shell.R
-import android.window.flags.DesktopModeFlags
+import android.window.DesktopModeFlags
private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350
private const val MAX_DRAWABLE_ALPHA = 255
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 52bf40062cdb..c2af1d45e76f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -49,7 +49,7 @@ import com.android.internal.R.attr.materialColorSurfaceDim
import com.android.window.flags.Flags
import com.android.window.flags.Flags.enableMinimizeButton
import com.android.wm.shell.R
-import android.window.flags.DesktopModeFlags
+import android.window.DesktopModeFlags
import com.android.wm.shell.windowdecor.MaximizeButtonView
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.OPACITY_100
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
index cae609526c65..2e9effb44d67 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -15,23 +15,39 @@
*/
package com.android.wm.shell.desktopmode
+import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
+import android.os.Binder
import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TransitionFlags
+import android.view.WindowManager.TransitionType
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -40,14 +56,18 @@ import org.mockito.kotlin.whenever
/**
* Tests for [DesktopFullImmersiveTransitionHandler].
*
- * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
@Mock private lateinit var mockTransitions: Transitions
private lateinit var desktopRepository: DesktopRepository
+ @Mock private lateinit var mockDisplayController: DisplayController
+ @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
private val transactionSupplier = { SurfaceControl.Transaction() }
private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
@@ -57,19 +77,22 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
desktopRepository = DesktopRepository(
context, ShellInit(TestShellExecutor()), mock(), mock()
)
+ whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY))
+ .thenReturn(DisplayLayout())
immersiveHandler = DesktopFullImmersiveTransitionHandler(
transitions = mockTransitions,
desktopRepository = desktopRepository,
- transactionSupplier = transactionSupplier
+ displayController = mockDisplayController,
+ shellTaskOrganizer = mockShellTaskOrganizer,
+ transactionSupplier = transactionSupplier,
)
}
@Test
fun enterImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -77,8 +100,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
immersive = false
)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
}
@@ -86,9 +109,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
@Test
fun exitImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -96,8 +118,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
immersive = true
)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
}
@@ -105,28 +127,251 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
@Test
fun enterImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.moveTaskToImmersive(task)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
}
@Test
fun exitImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.moveTaskToNonImmersive(task)
+
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_inImmersive_addsPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isTrue()
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_notInImmersive_doesNotAddPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_addsPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotAddPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_removesPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_updatesRepository() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ private fun createTransitionInfo(
+ @TransitionType type: Int = TRANSIT_CHANGE,
+ @TransitionFlags flags: Int = 0,
+ changes: List<TransitionInfo.Change> = emptyList()
+ ): TransitionInfo = TransitionInfo(type, flags).apply {
+ changes.forEach { change -> addChange(change) }
+ }
+
+ private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
+ this.changes.any { change ->
+ change.key == token.asBinder()
+ && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 1308114febbc..e20f0ecb1f3b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -957,6 +957,15 @@ class DesktopRepositoryTest : ShellTestCase() {
assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty()
}
+ @Test
+ fun getTaskInFullImmersiveState_byDisplay() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1)
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
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 27deb0b6abf6..b3c10d64c3a3 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
@@ -42,6 +42,7 @@ import android.graphics.Rect
import android.os.Binder
import android.os.Bundle
import android.os.Handler
+import android.os.IBinder
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
@@ -99,6 +100,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit
import com.android.wm.shell.desktopmode.persistence.Desktop
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.draganddrop.DragAndDropController
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.recents.RecentTasksController
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
@@ -144,13 +146,11 @@ import org.mockito.Mockito
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -201,6 +201,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var mockSurface: SurfaceControl
@Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
+ @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
@Mock private lateinit var mockHandler: Handler
@Mock lateinit var persistentRepository: DesktopPersistentRepository
@@ -266,6 +267,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller = createController()
controller.setSplitScreenController(splitScreenController)
+ controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
shellInit.init()
@@ -1542,75 +1544,142 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun onDesktopWindowMinimize_noActiveTask_doesntUpdateTransaction() {
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = 1)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = false)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ val wallpaperToken = MockToken().token()
+ taskRepository.wallpaperActivityToken = wallpaperToken
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntUpdateTransaction() {
- val task = setUpFreeformTask()
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK
+ }
}
@Test
fun onDesktopWindowMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
// The only active task is being minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntUpdateTransaction() {
+ fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
- val wct = WindowContainerTransaction()
// The only active task is already minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_multipleActiveTasks_doesntUpdateTransaction() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
+ fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() {
+ val task1 = setUpFreeformTask(active = true)
+ setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task1)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
+ val task1 = setUpFreeformTask(active = true)
+ val task2 = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
- val wct = WindowContainerTransaction()
// task1 is the only visible task as task2 is minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
+ controller.minimizeTask(task1)
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ // Adds remove wallpaper operation
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_triesToExitImmersive() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task))
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ val runOnTransit = RunOnStartTransitionCallback()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task)))
+ .thenReturn(runOnTransit)
+
+ controller.minimizeTask(task)
+
+ assertThat(runOnTransit.invocations).isEqualTo(1)
+ assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
}
@Test
@@ -3166,27 +3235,23 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun toggleImmersive_enter_resizesToDisplayBounds() {
+ fun toggleImmersive_enter_movesToImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, Rect())
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task)
}
@Test
- fun toggleImmersive_exit_resizesToStableBounds() {
+ fun toggleImmersive_exit_movesToNonImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, STABLE_BOUNDS)
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3198,7 +3263,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3210,7 +3275,113 @@ class DesktopTasksControllerTest : ShellTestCase() {
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler, never()).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task)
+ }
+
+ @Test
+ fun moveTaskToDesktop_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToDesktop_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() {
+ markTaskVisible(setUpFreeformTask())
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ @Test
+ fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() {
+ setUpFreeformTask()
+ val task = setUpFullscreenTask()
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
+ var invocations = 0
+ private set
+ var lastInvoked: IBinder? = null
+ private set
+
+ override fun invoke(transition: IBinder) {
+ invocations++
+ lastInvoked = transition
+ }
+ }
+
+ private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) {
+ assertThat(invocations).isEqualTo(1)
+ assertThat(lastInvoked).isEqualTo(transition)
}
/**
@@ -3291,18 +3462,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
private fun setUpFreeformTask(
displayId: Int = DEFAULT_DISPLAY,
bounds: Rect? = null,
- active: Boolean = true
+ active: Boolean = true,
+ background: Boolean = false,
): RunningTaskInfo {
val task = createFreeformTask(displayId, bounds)
val activityInfo = ActivityInfo()
task.topActivityInfo = activityInfo
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ if (background) {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+ whenever(recentTasksController.findTaskInBackground(task.taskId))
+ .thenReturn(createTaskInfo(task.taskId))
+ } else {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ }
if (active) {
taskRepository.addActiveTask(displayId, task.taskId)
taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true)
}
taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
- runningTasks.add(task)
+ if (!background) {
+ runningTasks.add(task)
+ }
return task
}
@@ -3556,6 +3736,21 @@ private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowC
assertThat(op.container).isEqualTo(token.asBinder())
}
+private fun WindowContainerTransaction.assertNoRemoveAt(index: Int, token: WindowContainerToken) {
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
+}
+
+private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowContainerToken) {
+
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
+}
+
private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
assertIndexInBounds(index)
val op = hierarchyOps[index]
@@ -3578,13 +3773,6 @@ private fun WindowContainerTransaction.assertLaunchTaskAt(
.isEqualTo(windowingMode)
}
-private fun WindowContainerTransaction.hasBoundsChange(
- token: WindowContainerToken,
- bounds: Rect
-): Boolean = this.changes.any { change ->
- change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
-}
-
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 36e0427a7e22..f95b0d1e7287 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -178,6 +178,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
mFreeformTaskListener.onTaskVanished(task);
verify(mDesktopRepository, never()).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopRepository).removeClosingTask(task.taskId);
verify(mDesktopRepository).removeFreeformTask(task.displayId, task.taskId);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index d4a319ef7195..7ae0bcd13681 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -22,7 +22,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
-import static android.view.WindowManager.TRANSIT_CHANGE;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -44,18 +43,14 @@ import android.window.WindowContainerToken;
import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
-
-import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
-import java.util.Optional;
-
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
@@ -80,6 +75,9 @@ public class FreeformTaskTransitionObserverTest {
private WindowDecorViewModel mWindowDecorViewModel;
@Mock
private TaskChangeListener mTaskChangeListener;
+ @Mock
+ private FocusTransitionObserver mFocusTransitionObserver;
+
private FreeformTaskTransitionObserver mTransitionObserver;
@Before
@@ -95,7 +93,7 @@ public class FreeformTaskTransitionObserverTest {
mTransitionObserver = new FreeformTaskTransitionObserver(
context, mShellInit, mTransitions,
Optional.of(mDesktopFullImmersiveTransitionHandler),
- mWindowDecorViewModel, Optional.of(mTaskChangeListener));
+ mWindowDecorViewModel, Optional.of(mTaskChangeListener), mFocusTransitionObserver);
final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
Runnable.class);
@@ -331,7 +329,7 @@ public class FreeformTaskTransitionObserverTest {
mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
- verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info);
}
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
index d63158c29688..015ea20767e9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -23,6 +23,7 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -30,9 +31,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import android.app.ActivityManager.RunningTaskInfo;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -43,17 +41,11 @@ import android.window.TransitionInfo.TransitionMode;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
import com.android.window.flags.Flags;
-import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.shared.IFocusTransitionListener;
-import com.android.wm.shell.shared.TransactionPool;
-import com.android.wm.shell.sysui.ShellController;
-import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.shared.FocusTransitionListener;
import org.junit.Before;
import org.junit.Rule;
@@ -75,57 +67,64 @@ public class FocusTransitionObserverTest extends ShellTestCase {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- private IFocusTransitionListener mListener;
- private Transitions mTransition;
+ private FocusTransitionListener mListener;
+ private final TestShellExecutor mShellExecutor = new TestShellExecutor();
private FocusTransitionObserver mFocusTransitionObserver;
@Before
public void setUp() {
- mListener = mock(IFocusTransitionListener.class);
- when(mListener.asBinder()).thenReturn(mock(IBinder.class));
-
+ mListener = mock(FocusTransitionListener.class);
mFocusTransitionObserver = new FocusTransitionObserver();
- mTransition =
- new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(),
- mock(ShellInit.class), mock(ShellController.class),
- mock(ShellTaskOrganizer.class), mock(TransactionPool.class),
- mock(DisplayController.class), new TestShellExecutor(),
- new Handler(Looper.getMainLooper()), new TestShellExecutor(),
- mock(HomeTransitionObserver.class),
- mFocusTransitionObserver);
- mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener);
+ mFocusTransitionObserver.setLocalFocusTransitionListener(mListener, mShellExecutor);
+ mShellExecutor.flushAll();
+ clearInvocations(mListener);
}
@Test
- public void testOnlyDisplayChangeAffectsDisplayFocus() throws RemoteException {
- final IBinder binder = mock(IBinder.class);
+ public void testBasicTaskAndDisplayFocusSwitch() throws RemoteException {
final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
- // Open a task on the secondary display, but it doesn't change display focus because it only
- // has a task change.
+ // First, open a task on the default display.
TransitionInfo info = mock(TransitionInfo.class);
final List<TransitionInfo.Change> changes = new ArrayList<>();
- setupTaskChange(changes, 123 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
- true /* focused */);
+ setupTaskChange(changes, 1 /* taskId */, TRANSIT_OPEN,
+ DEFAULT_DISPLAY, true /* focused */);
when(info.getChanges()).thenReturn(changes);
- mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ mFocusTransitionObserver.updateFocusState(info);
+ mShellExecutor.flushAll();
+ verify(mListener, never()).onFocusedDisplayChanged(anyInt());
+ verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */,
+ true /* isFocusedOnDisplay */, true /* isFocusedGlobally */);
clearInvocations(mListener);
- // Moving the secondary display to front must change display focus to it.
- changes.clear();
+ // Open a task on the secondary display.
+ setupTaskChange(changes, 2 /* taskId */, TRANSIT_OPEN,
+ SECONDARY_DISPLAY_ID, true /* focused */);
setupDisplayToTopChange(changes, SECONDARY_DISPLAY_ID);
when(info.getChanges()).thenReturn(changes);
- mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ mFocusTransitionObserver.updateFocusState(info);
+ mShellExecutor.flushAll();
verify(mListener, times(1))
.onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */,
+ true /* isFocusedOnDisplay */, false /* isFocusedGlobally */);
+ verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */,
+ true /* isFocusedOnDisplay */, true /* isFocusedGlobally */);
+ clearInvocations(mListener);
- // Moving the secondary display to front must change display focus back to it.
+ // Moving only the default display back to front, and verify that affected tasks are also
+ // notified.
changes.clear();
setupDisplayToTopChange(changes, DEFAULT_DISPLAY);
when(info.getChanges()).thenReturn(changes);
- mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ mFocusTransitionObserver.updateFocusState(info);
+ mShellExecutor.flushAll();
+ verify(mListener, times(1))
+ .onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */,
+ true /* isFocusedOnDisplay */, true /* isFocusedGlobally */);
+ verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */,
+ true /* isFocusedOnDisplay */, false /* isFocusedGlobally */);
}
private void setupTaskChange(List<TransitionInfo.Change> changes, int taskId,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 4aa7e18b4b84..e3e817ca3e87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -126,7 +126,6 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.any
-import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
@@ -455,18 +454,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
onClickListenerCaptor.value.onClick(view)
- val transactionCaptor = argumentCaptor<WindowContainerTransaction>()
- verify(mockFreeformTaskTransitionStarter)
- .startMinimizedModeTransition(transactionCaptor.capture())
- val wct = transactionCaptor.firstValue
-
- verify(mockTasksLimiter).addPendingMinimizeChange(
- anyOrNull(), eq(DEFAULT_DISPLAY), eq(decor.mTaskInfo.taskId))
-
- assertEquals(1, wct.getHierarchyOps().size)
- assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REORDER, wct.getHierarchyOps().get(0).getType())
- assertFalse(wct.getHierarchyOps().get(0).getToTop())
- assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
+ verify(mockDesktopTasksController).minimizeTask(decor.mTaskInfo)
}
@Test
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index f066e4620675..3ecd82b074a1 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -65,13 +65,7 @@ struct Idmap_data_header {
uint32_t string_pool_index_offset;
};
-struct Idmap_target_entry {
- uint32_t target_id;
- uint32_t overlay_id;
-};
-
struct Idmap_target_entry_inline {
- uint32_t target_id;
uint32_t start_value_index;
uint32_t value_count;
};
@@ -81,10 +75,9 @@ struct Idmap_target_entry_inline_value {
Res_value value;
};
-struct Idmap_overlay_entry {
- uint32_t overlay_id;
- uint32_t target_id;
-};
+static constexpr uint32_t convert_dev_target_id(uint32_t dev_target_id) {
+ return (0x00FFFFFFU & dtohl(dev_target_id));
+}
OverlayStringPool::OverlayStringPool(const LoadedIdmap* loaded_idmap)
: data_header_(loaded_idmap->data_header_),
@@ -117,27 +110,29 @@ size_t OverlayStringPool::size() const {
}
OverlayDynamicRefTable::OverlayDynamicRefTable(const Idmap_data_header* data_header,
- const Idmap_overlay_entry* entries,
+ Idmap_overlay_entries entries,
uint8_t target_assigned_package_id)
: data_header_(data_header),
entries_(entries),
- target_assigned_package_id_(target_assigned_package_id) {}
+ target_assigned_package_id_(target_assigned_package_id) {
+}
status_t OverlayDynamicRefTable::lookupResourceId(uint32_t* resId) const {
- const Idmap_overlay_entry* first_entry = entries_;
- const Idmap_overlay_entry* end_entry = entries_ + dtohl(data_header_->overlay_entry_count);
- auto entry = std::lower_bound(first_entry, end_entry, *resId,
- [](const Idmap_overlay_entry& e1, const uint32_t overlay_id) {
- return dtohl(e1.overlay_id) < overlay_id;
- });
-
- if (entry == end_entry || dtohl(entry->overlay_id) != *resId) {
+ const auto count = dtohl(data_header_->overlay_entry_count);
+ const auto overlay_it_end = entries_.overlay_id + count;
+ const auto entry_it = std::lower_bound(entries_.overlay_id, overlay_it_end, *resId,
+ [](uint32_t dev_overlay_id, uint32_t overlay_id) {
+ return dtohl(dev_overlay_id) < overlay_id;
+ });
+
+ if (entry_it == overlay_it_end || dtohl(*entry_it) != *resId) {
// A mapping for the target resource id could not be found.
return DynamicRefTable::lookupResourceId(resId);
}
- *resId = (0x00FFFFFFU & dtohl(entry->target_id))
- | (((uint32_t) target_assigned_package_id_) << 24U);
+ const auto index = entry_it - entries_.overlay_id;
+ *resId = convert_dev_target_id(entries_.target_id[index]) |
+ (((uint32_t)target_assigned_package_id_) << 24U);
return NO_ERROR;
}
@@ -145,12 +140,10 @@ status_t OverlayDynamicRefTable::lookupResourceIdNoRewrite(uint32_t* resId) cons
return DynamicRefTable::lookupResourceId(resId);
}
-IdmapResMap::IdmapResMap(const Idmap_data_header* data_header,
- const Idmap_target_entry* entries,
- const Idmap_target_entry_inline* inline_entries,
+IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries,
+ Idmap_target_inline_entries inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
- const ConfigDescription* configs,
- uint8_t target_assigned_package_id,
+ const ConfigDescription* configs, uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table)
: data_header_(data_header),
entries_(entries),
@@ -158,7 +151,8 @@ IdmapResMap::IdmapResMap(const Idmap_data_header* data_header,
inline_entry_values_(inline_entry_values),
configurations_(configs),
target_assigned_package_id_(target_assigned_package_id),
- overlay_ref_table_(overlay_ref_table) { }
+ overlay_ref_table_(overlay_ref_table) {
+}
IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const {
if ((target_res_id >> 24U) != target_assigned_package_id_) {
@@ -171,15 +165,15 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const {
target_res_id &= 0x00FFFFFFU;
// Check if the target resource is mapped to an overlay resource.
- auto first_entry = entries_;
- auto end_entry = entries_ + dtohl(data_header_->target_entry_count);
- auto entry = std::lower_bound(first_entry, end_entry, target_res_id,
- [](const Idmap_target_entry& e, const uint32_t target_id) {
- return (0x00FFFFFFU & dtohl(e.target_id)) < target_id;
- });
-
- if (entry != end_entry && (0x00FFFFFFU & dtohl(entry->target_id)) == target_res_id) {
- uint32_t overlay_resource_id = dtohl(entry->overlay_id);
+ const auto target_end = entries_.target_id + dtohl(data_header_->target_entry_count);
+ auto target_it = std::lower_bound(entries_.target_id, target_end, target_res_id,
+ [](uint32_t dev_target_id, uint32_t target_id) {
+ return convert_dev_target_id(dev_target_id) < target_id;
+ });
+
+ if (target_it != target_end && convert_dev_target_id(*target_it) == target_res_id) {
+ const auto index = target_it - entries_.target_id;
+ uint32_t overlay_resource_id = dtohl(entries_.overlay_id[index]);
// Lookup the resource without rewriting the overlay resource id back to the target resource id
// being looked up.
overlay_ref_table_->lookupResourceIdNoRewrite(&overlay_resource_id);
@@ -187,20 +181,22 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const {
}
// Check if the target resources is mapped to an inline table entry.
- auto first_inline_entry = inline_entries_;
- auto end_inline_entry = inline_entries_ + dtohl(data_header_->target_inline_entry_count);
- auto inline_entry = std::lower_bound(first_inline_entry, end_inline_entry, target_res_id,
- [](const Idmap_target_entry_inline& e,
- const uint32_t target_id) {
- return (0x00FFFFFFU & dtohl(e.target_id)) < target_id;
- });
-
- if (inline_entry != end_inline_entry &&
- (0x00FFFFFFU & dtohl(inline_entry->target_id)) == target_res_id) {
+ const auto inline_entry_target_end =
+ inline_entries_.target_id + dtohl(data_header_->target_inline_entry_count);
+ const auto inline_entry_target_it =
+ std::lower_bound(inline_entries_.target_id, inline_entry_target_end, target_res_id,
+ [](uint32_t dev_target_id, uint32_t target_id) {
+ return convert_dev_target_id(dev_target_id) < target_id;
+ });
+
+ if (inline_entry_target_it != inline_entry_target_end &&
+ convert_dev_target_id(*inline_entry_target_it) == target_res_id) {
+ const auto index = inline_entry_target_it - inline_entries_.target_id;
std::map<ConfigDescription, Res_value> values_map;
- for (int i = 0; i < inline_entry->value_count; i++) {
- const auto& value = inline_entry_values_[inline_entry->start_value_index + i];
- const auto& config = configurations_[value.config_index];
+ const auto& inline_entry = inline_entries_.entry[index];
+ for (int i = 0; i < dtohl(inline_entry.value_count); i++) {
+ const auto& value = inline_entry_values_[dtohl(inline_entry.start_value_index) + i];
+ const auto& config = configurations_[dtohl(value.config_index)];
values_map[config] = value.value;
}
return Result(std::move(values_map));
@@ -210,15 +206,15 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const {
namespace {
template <typename T>
-const T* ReadType(const uint8_t** in_out_data_ptr, size_t* in_out_size, const std::string& label,
+const T* ReadType(const uint8_t** in_out_data_ptr, size_t* in_out_size, const char* label,
size_t count = 1) {
if (!util::IsFourByteAligned(*in_out_data_ptr)) {
- LOG(ERROR) << "Idmap " << label << " is not word aligned.";
+ LOG(ERROR) << "Idmap " << label << " in " << __func__ << " is not word aligned.";
return {};
}
if ((*in_out_size / sizeof(T)) < count) {
- LOG(ERROR) << "Idmap too small for the number of " << label << " entries ("
- << count << ").";
+ LOG(ERROR) << "Idmap too small for the number of " << label << " in " << __func__
+ << " entries (" << count << ").";
return nullptr;
}
auto data_ptr = *in_out_data_ptr;
@@ -229,8 +225,8 @@ const T* ReadType(const uint8_t** in_out_data_ptr, size_t* in_out_size, const st
}
std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size_t* in_out_size,
- const std::string& label) {
- const auto* len = ReadType<uint32_t>(in_out_data_ptr, in_out_size, label + " length");
+ const char* label) {
+ const auto* len = ReadType<uint32_t>(in_out_data_ptr, in_out_size, label);
if (len == nullptr) {
return {};
}
@@ -242,7 +238,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
const uint32_t padding_size = (4U - ((size_t)*in_out_data_ptr & 0x3U)) % 4U;
for (uint32_t i = 0; i < padding_size; i++) {
if (**in_out_data_ptr != 0) {
- LOG(ERROR) << " Idmap padding of " << label << " is non-zero.";
+ LOG(ERROR) << " Idmap padding of " << label << " in " << __func__ << " is non-zero.";
return {};
}
*in_out_data_ptr += sizeof(uint8_t);
@@ -258,12 +254,10 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
#endif
LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
- const Idmap_data_header* data_header,
- const Idmap_target_entry* target_entries,
- const Idmap_target_entry_inline* target_inline_entries,
+ const Idmap_data_header* data_header, Idmap_target_entries target_entries,
+ Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
- const ConfigDescription* configs,
- const Idmap_overlay_entry* overlay_entries,
+ const ConfigDescription* configs, Idmap_overlay_entries overlay_entries,
std::unique_ptr<ResStringPool>&& string_pool,
std::string_view overlay_apk_path, std::string_view target_apk_path)
: header_(header),
@@ -274,10 +268,12 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head
configurations_(configs),
overlay_entries_(overlay_entries),
string_pool_(std::move(string_pool)),
- idmap_fd_(android::base::utf8::open(idmap_path.c_str(), O_RDONLY|O_CLOEXEC|O_BINARY|O_PATH)),
+ idmap_fd_(
+ android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)),
overlay_apk_path_(overlay_apk_path),
target_apk_path_(target_apk_path),
- idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) {}
+ idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) {
+}
std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) {
ATRACE_CALL();
@@ -319,14 +315,21 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
if (data_header == nullptr) {
return {};
}
- auto target_entries = ReadType<Idmap_target_entry>(&data_ptr, &data_size, "target",
- dtohl(data_header->target_entry_count));
- if (target_entries == nullptr) {
+ Idmap_target_entries target_entries{
+ .target_id = ReadType<uint32_t>(&data_ptr, &data_size, "entries.target_id",
+ dtohl(data_header->target_entry_count)),
+ .overlay_id = ReadType<uint32_t>(&data_ptr, &data_size, "entries.overlay_id",
+ dtohl(data_header->target_entry_count)),
+ };
+ if (!target_entries.target_id || !target_entries.overlay_id) {
return {};
}
- auto target_inline_entries = ReadType<Idmap_target_entry_inline>(
- &data_ptr, &data_size, "target inline", dtohl(data_header->target_inline_entry_count));
- if (target_inline_entries == nullptr) {
+ Idmap_target_inline_entries target_inline_entries{
+ .target_id = ReadType<uint32_t>(&data_ptr, &data_size, "target inline.target_id",
+ dtohl(data_header->target_inline_entry_count)),
+ .entry = ReadType<Idmap_target_entry_inline>(&data_ptr, &data_size, "target inline.entry",
+ dtohl(data_header->target_inline_entry_count))};
+ if (!target_inline_entries.target_id || !target_inline_entries.entry) {
return {};
}
@@ -344,9 +347,13 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
return {};
}
- auto overlay_entries = ReadType<Idmap_overlay_entry>(&data_ptr, &data_size, "target inline",
- dtohl(data_header->overlay_entry_count));
- if (overlay_entries == nullptr) {
+ Idmap_overlay_entries overlay_entries{
+ .overlay_id = ReadType<uint32_t>(&data_ptr, &data_size, "overlay entries.overlay_id",
+ dtohl(data_header->overlay_entry_count)),
+ .target_id = ReadType<uint32_t>(&data_ptr, &data_size, "overlay entries.target_id",
+ dtohl(data_header->overlay_entry_count)),
+ };
+ if (!overlay_entries.overlay_id || !overlay_entries.target_id) {
return {};
}
std::optional<std::string_view> string_pool = ReadString(&data_ptr, &data_size, "string pool");
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 64b1f0c6ed03..e213fbd22ab0 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -40,6 +40,19 @@ struct Idmap_target_entry_inline;
struct Idmap_target_entry_inline_value;
struct Idmap_overlay_entry;
+struct Idmap_target_entries {
+ const uint32_t* target_id = nullptr;
+ const uint32_t* overlay_id = nullptr;
+};
+struct Idmap_target_inline_entries {
+ const uint32_t* target_id = nullptr;
+ const Idmap_target_entry_inline* entry = nullptr;
+};
+struct Idmap_overlay_entries {
+ const uint32_t* overlay_id = nullptr;
+ const uint32_t* target_id = nullptr;
+};
+
// A string pool for overlay apk assets. The string pool holds the strings of the overlay resources
// table and additionally allows for loading strings from the idmap string pool. The idmap string
// pool strings are offset after the end of the overlay resource table string pool entries so
@@ -67,7 +80,7 @@ class OverlayDynamicRefTable : public DynamicRefTable {
private:
explicit OverlayDynamicRefTable(const Idmap_data_header* data_header,
- const Idmap_overlay_entry* entries,
+ Idmap_overlay_entries entries,
uint8_t target_assigned_package_id);
// Rewrites a compile-time overlay resource id to the runtime resource id of corresponding target
@@ -75,8 +88,8 @@ class OverlayDynamicRefTable : public DynamicRefTable {
status_t lookupResourceIdNoRewrite(uint32_t* resId) const;
const Idmap_data_header* data_header_;
- const Idmap_overlay_entry* entries_;
- const int8_t target_assigned_package_id_;
+ Idmap_overlay_entries entries_;
+ uint8_t target_assigned_package_id_;
friend LoadedIdmap;
friend IdmapResMap;
@@ -131,17 +144,15 @@ class IdmapResMap {
}
private:
- explicit IdmapResMap(const Idmap_data_header* data_header,
- const Idmap_target_entry* entries,
- const Idmap_target_entry_inline* inline_entries,
+ explicit IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries,
+ Idmap_target_inline_entries inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
- const ConfigDescription* configs,
- uint8_t target_assigned_package_id,
+ const ConfigDescription* configs, uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table);
const Idmap_data_header* data_header_;
- const Idmap_target_entry* entries_;
- const Idmap_target_entry_inline* inline_entries_;
+ Idmap_target_entries entries_;
+ Idmap_target_inline_entries inline_entries_;
const Idmap_target_entry_inline_value* inline_entry_values_;
const ConfigDescription* configurations_;
const uint8_t target_assigned_package_id_;
@@ -192,11 +203,11 @@ class LoadedIdmap {
const Idmap_header* header_;
const Idmap_data_header* data_header_;
- const Idmap_target_entry* target_entries_;
- const Idmap_target_entry_inline* target_inline_entries_;
+ Idmap_target_entries target_entries_;
+ Idmap_target_inline_entries target_inline_entries_;
const Idmap_target_entry_inline_value* inline_entry_values_;
const ConfigDescription* configurations_;
- const Idmap_overlay_entry* overlay_entries_;
+ const Idmap_overlay_entries overlay_entries_;
const std::unique_ptr<ResStringPool> string_pool_;
android::base::unique_fd idmap_fd_;
@@ -207,17 +218,13 @@ class LoadedIdmap {
private:
DISALLOW_COPY_AND_ASSIGN(LoadedIdmap);
- explicit LoadedIdmap(const std::string& idmap_path,
- const Idmap_header* header,
- const Idmap_data_header* data_header,
- const Idmap_target_entry* target_entries,
- const Idmap_target_entry_inline* target_inline_entries,
+ explicit LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
+ const Idmap_data_header* data_header, Idmap_target_entries target_entries,
+ Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values_,
- const ConfigDescription* configs,
- const Idmap_overlay_entry* overlay_entries,
+ const ConfigDescription* configs, Idmap_overlay_entries overlay_entries,
std::unique_ptr<ResStringPool>&& string_pool,
- std::string_view overlay_apk_path,
- std::string_view target_apk_path);
+ std::string_view overlay_apk_path, std::string_view target_apk_path);
friend OverlayStringPool;
};
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index c2648909386c..e330410ed1a0 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -48,7 +48,7 @@
namespace android {
constexpr const uint32_t kIdmapMagic = 0x504D4449u;
-constexpr const uint32_t kIdmapCurrentVersion = 0x00000009u;
+constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au;
// This must never change.
constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian)
diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap
index 8e847e81aa31..7e4b261cf109 100644
--- a/libs/androidfw/tests/data/overlay/overlay.idmap
+++ b/libs/androidfw/tests/data/overlay/overlay.idmap
Binary files differ
diff --git a/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp b/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
index e81cbfb508ae..c0ef4b14d53f 100644
--- a/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
+++ b/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
@@ -29,37 +29,6 @@ namespace android {
namespace uirenderer {
namespace skiapipeline {
-BackdropFilterDrawable::~BackdropFilterDrawable() {}
-
-bool BackdropFilterDrawable::prepareToDraw(SkCanvas* canvas, const RenderProperties& properties,
- int backdropImageWidth, int backdropImageHeight) {
- // the drawing bounds for blurred content.
- mDstBounds.setWH(properties.getWidth(), properties.getHeight());
-
- float alphaMultiplier = 1.0f;
- RenderNodeDrawable::setViewProperties(properties, canvas, &alphaMultiplier, true);
-
- // get proper subset for previous content.
- canvas->getTotalMatrix().mapRect(&mImageSubset, mDstBounds);
- SkRect imageSubset(mImageSubset);
- // ensure the subset is inside bounds of previous content.
- if (!mImageSubset.intersect(SkRect::MakeWH(backdropImageWidth, backdropImageHeight))) {
- return false;
- }
-
- // correct the drawing bounds if subset was changed.
- if (mImageSubset != imageSubset) {
- SkMatrix inverse;
- if (canvas->getTotalMatrix().invert(&inverse)) {
- inverse.mapRect(&mDstBounds, mImageSubset);
- }
- }
-
- // follow the alpha from the target RenderNode.
- mPaint.setAlpha(properties.layerProperties().alpha() * alphaMultiplier);
- return true;
-}
-
void BackdropFilterDrawable::onDraw(SkCanvas* canvas) {
const RenderProperties& properties = mTargetRenderNode->properties();
auto* backdropFilter = properties.layerProperties().getBackdropImageFilter();
@@ -68,27 +37,43 @@ void BackdropFilterDrawable::onDraw(SkCanvas* canvas) {
return;
}
- auto backdropImage = surface->makeImageSnapshot();
- // sync necessary properties from target RenderNode.
- if (!prepareToDraw(canvas, properties, backdropImage->width(), backdropImage->height())) {
+ SkRect srcBounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
+
+ float alphaMultiplier = 1.0f;
+ RenderNodeDrawable::setViewProperties(properties, canvas, &alphaMultiplier, true);
+ SkPaint paint;
+ paint.setAlpha(properties.layerProperties().alpha() * alphaMultiplier);
+
+ SkRect surfaceSubset;
+ canvas->getTotalMatrix().mapRect(&surfaceSubset, srcBounds);
+ if (!surfaceSubset.intersect(SkRect::MakeWH(surface->width(), surface->height()))) {
return;
}
- auto imageSubset = mImageSubset.roundOut();
+ auto backdropImage = surface->makeImageSnapshot(surfaceSubset.roundOut());
+
+ SkIRect imageBounds = SkIRect::MakeWH(backdropImage->width(), backdropImage->height());
+ SkIPoint offset;
+ SkIRect imageSubset;
+
#ifdef __ANDROID__
if (canvas->recordingContext()) {
backdropImage =
SkImages::MakeWithFilter(canvas->recordingContext(), backdropImage, backdropFilter,
- imageSubset, imageSubset, &mOutSubset, &mOutOffset);
+ imageBounds, imageBounds, &imageSubset, &offset);
} else
#endif
{
- backdropImage = SkImages::MakeWithFilter(backdropImage, backdropFilter, imageSubset,
- imageSubset, &mOutSubset, &mOutOffset);
+ backdropImage = SkImages::MakeWithFilter(backdropImage, backdropFilter, imageBounds,
+ imageBounds, &imageSubset, &offset);
}
- canvas->drawImageRect(backdropImage, SkRect::Make(mOutSubset), mDstBounds,
- SkSamplingOptions(SkFilterMode::kLinear), &mPaint,
- SkCanvas::kStrict_SrcRectConstraint);
+
+ canvas->save();
+ canvas->resetMatrix();
+ canvas->drawImageRect(backdropImage, SkRect::Make(imageSubset), surfaceSubset,
+ SkSamplingOptions(SkFilterMode::kLinear), &paint,
+ SkCanvas::kFast_SrcRectConstraint);
+ canvas->restore();
}
} // namespace skiapipeline
diff --git a/libs/hwui/pipeline/skia/BackdropFilterDrawable.h b/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
index 9e35837675ae..5e216a1fc3c3 100644
--- a/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
+++ b/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
@@ -37,23 +37,10 @@ public:
BackdropFilterDrawable(RenderNode* renderNode, SkCanvas* canvas)
: mTargetRenderNode(renderNode), mBounds(canvas->getLocalClipBounds()) {}
- ~BackdropFilterDrawable();
+ ~BackdropFilterDrawable() = default;
private:
RenderNode* mTargetRenderNode;
- SkPaint mPaint;
-
- SkRect mDstBounds;
- SkRect mImageSubset;
- SkIRect mOutSubset;
- SkIPoint mOutOffset;
-
- /**
- * Check all necessary properties before actual drawing.
- * Return true if ready to draw.
- */
- bool prepareToDraw(SkCanvas* canvas, const RenderProperties& properties, int backdropImageWidth,
- int backdropImageHeight);
protected:
void onDraw(SkCanvas* canvas) override;
diff --git a/libs/hwui/tests/common/scenes/BackdropBlur.cpp b/libs/hwui/tests/common/scenes/BackdropBlur.cpp
new file mode 100644
index 000000000000..a1133ffe96ef
--- /dev/null
+++ b/libs/hwui/tests/common/scenes/BackdropBlur.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <SkBlendMode.h>
+
+#include "SkImageFilter.h"
+#include "SkImageFilters.h"
+#include "TestSceneBase.h"
+#include "utils/Blur.h"
+
+class BackdropBlurAnimation : public TestScene {
+private:
+ std::unique_ptr<TestScene> listView;
+
+public:
+ explicit BackdropBlurAnimation(const TestScene::Options& opts) {
+ listView.reset(TestScene::testMap()["listview"].createScene(opts));
+ }
+
+ void createContent(int width, int height, Canvas& canvas) override {
+ sp<RenderNode> list = TestUtils::createNode(
+ 0, 0, width, height,
+ [this, width, height](RenderProperties& props, Canvas& canvas) {
+ props.setClipToBounds(false);
+ listView->createContent(width, height, canvas);
+ });
+
+ canvas.drawRenderNode(list.get());
+
+ int x = width / 8;
+ int y = height / 4;
+ sp<RenderNode> blurNode = TestUtils::createNode(
+ x, y, width - x, height - y, [](RenderProperties& props, Canvas& canvas) {
+ props.mutableOutline().setRoundRect(0, 0, props.getWidth(), props.getHeight(),
+ dp(16), 1);
+ props.mutableOutline().setShouldClip(true);
+ sk_sp<SkImageFilter> blurFilter = SkImageFilters::Blur(
+ Blur::convertRadiusToSigma(dp(8)), Blur::convertRadiusToSigma(dp(8)),
+ SkTileMode::kClamp, nullptr, nullptr);
+ props.mutateLayerProperties().setBackdropImageFilter(blurFilter.get());
+ canvas.drawColor(0x33000000, SkBlendMode::kSrcOver);
+ });
+
+ canvas.drawRenderNode(blurNode.get());
+ }
+
+ void doFrame(int frameNr) override { listView->doFrame(frameNr); }
+};
+
+static TestScene::Registrar _BackdropBlur(TestScene::Info{
+ "backdropblur", "A rounded rect that does a blur-behind of a sky animation.",
+ [](const TestScene::Options& opts) -> test::TestScene* {
+ return new BackdropBlurAnimation(opts);
+ }});
diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
index ca540874833c..4b29100c55a6 100644
--- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
@@ -1280,7 +1280,7 @@ RENDERTHREAD_TEST(BackdropFilterDrawable, drawing) {
canvas->drawDrawable(&backdropDrawable);
// the drawable is still visible, ok to draw.
EXPECT_EQ(2, canvas->mDrawCounter);
- EXPECT_EQ(SkRect::MakeLTRB(0, 0, CANVAS_WIDTH - 30, CANVAS_HEIGHT - 30), canvas->mDstBounds);
+ EXPECT_EQ(SkRect::MakeLTRB(30, 30, CANVAS_WIDTH, CANVAS_HEIGHT), canvas->mDstBounds);
canvas->translate(CANVAS_WIDTH, CANVAS_HEIGHT);
canvas->drawDrawable(&drawable);