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

Commit b3295cfd authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge changes I8beec37e,I0955772d into udc-dev

* changes:
  Create BubbleTaskViewHelper to be shared with new bubble expanded view
  Create interface and data to pass bubble updates to launcher
parents f7cc60d4 70b6e811
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ filegroup {
        "src/com/android/wm/shell/common/split/SplitScreenConstants.java",
        "src/com/android/wm/shell/sysui/ShellSharedConstants.java",
        "src/com/android/wm/shell/common/TransactionPool.java",
        "src/com/android/wm/shell/common/bubbles/*.java",
        "src/com/android/wm/shell/common/TriangleShape.java",
        "src/com/android/wm/shell/animation/Interpolators.java",
        "src/com/android/wm/shell/pip/PipContentOverlay.java",
        "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java",
+17 −1
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.wm.shell.common.bubbles.BubbleInfo;

import java.io.PrintWriter;
import java.util.List;
@@ -244,6 +245,16 @@ public class Bubble implements BubbleViewProvider {
        setEntry(entry);
    }

    /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */
    public BubbleInfo asBubbleBarBubble() {
        return new BubbleInfo(getKey(),
                getFlags(),
                getShortcutInfo().getId(),
                getIcon(),
                getUser().getIdentifier(),
                getPackageName());
    }

    @Override
    public String getKey() {
        return mKey;
@@ -545,8 +556,13 @@ public class Bubble implements BubbleViewProvider {
        }
    }

    /**
     * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles
     * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the
     * icon from the shortcut.
     */
    @Nullable
    Icon getIcon() {
    public Icon getIcon() {
        return mIcon;
    }

+137 −1
Original line number Diff line number Diff line
@@ -38,7 +38,9 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;

import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -59,6 +61,7 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Icon;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -88,13 +91,17 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -123,7 +130,8 @@ import java.util.function.IntConsumer;
 *
 * The controller manages addition, removal, and visible state of bubbles on screen.
 */
public class BubbleController implements ConfigurationChangeListener {
public class BubbleController implements ConfigurationChangeListener,
        RemoteCallable<BubbleController> {

    private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;

@@ -248,6 +256,8 @@ public class BubbleController implements ConfigurationChangeListener {
    private Optional<OneHandedController> mOneHandedOptional;
    /** Drag and drop controller to register listener for onDragStarted. */
    private DragAndDropController mDragAndDropController;
    /** Used to send bubble events to launcher. */
    private Bubbles.BubbleStateListener mBubbleStateListener;

    public BubbleController(Context context,
            ShellInit shellInit,
@@ -458,9 +468,15 @@ public class BubbleController implements ConfigurationChangeListener {
        mCurrentProfiles = userProfiles;

        mShellController.addConfigurationChangeListener(this);
        mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES,
                this::createExternalInterface, this);
        mShellCommandHandler.addDumpCallback(this::dump, this);
    }

    private ExternalInterfaceBinder createExternalInterface() {
        return new BubbleController.IBubblesImpl(this);
    }

    @VisibleForTesting
    public Bubbles asBubbles() {
        return mImpl;
@@ -475,6 +491,48 @@ public class BubbleController implements ConfigurationChangeListener {
        return mMainExecutor;
    }

    @Override
    public Context getContext() {
        return mContext;
    }

    @Override
    public ShellExecutor getRemoteCallExecutor() {
        return mMainExecutor;
    }

    /**
     * Sets a listener to be notified of bubble updates. This is used by launcher so that
     * it may render bubbles in itself. Only one listener is supported.
     */
    public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) {
        if (isShowingAsBubbleBar()) {
            // Only set the listener if bubble bar is showing.
            mBubbleStateListener = listener;
            sendInitialListenerUpdate();
        } else {
            mBubbleStateListener = null;
        }
    }

    /**
     * Unregisters the {@link Bubbles.BubbleStateListener}.
     */
    public void unregisterBubbleStateListener() {
        mBubbleStateListener = null;
    }

    /**
     * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble
     * state to it.
     */
    private void sendInitialListenerUpdate() {
        if (mBubbleStateListener != null) {
            BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
            mBubbleStateListener.onBubbleStateChange(update);
        }
    }

    /**
     * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
     */
@@ -1722,6 +1780,73 @@ public class BubbleController implements ConfigurationChangeListener {
        }
    }

    /**
     * The interface for calls from outside the host process.
     */
    @BinderThread
    private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder {
        private BubbleController mController;
        private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener;
        private final Bubbles.BubbleStateListener mBubbleListener =
                new Bubbles.BubbleStateListener() {

            @Override
            public void onBubbleStateChange(BubbleBarUpdate update) {
                Bundle b = new Bundle();
                b.setClassLoader(BubbleBarUpdate.class.getClassLoader());
                b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update);
                mListener.call(l -> l.onBubbleStateChange(b));
            }
        };

        IBubblesImpl(BubbleController controller) {
            mController = controller;
            mListener = new SingleInstanceRemoteListener<>(mController,
                    c -> c.registerBubbleStateListener(mBubbleListener),
                    c -> c.unregisterBubbleStateListener());
        }

        /**
         * Invalidates this instance, preventing future calls from updating the controller.
         */
        @Override
        public void invalidate() {
            mController = null;
        }

        @Override
        public void registerBubbleListener(IBubblesListener listener) {
            mMainExecutor.execute(() -> {
                mListener.register(listener);
            });
        }

        @Override
        public void unregisterBubbleListener(IBubblesListener listener) {
            mMainExecutor.execute(() -> mListener.unregister());
        }

        @Override
        public void showBubble(String key, boolean onLauncherHome) {
            // TODO
        }

        @Override
        public void removeBubble(String key, int reason) {
            // TODO
        }

        @Override
        public void collapseBubbles() {
            // TODO
        }

        @Override
        public void onTaskbarStateChanged(int newState) {
            // TODO (b/269670598)
        }
    }

    private class BubblesImpl implements Bubbles {
        // Up-to-date cached state of bubbles data for SysUI to query from the calling thread
        @VisibleForTesting
@@ -1835,6 +1960,17 @@ public class BubbleController implements ConfigurationChangeListener {

        private CachedState mCachedState = new CachedState();

        private IBubblesImpl mIBubbles;

        @Override
        public IBubbles createExternalInterface() {
            if (mIBubbles != null) {
                mIBubbles.invalidate();
            }
            mIBubbles = new IBubblesImpl(BubbleController.this);
            return mIBubbles;
        }

        @Override
        public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
            return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
+64 −0
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubbles.DismissReason;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.common.bubbles.RemovedBubble;

import java.io.PrintWriter;
import java.util.ArrayList;
@@ -113,6 +115,61 @@ public class BubbleData {
        void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
            removedBubbles.add(new Pair<>(bubbleToRemove, reason));
        }

        /**
         * Converts the update to a {@link BubbleBarUpdate} which contains updates relevant
         * to the bubble bar. Only used when {@link BubbleController#isShowingAsBubbleBar()} is
         * true.
         */
        BubbleBarUpdate toBubbleBarUpdate() {
            BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();

            bubbleBarUpdate.expandedChanged = expandedChanged;
            bubbleBarUpdate.expanded = expanded;
            if (selectionChanged) {
                bubbleBarUpdate.selectedBubbleKey = selectedBubble != null
                        ? selectedBubble.getKey()
                        : null;
            }
            bubbleBarUpdate.addedBubble = addedBubble != null
                    ? addedBubble.asBubbleBarBubble()
                    : null;
            // TODO(b/269670235): We need to handle updates better, I think for the bubble bar only
            //  certain updates need to be sent instead of any updatedBubble.
            bubbleBarUpdate.updatedBubble = updatedBubble != null
                    ? updatedBubble.asBubbleBarBubble()
                    : null;
            bubbleBarUpdate.suppressedBubbleKey = suppressedBubble != null
                    ? suppressedBubble.getKey()
                    : null;
            bubbleBarUpdate.unsupressedBubbleKey = unsuppressedBubble != null
                    ? unsuppressedBubble.getKey()
                    : null;
            for (int i = 0; i < removedBubbles.size(); i++) {
                Pair<Bubble, Integer> pair = removedBubbles.get(i);
                bubbleBarUpdate.removedBubbles.add(
                        new RemovedBubble(pair.first.getKey(), pair.second));
            }
            if (orderChanged) {
                // Include the new order
                for (int i = 0; i < bubbles.size(); i++) {
                    bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey());
                }
            }
            return bubbleBarUpdate;
        }

        /**
         * Gets the current state of active bubbles and populates the update with that.  Only
         * used when {@link BubbleController#isShowingAsBubbleBar()} is true.
         */
        BubbleBarUpdate getInitialState() {
            BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
            for (int i = 0; i < bubbles.size(); i++) {
                bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble());
            }
            return bubbleBarUpdate;
        }
    }

    /**
@@ -190,6 +247,13 @@ public class BubbleData {
        mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
    }

    /**
     * Returns a bubble bar update populated with the current list of active bubbles.
     */
    public BubbleBarUpdate getInitialStateForBubbleBar() {
        return mStateChange.getInitialState();
    }

    public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
        mBubbleMetadataFlagListener = listener;
    }
+282 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;

import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;

import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

import com.android.wm.shell.TaskView;
import com.android.wm.shell.TaskViewTaskController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellMainThread;

/**
 * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}.
 */
public class BubbleTaskViewHelper {

    private static final String TAG = BubbleTaskViewHelper.class.getSimpleName();

    /**
     * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events
     * on the task.
     */
    public interface Listener {

        /** Called when the task is first created. */
        void onTaskCreated();

        /** Called when the visibility of the task changes. */
        void onContentVisibilityChanged(boolean visible);

        /** Called when back is pressed on the task root. */
        void onBackPressed();
    }

    private final Context mContext;
    private final BubbleController mController;
    private final @ShellMainThread ShellExecutor mMainExecutor;
    private final BubbleTaskViewHelper.Listener mListener;
    private final View mParentView;

    @Nullable
    private Bubble mBubble;
    @Nullable
    private PendingIntent mPendingIntent;
    private TaskViewTaskController mTaskViewTaskController;
    @Nullable
    private TaskView mTaskView;
    private int mTaskId = INVALID_TASK_ID;

    private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
        private boolean mInitialized = false;
        private boolean mDestroyed = false;

        @Override
        public void onInitialized() {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
                        + " initialized=" + mInitialized
                        + " bubble=" + getBubbleKey());
            }

            if (mDestroyed || mInitialized) {
                return;
            }

            // Custom options so there is no activity transition animation
            ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
                    0 /* enterResId */, 0 /* exitResId */);

            Rect launchBounds = new Rect();
            mTaskView.getBoundsOnScreen(launchBounds);

            // TODO: I notice inconsistencies in lifecycle
            // Post to keep the lifecycle normal
            mParentView.post(() -> {
                if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                    Log.d(TAG, "onInitialized: calling startActivity, bubble="
                            + getBubbleKey());
                }
                try {
                    options.setTaskAlwaysOnTop(true);
                    options.setLaunchedFromBubble(true);

                    Intent fillInIntent = new Intent();
                    // Apply flags to make behaviour match documentLaunchMode=always.
                    fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);

                    if (mBubble.isAppBubble()) {
                        PendingIntent pi = PendingIntent.getActivity(mContext, 0,
                                mBubble.getAppBubbleIntent(),
                                PendingIntent.FLAG_MUTABLE,
                                null);
                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
                    } else if (mBubble.hasMetadataShortcutId()) {
                        options.setApplyActivityFlagsForBubbles(true);
                        mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                                options, launchBounds);
                    } else {
                        if (mBubble != null) {
                            mBubble.setIntentActive();
                        }
                        mTaskView.startActivity(mPendingIntent, fillInIntent, options,
                                launchBounds);
                    }
                } catch (RuntimeException e) {
                    // If there's a runtime exception here then there's something
                    // wrong with the intent, we can't really recover / try to populate
                    // the bubble again so we'll just remove it.
                    Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
                            + ", " + e.getMessage() + "; removing bubble");
                    mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
                }
                mInitialized = true;
            });
        }

        @Override
        public void onReleased() {
            mDestroyed = true;
        }

        @Override
        public void onTaskCreated(int taskId, ComponentName name) {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onTaskCreated: taskId=" + taskId
                        + " bubble=" + getBubbleKey());
            }
            // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
            mTaskId = taskId;

            // With the task org, the taskAppeared callback will only happen once the task has
            // already drawn
            mListener.onTaskCreated();
        }

        @Override
        public void onTaskVisibilityChanged(int taskId, boolean visible) {
            mListener.onContentVisibilityChanged(visible);
        }

        @Override
        public void onTaskRemovalStarted(int taskId) {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
                        + " bubble=" + getBubbleKey());
            }
            if (mBubble != null) {
                mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
            }
        }

        @Override
        public void onBackPressedOnTaskRoot(int taskId) {
            if (mTaskId == taskId && mController.isStackExpanded()) {
                mListener.onBackPressed();
            }
        }
    };

    public BubbleTaskViewHelper(Context context,
            BubbleController controller,
            BubbleTaskViewHelper.Listener listener,
            View parent) {
        mContext = context;
        mController = controller;
        mMainExecutor = mController.getMainExecutor();
        mListener = listener;
        mParentView = parent;
        mTaskViewTaskController = new TaskViewTaskController(mContext,
                mController.getTaskOrganizer(),
                mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
        mTaskView = new TaskView(mContext, mTaskViewTaskController);
        mTaskView.setListener(mMainExecutor, mTaskViewListener);
    }

    /**
     * Sets the bubble or updates the bubble used to populate the view.
     *
     * @return true if the bubble is new, false if it was an update to the same bubble.
     */
    public boolean update(Bubble bubble) {
        boolean isNew = mBubble == null || didBackingContentChange(bubble);
        mBubble = bubble;
        if (isNew) {
            mPendingIntent = mBubble.getBubbleIntent();
            return true;
        }
        return false;
    }

    /** Cleans up anything related to the task and {@code TaskView}. */
    public void cleanUpTaskView() {
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
        }
        if (mTaskId != INVALID_TASK_ID) {
            try {
                ActivityTaskManager.getService().removeTask(mTaskId);
            } catch (RemoteException e) {
                Log.w(TAG, e.getMessage());
            }
        }
        if (mTaskView != null) {
            mTaskView.release();
            mTaskView = null;
        }
    }

    /** Returns the bubble key associated with this view. */
    @Nullable
    public String getBubbleKey() {
        return mBubble != null ? mBubble.getKey() : null;
    }

    /** Returns the TaskView associated with this view. */
    @Nullable
    public TaskView getTaskView() {
        return mTaskView;
    }

    /**
     * Returns the task id associated with the task in this view. If the task doesn't exist then
     * {@link ActivityTaskManager#INVALID_TASK_ID}.
     */
    public int getTaskId() {
        return mTaskId;
    }

    /** Returns whether the bubble set on the helper is valid to populate the task view. */
    public boolean isValidBubble() {
        return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId());
    }

    // TODO (b/274980695): Is this still relevant?
    /**
     * Bubbles are backed by a pending intent or a shortcut, once the activity is
     * started we never change it / restart it on notification updates -- unless the bubble's
     * backing data switches.
     *
     * This indicates if the new bubble is backed by a different data source than what was
     * previously shown here (e.g. previously a pending intent & now a shortcut).
     *
     * @param newBubble the bubble this view is being updated with.
     * @return true if the backing content has changed.
     */
    private boolean didBackingContentChange(Bubble newBubble) {
        boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
        boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
        return prevWasIntentBased != newIsIntentBased;
    }
}
Loading