summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Evan Rosky <erosky@google.com> 2025-01-16 11:34:42 -0800
committer Evan Rosky <erosky@google.com> 2025-01-22 16:12:47 -0800
commit5c73d96e34be4e9dffd751ac2b265ecbba7ffc94 (patch)
tree7ba6c5db4a6f569c67af9f90fddd9b1d9f388c2b
parente06e571f349f738a6233dd70b553f9738887c5d5 (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
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java104
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java199
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java29
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());
+ }
}