diff options
| author | 2023-03-27 21:50:59 +0000 | |
|---|---|---|
| committer | 2023-03-27 21:50:59 +0000 | |
| commit | b3295cfdf02d78a4e8162bd60014c8f8404bb2cf (patch) | |
| tree | 66d43c64f0841b94a58c03e142696b566e83a8ae | |
| parent | f7cc60d4ade75e9330f277277f2c99bfa0995ea7 (diff) | |
| parent | 70b6e81102d369829774d2fc64452b29f12f9d56 (diff) | |
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
12 files changed, 951 insertions, 2 deletions
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index c7c94246b96a..54978bd4496d 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -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", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 85a353f2d586..4805ed39e1a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -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; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d2889e782aea..4b4b1af3662d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -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); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 3fd09675a245..a26c0c487d19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -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; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java new file mode 100644 index 000000000000..2a3162931648 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -0,0 +1,282 @@ +/* + * 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; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 876a720f7722..259f69296ac7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -39,6 +39,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -81,6 +82,11 @@ public interface Bubbles { int DISMISS_RELOAD_FROM_DISK = 15; int DISMISS_USER_REMOVED = 16; + /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ + default IBubbles createExternalInterface() { + return null; + } + /** * @return {@code true} if there is a bubble associated with the provided key and if its * notification is hidden from the shade or there is a group summary associated with the @@ -277,6 +283,17 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); + /** + * A listener to be notified of bubble state changes, used by launcher to render bubbles in + * its process. + */ + interface BubbleStateListener { + /** + * Called when the bubbles state changes. + */ + void onBubbleStateChange(BubbleBarUpdate update); + } + /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl new file mode 100644 index 000000000000..862e818a998b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -0,0 +1,40 @@ +/* + * 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 android.content.Intent; +import com.android.wm.shell.bubbles.IBubblesListener; + +/** + * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when + * showing in the bubble bar. + */ +interface IBubbles { + + oneway void registerBubbleListener(in IBubblesListener listener) = 1; + + oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; + + oneway void showBubble(in String key, in boolean onLauncherHome) = 3; + + oneway void removeBubble(in String key, in int reason) = 4; + + oneway void collapseBubbles() = 5; + + oneway void onTaskbarStateChanged(in int newState) = 6; + +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl new file mode 100644 index 000000000000..e48f8d5f1c84 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -0,0 +1,29 @@ +/* + * 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 android.os.Bundle; + +/** + * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. + */ +oneway interface IBubblesListener { + + /** + * Called when the bubbles state changes. + */ + void onBubbleStateChange(in Bundle update); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java new file mode 100644 index 000000000000..81423473171d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java @@ -0,0 +1,137 @@ +/* + * 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.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an update to bubbles state. This is passed through + * {@link com.android.wm.shell.bubbles.IBubblesListener} to launcher so that taskbar may render + * bubbles. This should be kept this as minimal as possible in terms of data. + */ +public class BubbleBarUpdate implements Parcelable { + + public static final String BUNDLE_KEY = "update"; + + public boolean expandedChanged; + public boolean expanded; + @Nullable + public String selectedBubbleKey; + @Nullable + public BubbleInfo addedBubble; + @Nullable + public BubbleInfo updatedBubble; + @Nullable + public String suppressedBubbleKey; + @Nullable + public String unsupressedBubbleKey; + + // This is only populated if bubbles have been removed. + public List<RemovedBubble> removedBubbles = new ArrayList<>(); + + // This is only populated if the order of the bubbles has changed. + public List<String> bubbleKeysInOrder = new ArrayList<>(); + + // This is only populated the first time a listener is connected so it gets the current state. + public List<BubbleInfo> currentBubbleList = new ArrayList<>(); + + public BubbleBarUpdate() { + } + + public BubbleBarUpdate(Parcel parcel) { + expandedChanged = parcel.readBoolean(); + expanded = parcel.readBoolean(); + selectedBubbleKey = parcel.readString(); + addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + updatedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + suppressedBubbleKey = parcel.readString(); + unsupressedBubbleKey = parcel.readString(); + removedBubbles = parcel.readParcelableList(new ArrayList<>(), + RemovedBubble.class.getClassLoader()); + parcel.readStringList(bubbleKeysInOrder); + currentBubbleList = parcel.readParcelableList(new ArrayList<>(), + BubbleInfo.class.getClassLoader()); + } + + /** + * Returns whether anything has changed in this update. + */ + public boolean anythingChanged() { + return expandedChanged + || selectedBubbleKey != null + || addedBubble != null + || updatedBubble != null + || !removedBubbles.isEmpty() + || !bubbleKeysInOrder.isEmpty() + || suppressedBubbleKey != null + || unsupressedBubbleKey != null + || !currentBubbleList.isEmpty(); + } + + @Override + public String toString() { + return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + + " expanded=" + expanded + + " selectedBubbleKey=" + selectedBubbleKey + + " addedBubble=" + addedBubble + + " updatedBubble=" + updatedBubble + + " suppressedBubbleKey=" + suppressedBubbleKey + + " unsuppressedBubbleKey=" + unsupressedBubbleKey + + " removedBubbles=" + removedBubbles + + " bubbles=" + bubbleKeysInOrder + + " currentBubbleList=" + currentBubbleList + + " }"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBoolean(expandedChanged); + parcel.writeBoolean(expanded); + parcel.writeString(selectedBubbleKey); + parcel.writeParcelable(addedBubble, flags); + parcel.writeParcelable(updatedBubble, flags); + parcel.writeString(suppressedBubbleKey); + parcel.writeString(unsupressedBubbleKey); + parcel.writeParcelableList(removedBubbles, flags); + parcel.writeStringList(bubbleKeysInOrder); + parcel.writeParcelableList(currentBubbleList, flags); + } + + @NonNull + public static final Creator<BubbleBarUpdate> CREATOR = + new Creator<BubbleBarUpdate>() { + public BubbleBarUpdate createFromParcel(Parcel source) { + return new BubbleBarUpdate(source); + } + public BubbleBarUpdate[] newArray(int size) { + return new BubbleBarUpdate[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java new file mode 100644 index 000000000000..b0dea7231a1e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java @@ -0,0 +1,154 @@ +/* + * 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.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Contains information necessary to present a bubble. + */ +public class BubbleInfo implements Parcelable { + + // TODO(b/269672147): needs a title string for a11y & that comes from notification + // TODO(b/269671451): needs whether the bubble is an 'important person' or not + + private String mKey; // Same key as the Notification + private int mFlags; // Flags from BubbleMetadata + private String mShortcutId; + private int mUserId; + private String mPackageName; + /** + * All notification bubbles require a shortcut to be set on the notification, however, the + * app could still specify an Icon and PendingIntent to use for the bubble. In that case + * this icon will be populated. If the bubble is entirely shortcut based, this will be null. + */ + @Nullable + private Icon mIcon; + + public BubbleInfo(String key, int flags, String shortcutId, @Nullable Icon icon, + int userId, String packageName) { + mKey = key; + mFlags = flags; + mShortcutId = shortcutId; + mIcon = icon; + mUserId = userId; + mPackageName = packageName; + } + + public BubbleInfo(Parcel source) { + mKey = source.readString(); + mFlags = source.readInt(); + mShortcutId = source.readString(); + mIcon = source.readTypedObject(Icon.CREATOR); + mUserId = source.readInt(); + mPackageName = source.readString(); + } + + public String getKey() { + return mKey; + } + + public String getShortcutId() { + return mShortcutId; + } + + public Icon getIcon() { + return mIcon; + } + + public int getFlags() { + return mFlags; + } + + public int getUserId() { + return mUserId; + } + + public String getPackageName() { + return mPackageName; + } + + /** + * Whether this bubble is currently being hidden from the stack. + */ + public boolean isBubbleSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0; + } + + /** + * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API + * to + * hide the bubble when in the same content). + */ + public boolean isBubbleSuppressable() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0; + } + + /** + * Whether the notification for this bubble is hidden from the shade. + */ + public boolean isNotificationSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BubbleInfo)) return false; + BubbleInfo bubble = (BubbleInfo) o; + return Objects.equals(mKey, bubble.mKey); + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mKey); + parcel.writeInt(mFlags); + parcel.writeString(mShortcutId); + parcel.writeTypedObject(mIcon, flags); + parcel.writeInt(mUserId); + parcel.writeString(mPackageName); + } + + @NonNull + public static final Creator<BubbleInfo> CREATOR = + new Creator<BubbleInfo>() { + public BubbleInfo createFromParcel(Parcel source) { + return new BubbleInfo(source); + } + + public BubbleInfo[] newArray(int size) { + return new BubbleInfo[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java new file mode 100644 index 000000000000..f90591b84b7e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java @@ -0,0 +1,70 @@ +/* + * 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.common.bubbles; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a removed bubble, defining the key and reason the bubble was removed. + */ +public class RemovedBubble implements Parcelable { + + private final String mKey; + private final int mRemovalReason; + + public RemovedBubble(String key, int removalReason) { + mKey = key; + mRemovalReason = removalReason; + } + + public RemovedBubble(Parcel parcel) { + mKey = parcel.readString(); + mRemovalReason = parcel.readInt(); + } + + public String getKey() { + return mKey; + } + + public int getRemovalReason() { + return mRemovalReason; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKey); + dest.writeInt(mRemovalReason); + } + + @NonNull + public static final Creator<RemovedBubble> CREATOR = + new Creator<RemovedBubble>() { + public RemovedBubble createFromParcel(Parcel source) { + return new RemovedBubble(source); + } + public RemovedBubble[] newArray(int size) { + return new RemovedBubble[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java index bdda6a8e926b..bfa63909cd47 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -22,6 +22,8 @@ package com.android.wm.shell.sysui; public class ShellSharedConstants { // See IPip.aidl public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip"; + // See IBubbles.aidl + public static final String KEY_EXTRA_SHELL_BUBBLES = "extra_shell_bubbles"; // See ISplitScreen.aidl public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen"; // See IOneHanded.aidl |