diff options
| author | 2025-01-16 11:34:42 -0800 | |
|---|---|---|
| committer | 2025-01-22 16:12:47 -0800 | |
| commit | 5c73d96e34be4e9dffd751ac2b265ecbba7ffc94 (patch) | |
| tree | 7ba6c5db4a6f569c67af9f90fddd9b1d9f388c2b | |
| parent | e06e571f349f738a6233dd70b553f9738887c5d5 (diff) | |
Add a convert-from-bubble transition
This adds convert-from-bubble transition support. Unlike the
to-bubble transition, this one does NOT actually do an animation,
it is just the necessary "setup" logic required to synchronize
taking the task out of the taskview before dispatching to a
handler which will play the actual animation. The basic principle
is that the windowing-feature that "accepts" the task is the one
that animates it.
Because bubbles use taskview and taskviews are view-hierarchys,
removing the task from the taskview seamlessly is non-trivial.
In this case, there's a `pluck` utility added which posts
reparenting on the taskview's choreographer and then hides the
taskview. Once that happens, it can pass the task-surface onto
another transition handler.
This also introduces a bubbles-specific TaskViewController so
that it can implement moveTaskViewToFullscreen with this
bubble-specific transition.
Bug: 384976265
Test: BubbleTransitionsTest
Flag: com.android.wm.shell.enable_bubble_to_fullscreen
Change-Id: I5edd380200a9839c75e37a87fb9a244b62df702b
6 files changed, 339 insertions, 7 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index e68a98fb7f21..a65e69eee5fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -542,7 +542,7 @@ public class Bubble implements BubbleViewProvider { return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); } - BubbleTransitions.BubbleTransition getPreparingTransition() { + public BubbleTransitions.BubbleTransition getPreparingTransition() { return mPreparingTransition; } @@ -572,7 +572,8 @@ public class Bubble implements BubbleViewProvider { mIntentActive = false; } - private void cleanupTaskView() { + /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */ + public void cleanupTaskView() { if (mBubbleTaskView != null) { mBubbleTaskView.cleanup(); mBubbleTaskView = null; @@ -593,7 +594,7 @@ public class Bubble implements BubbleViewProvider { * <p>If we're switching between bar and floating modes, pass {@code false} on * {@code cleanupTaskView} to avoid recreating it in the new mode. */ - void cleanupViews(boolean cleanupTaskView) { + public void cleanupViews(boolean cleanupTaskView) { cleanupExpandedView(cleanupTaskView); mIconView = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d0f912ac2142..01c336d069aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -40,9 +40,11 @@ import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; +import android.app.TaskInfo; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -78,6 +80,8 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.window.ScreenCapture; import android.window.ScreenCapture.SynchronousScreenCaptureListener; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import androidx.annotation.MainThread; import androidx.annotation.Nullable; @@ -360,7 +364,7 @@ public class BubbleController implements ConfigurationChangeListener, } else { tvTransitions = taskViewTransitions; } - mTaskViewController = tvTransitions; + mTaskViewController = new BubbleTaskViewController(tvTransitions); mBubbleTransitions = new BubbleTransitions(transitions, organizer, taskViewRepository, data, tvTransitions, context); mTransitions = transitions; @@ -2076,7 +2080,12 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void removeBubble(Bubble removedBubble) { if (mLayerView != null) { + final BubbleTransitions.BubbleTransition bubbleTransit = + removedBubble.getPreparingTransition(); mLayerView.removeBubble(removedBubble, () -> { + if (bubbleTransit != null) { + bubbleTransit.continueCollapse(); + } if (!mBubbleData.hasBubbles() && !isStackExpanded()) { mLayerView.setVisibility(INVISIBLE); removeFromWindowManagerMaybe(); @@ -2691,7 +2700,18 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void collapseBubbles() { - mMainExecutor.execute(() -> mController.collapseStack()); + mMainExecutor.execute(() -> { + if (mBubbleData.getSelectedBubble() instanceof Bubble) { + if (((Bubble) mBubbleData.getSelectedBubble()).getPreparingTransition() + != null) { + // Currently preparing a transition which will, itself, collapse the bubble. + // For transition preparation, the timing of bubble-collapse must be in + // sync with the rest of the set-up. + return; + } + } + mController.collapseStack(); + }); } @Override @@ -3083,4 +3103,84 @@ public class BubbleController implements ConfigurationChangeListener, return mKeyToShownInShadeMap.get(key); } } + + private class BubbleTaskViewController implements TaskViewController { + private final TaskViewTransitions mBaseTransitions; + + BubbleTaskViewController(TaskViewTransitions baseTransitions) { + mBaseTransitions = baseTransitions; + } + + @Override + public void registerTaskView(TaskViewTaskController tv) { + mBaseTransitions.registerTaskView(tv); + } + + @Override + public void unregisterTaskView(TaskViewTaskController tv) { + mBaseTransitions.unregisterTaskView(tv); + } + + @Override + public void startShortcutActivity(@NonNull TaskViewTaskController destination, + @NonNull ShortcutInfo shortcut, @NonNull ActivityOptions options, + @Nullable Rect launchBounds) { + mBaseTransitions.startShortcutActivity(destination, shortcut, options, launchBounds); + } + + @Override + public void startActivity(@NonNull TaskViewTaskController destination, + @NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options, @Nullable Rect launchBounds) { + mBaseTransitions.startActivity(destination, pendingIntent, fillInIntent, + options, launchBounds); + } + + @Override + public void startRootTask(@NonNull TaskViewTaskController destination, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + @Nullable WindowContainerTransaction wct) { + mBaseTransitions.startRootTask(destination, taskInfo, leash, wct); + } + + @Override + public void removeTaskView(@NonNull TaskViewTaskController taskView, + @Nullable WindowContainerToken taskToken) { + mBaseTransitions.removeTaskView(taskView, taskToken); + } + + @Override + public void moveTaskViewToFullscreen(@NonNull TaskViewTaskController taskView) { + final TaskInfo tinfo = taskView.getTaskInfo(); + if (tinfo == null) { + return; + } + Bubble bub = null; + for (Bubble b : mBubbleData.getBubbles()) { + if (b.getTaskId() == tinfo.taskId) { + bub = b; + break; + } + } + if (bub == null) { + return; + } + mBubbleTransitions.startConvertFromBubble(bub, tinfo); + } + + @Override + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + mBaseTransitions.setTaskViewVisible(taskView, visible); + } + + @Override + public void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) { + mBaseTransitions.setTaskBounds(taskView, boundsOnScreen); + } + + @Override + public boolean isUsingShellTransitions() { + return mBaseTransitions.isUsingShellTransitions(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index e37844f53b11..29fb1a23017c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -18,6 +18,8 @@ package com.android.wm.shell.bubbles; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.View.INVISIBLE; import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; @@ -30,8 +32,10 @@ import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; import android.view.SurfaceView; +import android.view.View; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; @@ -53,6 +57,12 @@ import java.util.concurrent.Executor; public class BubbleTransitions { private static final String TAG = "BubbleTransitions"; + /** + * Multiplier used to convert a view elevation to an "equivalent" shadow-radius. This is the + * same multiple used by skia and surface-outsets in WMS. + */ + private static final float ELEVATION_TO_RADIUS = 2; + @NonNull final Transitions mTransitions; @NonNull final ShellTaskOrganizer mTaskOrganizer; @NonNull final TaskViewRepository mRepository; @@ -90,6 +100,44 @@ public class BubbleTransitions { } /** + * Starts a convert-from-bubble transition. + * + * @see ConvertFromBubble + */ + public BubbleTransition startConvertFromBubble(Bubble bubble, + TaskInfo taskInfo) { + ConvertFromBubble convert = new ConvertFromBubble(bubble, taskInfo); + return convert; + } + + /** + * Plucks the task-surface out of an ancestor view while making the view invisible. This helper + * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent). + */ + private void pluck(SurfaceControl taskLeash, View fromView, SurfaceControl dest, + float destX, float destY, float cornerRadius, SurfaceControl.Transaction t, + Runnable onPlucked) { + SurfaceControl.Transaction pluckT = new SurfaceControl.Transaction(); + pluckT.reparent(taskLeash, dest); + t.reparent(taskLeash, dest); + pluckT.setPosition(taskLeash, destX, destY); + t.setPosition(taskLeash, destX, destY); + pluckT.show(taskLeash); + pluckT.setAlpha(taskLeash, 1.f); + float shadowRadius = fromView.getElevation() * ELEVATION_TO_RADIUS; + pluckT.setShadowRadius(taskLeash, shadowRadius); + pluckT.setCornerRadius(taskLeash, cornerRadius); + t.setShadowRadius(taskLeash, shadowRadius); + t.setCornerRadius(taskLeash, cornerRadius); + + // Need to remove the taskview AFTER applying the startTransaction because it isn't + // synchronized. + pluckT.addTransactionCommittedListener(mMainExecutor, onPlucked::run); + fromView.getViewRootImpl().applyTransactionOnDraw(pluckT); + fromView.setVisibility(INVISIBLE); + } + + /** * Interface to a bubble-specific transition. Bubble transitions have a multi-step lifecycle * in order to coordinate with the bubble view logic. These steps are communicated on this * interface. @@ -98,6 +146,7 @@ public class BubbleTransitions { default void surfaceCreated() {} default void continueExpand() {} void skip(); + default void continueCollapse() {} } /** @@ -316,4 +365,154 @@ public class BubbleTransitions { } } } + + /** + * BubbleTransition that coordinates the setup for moving a task out of a bubble. The actual + * animation is owned by the "receiver" of the task; however, because Bubbles uses TaskView, + * we need to do some extra coordination work to get the task surface out of the view + * "seamlessly". + * + * The process here looks like: + * 1. Send transition to WM for leaving bubbles mode + * 2. in startAnimation, set-up a "pluck" operation to pull the task surface out of taskview + * 3. Once "plucked", remove the view (calls continueCollapse when surfaces can be cleaned-up) + * 4. Then re-dispatch the transition animation so that the "receiver" can animate it. + * + * So, constructor -> startAnimation -> continueCollapse -> re-dispatch. + */ + @VisibleForTesting + class ConvertFromBubble implements Transitions.TransitionHandler, BubbleTransition { + @NonNull final Bubble mBubble; + IBinder mTransition; + TaskInfo mTaskInfo; + SurfaceControl mTaskLeash; + SurfaceControl mRootLeash; + + ConvertFromBubble(@NonNull Bubble bubble, TaskInfo taskInfo) { + mBubble = bubble; + mTaskInfo = taskInfo; + + mBubble.setPreparingTransition(this); + WindowContainerTransaction wct = new WindowContainerTransaction(); + WindowContainerToken token = mTaskInfo.getToken(); + wct.setWindowingMode(token, WINDOWING_MODE_UNDEFINED); + wct.setAlwaysOnTop(token, false); + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(token, false); + mTaskViewTransitions.enqueueExternal( + mBubble.getTaskView().getController(), + () -> { + mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + return mTransition; + }); + } + + @Override + public void skip() { + mBubble.setPreparingTransition(null); + final TaskViewTaskController tv = + mBubble.getTaskView().getController(); + tv.notifyTaskRemovalStarted(tv.getTaskInfo()); + mTaskLeash = null; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @android.annotation.Nullable TransitionRequestInfo request) { + return null; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (!aborted) return; + mTransition = null; + skip(); + mTaskViewTransitions.onExternalDone(transition); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mTransition != transition) return false; + + final TaskViewTaskController tv = + mBubble.getTaskView().getController(); + if (tv == null) { + mTaskViewTransitions.onExternalDone(transition); + return false; + } + + TransitionInfo.Change taskChg = null; + + boolean found = false; + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change chg = info.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + if (chg.getMode() != TRANSIT_CHANGE) continue; + if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue; + found = true; + mRepository.remove(tv); + taskChg = chg; + break; + } + + if (!found) { + Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get " + + "one, cleaning up the task view"); + tv.setTaskNotFound(); + skip(); + mTaskViewTransitions.onExternalDone(transition); + return false; + } + + mTaskLeash = taskChg.getLeash(); + mRootLeash = info.getRoot(0).getLeash(); + + SurfaceControl dest = + mBubble.getBubbleBarExpandedView().getViewRootImpl().getSurfaceControl(); + final Runnable onPlucked = () -> { + // Need to remove the taskview AFTER applying the startTransaction because + // it isn't synchronized. + tv.notifyTaskRemovalStarted(tv.getTaskInfo()); + // Unset after removeView so it can be used to pick a different animation. + mBubble.setPreparingTransition(null); + mBubbleData.setExpanded(false /* expanded */); + }; + if (dest != null) { + pluck(mTaskLeash, mBubble.getBubbleBarExpandedView(), dest, + taskChg.getStartAbsBounds().left - info.getRoot(0).getOffset().x, + taskChg.getStartAbsBounds().top - info.getRoot(0).getOffset().y, + mBubble.getBubbleBarExpandedView().getCornerRadius(), startTransaction, + onPlucked); + mBubble.getBubbleBarExpandedView().post(() -> mTransitions.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCallback, + null)); + } else { + onPlucked.run(); + mTransitions.dispatchTransition(mTransition, info, startTransaction, + finishTransaction, finishCallback, null); + } + + mTaskViewTransitions.onExternalDone(transition); + return true; + } + + @Override + public void continueCollapse() { + mBubble.cleanupTaskView(); + if (mTaskLeash == null) return; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(mTaskLeash, mRootLeash); + t.apply(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 91dcbdf5f117..f3f8d6f96a42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -355,8 +355,10 @@ public class BubbleBarLayerView extends FrameLayout /** Removes the given {@code bubble}. */ public void removeBubble(Bubble bubble, Runnable endAction) { + final boolean inTransition = bubble.getPreparingTransition() != null; Runnable cleanUp = () -> { - bubble.cleanupViews(); + // The transition is already managing the task/wm state. + bubble.cleanupViews(!inTransition); endAction.run(); }; if (mBubbleData.getBubbles().isEmpty()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index aef75e2dc99e..a0cc2bc8887b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -417,7 +417,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } } - void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + /** Notifies listeners of a task being removed. */ + public void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null) return; final int taskId = taskInfo.taskId; mListenerExecutor.execute(() -> mListener.onTaskRemovalStarted(taskId)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index b4f514acf2dd..9d0ddbc6de12 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -179,4 +179,33 @@ public class BubbleTransitionsTest extends ShellTestCase { animCb.getValue().run(); assertTrue(finishCalled[0]); } + + @Test + public void testConvertFromBubble() { + ActivityManager.RunningTaskInfo taskInfo = setupBubble(); + final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertFromBubble( + mBubble, taskInfo); + final BubbleTransitions.ConvertFromBubble cfb = (BubbleTransitions.ConvertFromBubble) bt; + verify(mTransitions).startTransition(anyInt(), any(), eq(cfb)); + verify(mBubble).setPreparingTransition(eq(bt)); + assertTrue(mTaskViewTransitions.hasPending()); + + final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0); + final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, + mock(SurfaceControl.class)); + chg.setMode(TRANSIT_CHANGE); + chg.setTaskInfo(taskInfo); + info.addChange(chg); + info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + Transitions.TransitionFinishCallback finishCb = wct -> {}; + cfb.startAnimation(cfb.mTransition, info, startT, finishT, finishCb); + + // Can really only verify that it interfaces with the taskViewTransitions queue. + // The actual functioning of this is tightly-coupled with SurfaceFlinger and renderthread + // in order to properly synchronize surface manipulation with drawing and thus can't be + // directly tested. + assertFalse(mTaskViewTransitions.hasPending()); + } } |