Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e06e571f authored by Evan Rosky's avatar Evan Rosky
Browse files

Add Transition for convert-task-to-bubble

This adds BubbleTransitions infrastructure for managing the
lifetime of a bubble-transition. Bubbles have multiple stages
to their lifecycle so this encapsulates a logical operation
into a single object.

While a transition is ongoing for a bubble, it marks itself
as a "PendingTransition" in the bubble. This is to support
coordination (since the call-graph goes through bubble
internals).

In this case (covert-to-bubble), a ConvertToBubble object is
created for every convert-to-bubble event. It coordinates with
bubble inflation, shell-transitions, and launcher-roundtrip.

Additionally, some infrastructure was set-up for associating
a bubble with a task (rather than activity). This is in the
form of a TaskInfo in Bubble and corresponding key-type.

Since convert-to-bubble is effectively "expanding a new bubble",
there is a lot of overlap with expand animation, so some of
BubbleBarLayerView was refactored to share.

A convertToBubble animation was added (using SizeChangeAnimation
ported from core).

Bug: 384976265
Test: BubbleTransitionsTest
Flag: com.android.wm.shell.enable_bubble_to_fullscreen
Change-Id: I25fde9b0a3b4b69eaad57724febab1103f8b7a0b
parent 84319ceb
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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.
@@ -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) {
@@ -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,
@@ -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,
@@ -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.
     */
@@ -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.
@@ -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.
+32 −6
Original line number Diff line number Diff line
@@ -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;
@@ -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,
@@ -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;
@@ -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);
        }
    }

    /**
@@ -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()) {
+12 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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);
+3 −1
Original line number Diff line number Diff line
@@ -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);
+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