Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +67 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; Loading Loading @@ -57,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleInfo; import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; import com.android.wm.shell.taskview.TaskView; import java.io.PrintWriter; import java.util.List; Loading Loading @@ -203,6 +205,13 @@ public class Bubble implements BubbleViewProvider { @Nullable private Intent mAppIntent; /** * Set while preparing a transition for animation. Several steps are needed before animation * starts, so this is used to detect and route associated events to the coordinating transition. */ @Nullable private BubbleTransitions.BubbleTransition mPreparingTransition; /** * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. Loading Loading @@ -280,6 +289,30 @@ public class Bubble implements BubbleViewProvider { mShortcutInfo = info; } private Bubble( TaskInfo task, UserHandle user, @Nullable Icon icon, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mGroupKey = null; mLocusId = null; mFlags = 0; mUser = user; mIcon = icon; mIsAppBubble = true; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = task.taskId; mAppIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = task.baseActivity.getPackageName(); } /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { Loading @@ -291,6 +324,16 @@ public class Bubble implements BubbleViewProvider { mainExecutor, bgExecutor); } /** Creates a task bubble. */ public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(info, user, icon, getAppBubbleKeyForTask(info), mainExecutor, bgExecutor); } /** Creates a shortcut bubble. */ public static Bubble createShortcutBubble( ShortcutInfo info, Loading @@ -316,6 +359,15 @@ public class Bubble implements BubbleViewProvider { return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); } /** * Returns the key for an app bubble from an app with package name, {@code packageName} on an * Android user, {@code user}. */ public static String getAppBubbleKeyForTask(TaskInfo taskInfo) { Objects.requireNonNull(taskInfo); return KEY_APP_BUBBLE + ":" + taskInfo.taskId; } @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, Loading Loading @@ -469,6 +521,10 @@ public class Bubble implements BubbleViewProvider { return mBubbleTaskView; } public TaskView getTaskView() { return mBubbleTaskView.getTaskView(); } /** * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. */ Loading @@ -486,6 +542,10 @@ public class Bubble implements BubbleViewProvider { return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); } BubbleTransitions.BubbleTransition getPreparingTransition() { return mPreparingTransition; } /** * Call this to clean up the task for the bubble. Ensure this is always called when done with * the bubble. Loading Loading @@ -555,6 +615,13 @@ public class Bubble implements BubbleViewProvider { mInflateSynchronously = inflateSynchronously; } /** * Sets the current bubble-transition that is coordinating a change in this bubble. */ void setPreparingTransition(BubbleTransitions.BubbleTransition transit) { mPreparingTransition = transit; } /** * Sets whether this bubble is considered text changed. This method is purely for * testing. Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +32 −6 Original line number Diff line number Diff line Loading @@ -110,6 +110,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ConfigurationChangeListener; Loading Loading @@ -287,6 +288,8 @@ public class BubbleController implements ConfigurationChangeListener, /** Used to send updates to the views from {@link #mBubbleDataListener}. */ private BubbleViewCallback mBubbleViewCallback; private final BubbleTransitions mBubbleTransitions; public BubbleController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, Loading Loading @@ -350,12 +353,16 @@ public class BubbleController implements ConfigurationChangeListener, context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)); mDisplayController = displayController; final TaskViewTransitions tvTransitions; if (TaskViewTransitions.useRepo()) { mTaskViewController = new TaskViewTransitions(transitions, taskViewRepository, organizer, syncQueue); tvTransitions = new TaskViewTransitions(transitions, taskViewRepository, organizer, syncQueue); } else { mTaskViewController = taskViewTransitions; tvTransitions = taskViewTransitions; } mTaskViewController = tvTransitions; mBubbleTransitions = new BubbleTransitions(transitions, organizer, taskViewRepository, data, tvTransitions, context); mTransitions = transitions; mOneHandedOptional = oneHandedOptional; mDragAndDropController = dragAndDropController; Loading Loading @@ -1456,7 +1463,19 @@ public class BubbleController implements ConfigurationChangeListener, * @param taskInfo the task. */ public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) { // TODO(384976265): Not implemented yet if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); if (b.isInflated()) { mBubbleData.setSelectedBubbleAndExpandStack(b); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); mBubbleTransitions.startConvertToBubble(b, taskInfo, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, mLogger, mStackView, mLayerView, mBubbleIconFactory, mInflateSynchronously); } } /** Loading Loading @@ -2261,10 +2280,17 @@ public class BubbleController implements ConfigurationChangeListener, private void showExpandedViewForBubbleBar() { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); if (selectedBubble != null && mLayerView != null) { mLayerView.showExpandedView(selectedBubble); if (selectedBubble == null) return; if (selectedBubble instanceof Bubble) { final Bubble bubble = (Bubble) selectedBubble; if (bubble.getPreparingTransition() != null) { bubble.getPreparingTransition().continueExpand(); return; } } if (mLayerView == null) return; mLayerView.showExpandedView(selectedBubble); } private void collapseExpandedViewForBubbleBar() { if (mLayerView != null && mLayerView.isExpanded()) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +12 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; Loading Loading @@ -470,6 +471,17 @@ public class BubbleData { return bubbleToReturn; } Bubble getOrCreateBubble(TaskInfo taskInfo) { UserHandle user = UserHandle.of(mCurrentUserId); String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo); Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); if (bubbleToReturn == null) { bubbleToReturn = Bubble.createTaskBubble(taskInfo, user, null, mMainExecutor, mBgExecutor); } return bubbleToReturn; } @Nullable private Bubble findAndRemoveBubbleFromOverflow(String key) { Bubble bubbleToReturn = getBubbleInStackWithKey(key); Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +3 −1 Original line number Diff line number Diff line Loading @@ -109,7 +109,9 @@ public class BubbleTaskViewHelper { MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); if (mBubble.isAppBubble()) { if (mBubble.getPreparingTransition() != null) { mBubble.getPreparingTransition().surfaceCreated(); } else if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java 0 → 100644 +319 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.bubbles; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; import android.graphics.Rect; import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; import android.view.SurfaceView; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.taskview.TaskView; import com.android.wm.shell.taskview.TaskViewRepository; import com.android.wm.shell.taskview.TaskViewTaskController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; import java.util.concurrent.Executor; /** * Implements transition coordination for bubble operations. */ public class BubbleTransitions { private static final String TAG = "BubbleTransitions"; @NonNull final Transitions mTransitions; @NonNull final ShellTaskOrganizer mTaskOrganizer; @NonNull final TaskViewRepository mRepository; @NonNull final Executor mMainExecutor; @NonNull final BubbleData mBubbleData; @NonNull final TaskViewTransitions mTaskViewTransitions; @NonNull final Context mContext; BubbleTransitions(@NonNull Transitions transitions, @NonNull ShellTaskOrganizer organizer, @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData, @NonNull TaskViewTransitions taskViewTransitions, Context context) { mTransitions = transitions; mTaskOrganizer = organizer; mRepository = repository; mMainExecutor = transitions.getMainExecutor(); mBubbleData = bubbleData; mTaskViewTransitions = taskViewTransitions; mContext = context; } /** * Starts a convert-to-bubble transition. * * @see ConvertToBubble */ public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { ConvertToBubble convert = new ConvertToBubble(bubble, taskInfo, mContext, expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, inflateSync); return convert; } /** * 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. */ interface BubbleTransition { default void surfaceCreated() {} default void continueExpand() {} void skip(); } /** * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The * steps are as follows: * * 1. Start inflating the bubble view * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. * 3. Transition becomes ready, so notify Launcher * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible * 5. Surface is created which kicks off actual animation * * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. * * continueExpand and surfaceCreated are set-up to happen in either order, though, to support * UX/timing adjustments. */ @VisibleForTesting class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { final BubbleBarLayerView mLayerView; Bubble mBubble; IBinder mTransition; Transitions.TransitionFinishCallback mFinishCb; WindowContainerTransaction mFinishWct = null; final Rect mStartBounds = new Rect(); SurfaceControl mSnapshot = null; TaskInfo mTaskInfo; boolean mFinishedExpand = false; BubbleViewProvider mPriorBubble = null; private SurfaceControl.Transaction mFinishT; private SurfaceControl mTaskLeash; ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { mBubble = bubble; mTaskInfo = taskInfo; mLayerView = layerView; mBubble.setInflateSynchronously(inflateSync); mBubble.setPreparingTransition(this); mBubble.inflate( this::onInflated, context, expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, false /* skipInflation */); } @VisibleForTesting void onInflated(Bubble b) { if (b != mBubble) { throw new IllegalArgumentException("inflate callback doesn't match bubble"); } final Rect launchBounds = new Rect(); mLayerView.getExpandedViewRestBounds(launchBounds); WindowContainerTransaction wct = new WindowContainerTransaction(); if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) { wct.reparent(mTaskInfo.token, null, true); } } wct.setAlwaysOnTop(mTaskInfo.token, true); wct.setWindowingMode(mTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW); wct.setBounds(mTaskInfo.token, launchBounds); final TaskView tv = b.getTaskView(); tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT); final TaskViewRepository.TaskViewState state = mRepository.byTaskView( tv.getController()); if (state != null) { state.mVisible = true; } mTaskViewTransitions.enqueueExternal(tv.getController(), () -> { mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); return mTransition; }); } @Override public void skip() { mBubble.setPreparingTransition(null); mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; } @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @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; 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; 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; mStartBounds.set(chg.getStartAbsBounds()); // Converting a task into taskview, so treat as "new" mFinishWct = new WindowContainerTransaction(); mTaskInfo = chg.getTaskInfo(); mFinishT = finishTransaction; mTaskLeash = chg.getLeash(); found = true; mSnapshot = chg.getSnapshot(); break; } if (!found) { Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get " + "one, cleaning up the task view"); mBubble.getTaskView().getController().setTaskNotFound(); mTaskViewTransitions.onExternalDone(transition); return false; } mFinishCb = finishCallback; // Now update state (and talk to launcher) in parallel with snapshot stuff mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true, /* showInShade= */ false); startTransaction.show(mSnapshot); // Move snapshot to root so that it remains visible while task is moved to taskview startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash()); startTransaction.setPosition(mSnapshot, mStartBounds.left - info.getRoot(0).getOffset().x, mStartBounds.top - info.getRoot(0).getOffset().y); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); return true; } @Override public void continueExpand() { mFinishedExpand = true; final boolean animate = mLayerView.canExpandView(mBubble); if (animate) { mPriorBubble = mLayerView.prepareConvertedView(mBubble); } if (mPriorBubble != null) { // TODO: an animation. For now though, just remove it. final BubbleBarExpandedView priorView = mPriorBubble.getBubbleBarExpandedView(); mLayerView.removeView(priorView); mPriorBubble = null; } if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { playAnimation(animate); } } @Override public void surfaceCreated() { mMainExecutor.execute(() -> { final TaskViewTaskController tvc = mBubble.getTaskView().getController(); final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); if (state == null) return; state.mVisible = true; if (mFinishedExpand) { playAnimation(true /* animate */); } }); } private void playAnimation(boolean animate) { final TaskViewTaskController tv = mBubble.getTaskView().getController(); final SurfaceControl.Transaction startT = new SurfaceControl.Transaction(); mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT, (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct); if (mFinishWct.isEmpty()) { mFinishWct = null; } // Preparation is complete. mBubble.setPreparingTransition(null); if (animate) { mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; }); } else { startT.apply(); mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; } } } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +67 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; Loading Loading @@ -57,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleInfo; import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; import com.android.wm.shell.taskview.TaskView; import java.io.PrintWriter; import java.util.List; Loading Loading @@ -203,6 +205,13 @@ public class Bubble implements BubbleViewProvider { @Nullable private Intent mAppIntent; /** * Set while preparing a transition for animation. Several steps are needed before animation * starts, so this is used to detect and route associated events to the coordinating transition. */ @Nullable private BubbleTransitions.BubbleTransition mPreparingTransition; /** * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. Loading Loading @@ -280,6 +289,30 @@ public class Bubble implements BubbleViewProvider { mShortcutInfo = info; } private Bubble( TaskInfo task, UserHandle user, @Nullable Icon icon, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mGroupKey = null; mLocusId = null; mFlags = 0; mUser = user; mIcon = icon; mIsAppBubble = true; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = task.taskId; mAppIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = task.baseActivity.getPackageName(); } /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { Loading @@ -291,6 +324,16 @@ public class Bubble implements BubbleViewProvider { mainExecutor, bgExecutor); } /** Creates a task bubble. */ public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(info, user, icon, getAppBubbleKeyForTask(info), mainExecutor, bgExecutor); } /** Creates a shortcut bubble. */ public static Bubble createShortcutBubble( ShortcutInfo info, Loading @@ -316,6 +359,15 @@ public class Bubble implements BubbleViewProvider { return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); } /** * Returns the key for an app bubble from an app with package name, {@code packageName} on an * Android user, {@code user}. */ public static String getAppBubbleKeyForTask(TaskInfo taskInfo) { Objects.requireNonNull(taskInfo); return KEY_APP_BUBBLE + ":" + taskInfo.taskId; } @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, Loading Loading @@ -469,6 +521,10 @@ public class Bubble implements BubbleViewProvider { return mBubbleTaskView; } public TaskView getTaskView() { return mBubbleTaskView.getTaskView(); } /** * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. */ Loading @@ -486,6 +542,10 @@ public class Bubble implements BubbleViewProvider { return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); } BubbleTransitions.BubbleTransition getPreparingTransition() { return mPreparingTransition; } /** * Call this to clean up the task for the bubble. Ensure this is always called when done with * the bubble. Loading Loading @@ -555,6 +615,13 @@ public class Bubble implements BubbleViewProvider { mInflateSynchronously = inflateSynchronously; } /** * Sets the current bubble-transition that is coordinating a change in this bubble. */ void setPreparingTransition(BubbleTransitions.BubbleTransition transit) { mPreparingTransition = transit; } /** * Sets whether this bubble is considered text changed. This method is purely for * testing. Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +32 −6 Original line number Diff line number Diff line Loading @@ -110,6 +110,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ConfigurationChangeListener; Loading Loading @@ -287,6 +288,8 @@ public class BubbleController implements ConfigurationChangeListener, /** Used to send updates to the views from {@link #mBubbleDataListener}. */ private BubbleViewCallback mBubbleViewCallback; private final BubbleTransitions mBubbleTransitions; public BubbleController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, Loading Loading @@ -350,12 +353,16 @@ public class BubbleController implements ConfigurationChangeListener, context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)); mDisplayController = displayController; final TaskViewTransitions tvTransitions; if (TaskViewTransitions.useRepo()) { mTaskViewController = new TaskViewTransitions(transitions, taskViewRepository, organizer, syncQueue); tvTransitions = new TaskViewTransitions(transitions, taskViewRepository, organizer, syncQueue); } else { mTaskViewController = taskViewTransitions; tvTransitions = taskViewTransitions; } mTaskViewController = tvTransitions; mBubbleTransitions = new BubbleTransitions(transitions, organizer, taskViewRepository, data, tvTransitions, context); mTransitions = transitions; mOneHandedOptional = oneHandedOptional; mDragAndDropController = dragAndDropController; Loading Loading @@ -1456,7 +1463,19 @@ public class BubbleController implements ConfigurationChangeListener, * @param taskInfo the task. */ public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) { // TODO(384976265): Not implemented yet if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); if (b.isInflated()) { mBubbleData.setSelectedBubbleAndExpandStack(b); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); mBubbleTransitions.startConvertToBubble(b, taskInfo, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, mLogger, mStackView, mLayerView, mBubbleIconFactory, mInflateSynchronously); } } /** Loading Loading @@ -2261,10 +2280,17 @@ public class BubbleController implements ConfigurationChangeListener, private void showExpandedViewForBubbleBar() { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); if (selectedBubble != null && mLayerView != null) { mLayerView.showExpandedView(selectedBubble); if (selectedBubble == null) return; if (selectedBubble instanceof Bubble) { final Bubble bubble = (Bubble) selectedBubble; if (bubble.getPreparingTransition() != null) { bubble.getPreparingTransition().continueExpand(); return; } } if (mLayerView == null) return; mLayerView.showExpandedView(selectedBubble); } private void collapseExpandedViewForBubbleBar() { if (mLayerView != null && mLayerView.isExpanded()) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +12 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; Loading Loading @@ -470,6 +471,17 @@ public class BubbleData { return bubbleToReturn; } Bubble getOrCreateBubble(TaskInfo taskInfo) { UserHandle user = UserHandle.of(mCurrentUserId); String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo); Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); if (bubbleToReturn == null) { bubbleToReturn = Bubble.createTaskBubble(taskInfo, user, null, mMainExecutor, mBgExecutor); } return bubbleToReturn; } @Nullable private Bubble findAndRemoveBubbleFromOverflow(String key) { Bubble bubbleToReturn = getBubbleInStackWithKey(key); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +3 −1 Original line number Diff line number Diff line Loading @@ -109,7 +109,9 @@ public class BubbleTaskViewHelper { MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); if (mBubble.isAppBubble()) { if (mBubble.getPreparingTransition() != null) { mBubble.getPreparingTransition().surfaceCreated(); } else if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java 0 → 100644 +319 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.bubbles; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; import android.graphics.Rect; import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; import android.view.SurfaceView; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.taskview.TaskView; import com.android.wm.shell.taskview.TaskViewRepository; import com.android.wm.shell.taskview.TaskViewTaskController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; import java.util.concurrent.Executor; /** * Implements transition coordination for bubble operations. */ public class BubbleTransitions { private static final String TAG = "BubbleTransitions"; @NonNull final Transitions mTransitions; @NonNull final ShellTaskOrganizer mTaskOrganizer; @NonNull final TaskViewRepository mRepository; @NonNull final Executor mMainExecutor; @NonNull final BubbleData mBubbleData; @NonNull final TaskViewTransitions mTaskViewTransitions; @NonNull final Context mContext; BubbleTransitions(@NonNull Transitions transitions, @NonNull ShellTaskOrganizer organizer, @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData, @NonNull TaskViewTransitions taskViewTransitions, Context context) { mTransitions = transitions; mTaskOrganizer = organizer; mRepository = repository; mMainExecutor = transitions.getMainExecutor(); mBubbleData = bubbleData; mTaskViewTransitions = taskViewTransitions; mContext = context; } /** * Starts a convert-to-bubble transition. * * @see ConvertToBubble */ public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { ConvertToBubble convert = new ConvertToBubble(bubble, taskInfo, mContext, expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, inflateSync); return convert; } /** * 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. */ interface BubbleTransition { default void surfaceCreated() {} default void continueExpand() {} void skip(); } /** * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The * steps are as follows: * * 1. Start inflating the bubble view * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. * 3. Transition becomes ready, so notify Launcher * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible * 5. Surface is created which kicks off actual animation * * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. * * continueExpand and surfaceCreated are set-up to happen in either order, though, to support * UX/timing adjustments. */ @VisibleForTesting class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { final BubbleBarLayerView mLayerView; Bubble mBubble; IBinder mTransition; Transitions.TransitionFinishCallback mFinishCb; WindowContainerTransaction mFinishWct = null; final Rect mStartBounds = new Rect(); SurfaceControl mSnapshot = null; TaskInfo mTaskInfo; boolean mFinishedExpand = false; BubbleViewProvider mPriorBubble = null; private SurfaceControl.Transaction mFinishT; private SurfaceControl mTaskLeash; ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { mBubble = bubble; mTaskInfo = taskInfo; mLayerView = layerView; mBubble.setInflateSynchronously(inflateSync); mBubble.setPreparingTransition(this); mBubble.inflate( this::onInflated, context, expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, false /* skipInflation */); } @VisibleForTesting void onInflated(Bubble b) { if (b != mBubble) { throw new IllegalArgumentException("inflate callback doesn't match bubble"); } final Rect launchBounds = new Rect(); mLayerView.getExpandedViewRestBounds(launchBounds); WindowContainerTransaction wct = new WindowContainerTransaction(); if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) { wct.reparent(mTaskInfo.token, null, true); } } wct.setAlwaysOnTop(mTaskInfo.token, true); wct.setWindowingMode(mTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW); wct.setBounds(mTaskInfo.token, launchBounds); final TaskView tv = b.getTaskView(); tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT); final TaskViewRepository.TaskViewState state = mRepository.byTaskView( tv.getController()); if (state != null) { state.mVisible = true; } mTaskViewTransitions.enqueueExternal(tv.getController(), () -> { mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); return mTransition; }); } @Override public void skip() { mBubble.setPreparingTransition(null); mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; } @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @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; 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; 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; mStartBounds.set(chg.getStartAbsBounds()); // Converting a task into taskview, so treat as "new" mFinishWct = new WindowContainerTransaction(); mTaskInfo = chg.getTaskInfo(); mFinishT = finishTransaction; mTaskLeash = chg.getLeash(); found = true; mSnapshot = chg.getSnapshot(); break; } if (!found) { Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get " + "one, cleaning up the task view"); mBubble.getTaskView().getController().setTaskNotFound(); mTaskViewTransitions.onExternalDone(transition); return false; } mFinishCb = finishCallback; // Now update state (and talk to launcher) in parallel with snapshot stuff mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true, /* showInShade= */ false); startTransaction.show(mSnapshot); // Move snapshot to root so that it remains visible while task is moved to taskview startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash()); startTransaction.setPosition(mSnapshot, mStartBounds.left - info.getRoot(0).getOffset().x, mStartBounds.top - info.getRoot(0).getOffset().y); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); return true; } @Override public void continueExpand() { mFinishedExpand = true; final boolean animate = mLayerView.canExpandView(mBubble); if (animate) { mPriorBubble = mLayerView.prepareConvertedView(mBubble); } if (mPriorBubble != null) { // TODO: an animation. For now though, just remove it. final BubbleBarExpandedView priorView = mPriorBubble.getBubbleBarExpandedView(); mLayerView.removeView(priorView); mPriorBubble = null; } if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { playAnimation(animate); } } @Override public void surfaceCreated() { mMainExecutor.execute(() -> { final TaskViewTaskController tvc = mBubble.getTaskView().getController(); final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); if (state == null) return; state.mVisible = true; if (mFinishedExpand) { playAnimation(true /* animate */); } }); } private void playAnimation(boolean animate) { final TaskViewTaskController tv = mBubble.getTaskView().getController(); final SurfaceControl.Transaction startT = new SurfaceControl.Transaction(); mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT, (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct); if (mFinishWct.isEmpty()) { mFinishWct = null; } // Preparation is complete. mBubble.setPreparingTransition(null); if (animate) { mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; }); } else { startT.apply(); mFinishCb.onTransitionFinished(mFinishWct); mFinishCb = null; } } } }