From 0c83394aaeebec3de744cd873d2580182f3e41a6 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Fri, 11 Nov 2022 15:13:22 -0800 Subject: Create interface and data to pass bubble updates to launcher 1) Create a common folder for bubbles in wm shell so that we can share data between shell and launcher, the information shared will be: - BubbleBarUpdate - a specific change in bubble state that launcher can use to update the bubble bar, the update will be created via the BubbleData.Update object - BubbleInfo - an object representing the necessary information to render a bubble, BubbleInfo can be created via a Bubble object - RemovedBubble - an object representing a removed bubble 2) Create a two aidl interfaces allowing launcher to register a listener to get notified of BubbleBarUpdates and to notify WMShell of UI interactions with the bubble bar so that shell can show or hide the TaskView associated with the bubble. 3) Create the impl of the aidl interfaces in BubbleController 4) The listener is only registered if the flag is active Bug: 253318833 Test: treehugger / manual with other CLs Change-Id: I0955772db2502d99367e793d3e2cb229d31bd7b7 --- libs/WindowManager/Shell/Android.bp | 2 + .../src/com/android/wm/shell/bubbles/Bubble.java | 18 ++- .../android/wm/shell/bubbles/BubbleController.java | 138 +++++++++++++++++- .../com/android/wm/shell/bubbles/BubbleData.java | 64 +++++++++ .../src/com/android/wm/shell/bubbles/Bubbles.java | 17 +++ .../src/com/android/wm/shell/bubbles/IBubbles.aidl | 40 ++++++ .../android/wm/shell/bubbles/IBubblesListener.aidl | 29 ++++ .../wm/shell/common/bubbles/BubbleBarUpdate.java | 137 ++++++++++++++++++ .../wm/shell/common/bubbles/BubbleInfo.java | 154 +++++++++++++++++++++ .../wm/shell/common/bubbles/RemovedBubble.java | 70 ++++++++++ .../wm/shell/sysui/ShellSharedConstants.java | 2 + 11 files changed, 669 insertions(+), 2 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java 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 { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -248,6 +256,8 @@ public class BubbleController implements ConfigurationChangeListener { private Optional 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 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 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/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 removedBubbles = new ArrayList<>(); + + // This is only populated if the order of the bubbles has changed. + public List bubbleKeysInOrder = new ArrayList<>(); + + // This is only populated the first time a listener is connected so it gets the current state. + public List 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 CREATOR = + new Creator() { + 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 CREATOR = + new Creator() { + 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 CREATOR = + new Creator() { + 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 -- cgit v1.2.3-59-g8ed1b From 70b6e81102d369829774d2fc64452b29f12f9d56 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Wed, 1 Feb 2023 16:22:28 -0800 Subject: Create BubbleTaskViewHelper to be shared with new bubble expanded view For the bubble bar we will create a different expanded view to show the bubble in. This change factors out common code so that it can be shared between the two separate views. Currently not used by BubbleExpandedView but will be later. Test: atest BubblesTests Bug: 253318833 Bug: 272102927 Change-Id: I8beec37e33fd3e10d9bd603b681b845785c64534 --- .../wm/shell/bubbles/BubbleTaskViewHelper.java | 282 +++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java 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; + } +} -- cgit v1.2.3-59-g8ed1b