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

Commit 5c73d96e authored by Evan Rosky's avatar Evan Rosky
Browse files

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
parent e06e571f
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -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;
    }
+102 −2
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+199 −0
Original line number Diff line number Diff line
@@ -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;
@@ -89,6 +99,44 @@ public class BubbleTransitions {
        return convert;
    }

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