diff options
Diffstat (limited to 'libs')
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 Binary files differindex 8e847e81aa31..7e4b261cf109 100644 --- a/libs/androidfw/tests/data/overlay/overlay.idmap +++ b/libs/androidfw/tests/data/overlay/overlay.idmap 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); |