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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user