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
This commit is contained in:
Mady Mellor
2023-03-27 21:50:59 +00:00
committed by Android (Google) Code Review
12 changed files with 951 additions and 2 deletions

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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 {
/**

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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];
}
};
}

View File

@@ -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];
}
};
}

View File

@@ -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];
}
};
}

View File

@@ -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