Handle process die event of a bubble activity gracefully.
When the process behind a bubble activity died (e.g. killed by lmkd,
or app crash), collapse the bubble and restart the activity when it's
expanded next time.
Bug: 126945401
Test: atest TaskStackChangedListenerTest
Change-Id: Ia2ee21f9ced51fe33756b1e12159f445d6ed083d
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl
index 3f6880f..8c43ae5 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -177,4 +177,11 @@
* @param displayId the id of the display on which contents are drawn.
*/
void onSingleTaskDisplayDrawn(int displayId);
+
+ /*
+ * Called when the last task is removed from a display which can only contain one task.
+ *
+ * @param displayId the id of the display from which the window is removed.
+ */
+ void onSingleTaskDisplayEmpty(int displayId);
}
diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java
index 36daf32..fcf17bf 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -177,4 +177,8 @@
@Override
public void onSingleTaskDisplayDrawn(int displayId) throws RemoteException {
}
+
+ @Override
+ public void onSingleTaskDisplayEmpty(int displayId) throws RemoteException {
+ }
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
index bd2b19c..3ec5642 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
@@ -72,6 +72,13 @@
*/
public void onSingleTaskDisplayDrawn(int displayId) { }
+ /**
+ * Called when the last task is removed from a display which can only contain one task.
+ *
+ * @param displayId the id of the display from which the window is removed.
+ */
+ public void onSingleTaskDisplayEmpty(int displayId) {}
+
public void onTaskProfileLocked(int taskId, int userId) { }
public void onTaskCreated(int taskId, ComponentName componentName) { }
public void onTaskRemoved(int taskId) { }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index c89f2ab..d514a75 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -208,6 +208,12 @@
0 /* unused */).sendToTarget();
}
+ @Override
+ public void onSingleTaskDisplayEmpty(int displayId) throws RemoteException {
+ mHandler.obtainMessage(H.ON_SINGLE_TASK_DISPLAY_EMPTY, displayId,
+ 0 /* unused */).sendToTarget();
+ }
+
private final class H extends Handler {
private static final int ON_TASK_STACK_CHANGED = 1;
private static final int ON_TASK_SNAPSHOT_CHANGED = 2;
@@ -228,6 +234,7 @@
private static final int ON_SIZE_COMPAT_MODE_ACTIVITY_CHANGED = 17;
private static final int ON_BACK_PRESSED_ON_TASK_ROOT = 18;
private static final int ON_SINGLE_TASK_DISPLAY_DRAWN = 19;
+ private static final int ON_SINGLE_TASK_DISPLAY_EMPTY = 20;
public H(Looper looper) {
@@ -370,6 +377,13 @@
}
break;
}
+ case ON_SINGLE_TASK_DISPLAY_EMPTY: {
+ for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+ mTaskStackListeners.get(i).onSingleTaskDisplayEmpty(
+ msg.arg1);
+ }
+ break;
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 573a066..5d61179 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -762,6 +762,20 @@
expandedBubble.setContentVisibility(true);
}
}
+
+ @Override
+ public void onSingleTaskDisplayEmpty(int displayId) {
+ final Bubble expandedBubble = getExpandedBubble(mContext);
+ if (expandedBubble == null) {
+ return;
+ }
+ if (expandedBubble.getDisplayId() == displayId) {
+ mBubbleData.setExpanded(false);
+ }
+ if (expandedBubble.expandedView != null) {
+ expandedBubble.expandedView.notifyDisplayEmpty();
+ }
+ }
}
private static boolean areBubblesEnabled(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 0a19ac3..8c09913 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -63,6 +63,17 @@
public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
+ private enum ActivityViewStatus {
+ // ActivityView is being initialized, cannot start an activity yet.
+ INITIALIZING,
+ // ActivityView is initialized, and ready to start an activity.
+ INITIALIZED,
+ // Activity runs in the ActivityView.
+ ACTIVITY_STARTED,
+ // ActivityView is released, so activity launching will no longer be permitted.
+ RELEASED,
+ }
+
// The triangle pointing to the expanded view
private View mPointerView;
private int mPointerMargin;
@@ -72,7 +83,7 @@
// Views for expanded state
private ActivityView mActivityView;
- private boolean mActivityViewReady = false;
+ private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING;
private int mTaskId = -1;
private PendingIntent mBubbleIntent;
@@ -98,19 +109,21 @@
private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
@Override
public void onActivityViewReady(ActivityView view) {
- if (!mActivityViewReady) {
- mActivityViewReady = true;
- // Custom options so there is no activity transition animation
- ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
- 0 /* enterResId */, 0 /* exitResId */);
- // Post to keep the lifecycle normal
- post(() -> mActivityView.startActivity(mBubbleIntent, options));
+ switch (mActivityViewStatus) {
+ case INITIALIZING:
+ case INITIALIZED:
+ // Custom options so there is no activity transition animation
+ ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
+ 0 /* enterResId */, 0 /* exitResId */);
+ // Post to keep the lifecycle normal
+ post(() -> mActivityView.startActivity(mBubbleIntent, options));
+ mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED;
}
}
@Override
public void onActivityViewDestroyed(ActivityView view) {
- mActivityViewReady = false;
+ mActivityViewStatus = ActivityViewStatus.RELEASED;
}
@Override
@@ -433,8 +446,10 @@
if (mActivityView == null) {
return;
}
- if (mActivityViewReady) {
- mActivityView.release();
+ switch (mActivityViewStatus) {
+ case INITIALIZED:
+ case ACTIVITY_STARTED:
+ mActivityView.release();
}
if (mTaskId != -1) {
try {
@@ -447,7 +462,16 @@
removeView(mActivityView);
mActivityView = null;
- mActivityViewReady = false;
+ }
+
+ /**
+ * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay}
+ * which {@link ActivityView} uses.
+ */
+ void notifyDisplayEmpty() {
+ if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) {
+ mActivityViewStatus = ActivityViewStatus.INITIALIZED;
+ }
}
private boolean usingActivityView() {
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 74c3069..d902454 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -5441,6 +5441,7 @@
task.cleanUpResourcesForDestroy();
}
+ final ActivityDisplay display = getDisplay();
if (mTaskHistory.isEmpty()) {
if (DEBUG_STACK) Slog.i(TAG_STACK, "removeTask: removing stack=" + this);
// We only need to adjust focused stack if this stack is in focus and we are not in the
@@ -5449,11 +5450,11 @@
&& mRootActivityContainer.isTopDisplayFocusedStack(this)) {
String myReason = reason + " leftTaskHistoryEmpty";
if (!inMultiWindowMode() || adjustFocusToNextFocusableStack(myReason) == null) {
- getDisplay().moveHomeStackToFront(myReason);
+ display.moveHomeStackToFront(myReason);
}
}
if (isAttached()) {
- getDisplay().positionChildAtBottom(this);
+ display.positionChildAtBottom(this);
}
if (!isActivityTypeHome() || !isAttached()) {
remove();
@@ -5466,6 +5467,9 @@
if (inPinnedWindowingMode()) {
mService.getTaskChangeNotificationController().notifyActivityUnpinned();
}
+ if (display.isSingleTaskInstance()) {
+ mService.notifySingleTaskDisplayEmpty(display.mDisplayId);
+ }
}
TaskRecord createTaskRecord(int taskId, ActivityInfo info, Intent intent,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 9fc278e..277da06 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -6040,6 +6040,10 @@
return allUids.contains(uid);
}
+ void notifySingleTaskDisplayEmpty(int displayId) {
+ mTaskChangeNotificationController.notifySingleTaskDisplayEmpty(displayId);
+ }
+
final class H extends Handler {
static final int REPORT_TIME_TRACKER_MSG = 1;
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index 27175c7..80470c6 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -55,6 +55,7 @@
private static final int NOTIFY_SIZE_COMPAT_MODE_ACTIVITY_CHANGED_MSG = 20;
private static final int NOTIFY_BACK_PRESSED_ON_TASK_ROOT = 21;
private static final int NOTIFY_SINGLE_TASK_DISPLAY_DRAWN = 22;
+ private static final int NOTIFY_SINGLE_TASK_DISPLAY_EMPTY = 23;
// Delay in notifying task stack change listeners (in millis)
private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100;
@@ -159,6 +160,10 @@
l.onSingleTaskDisplayDrawn(m.arg1);
};
+ private final TaskStackConsumer mNotifySingleTaskDisplayEmpty = (l, m) -> {
+ l.onSingleTaskDisplayEmpty(m.arg1);
+ };
+
@FunctionalInterface
public interface TaskStackConsumer {
void accept(ITaskStackListener t, Message m) throws RemoteException;
@@ -241,6 +246,9 @@
case NOTIFY_SINGLE_TASK_DISPLAY_DRAWN:
forAllRemoteListeners(mNotifySingleTaskDisplayDrawn, msg);
break;
+ case NOTIFY_SINGLE_TASK_DISPLAY_EMPTY:
+ forAllRemoteListeners(mNotifySingleTaskDisplayEmpty, msg);
+ break;
}
}
}
@@ -495,4 +503,15 @@
forAllLocalListeners(mNotifySingleTaskDisplayDrawn, msg);
msg.sendToTarget();
}
+
+ /**
+ * Notify listeners that the last task is removed from a single task display.
+ */
+ void notifySingleTaskDisplayEmpty(int displayId) {
+ final Message msg = mHandler.obtainMessage(
+ NOTIFY_SINGLE_TASK_DISPLAY_EMPTY,
+ displayId, 0 /* unused */);
+ forAllLocalListeners(mNotifySingleTaskDisplayEmpty, msg);
+ msg.sendToTarget();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
index 19fd93fe..6e41118 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
@@ -271,6 +271,54 @@
waitForCallback(singleTaskDisplayDrawnLatch);
}
+ @Test
+ public void testSingleTaskDisplayEmpty() throws Exception {
+ final Instrumentation instrumentation = getInstrumentation();
+
+ final CountDownLatch activityViewReadyLatch = new CountDownLatch(1);
+ final CountDownLatch activityViewDestroyedLatch = new CountDownLatch(1);
+ final CountDownLatch singleTaskDisplayDrawnLatch = new CountDownLatch(1);
+ final CountDownLatch singleTaskDisplayEmptyLatch = new CountDownLatch(1);
+
+ registerTaskStackChangedListener(new TaskStackListener() {
+ @Override
+ public void onSingleTaskDisplayDrawn(int displayId) throws RemoteException {
+ singleTaskDisplayDrawnLatch.countDown();
+ }
+ @Override
+ public void onSingleTaskDisplayEmpty(int displayId)
+ throws RemoteException {
+ singleTaskDisplayEmptyLatch.countDown();
+ }
+ });
+ final ActivityViewTestActivity activity =
+ (ActivityViewTestActivity) startTestActivity(ActivityViewTestActivity.class);
+ final ActivityView activityView = activity.getActivityView();
+ activityView.setCallback(new ActivityView.StateCallback() {
+ @Override
+ public void onActivityViewReady(ActivityView view) {
+ activityViewReadyLatch.countDown();
+ }
+
+ @Override
+ public void onActivityViewDestroyed(ActivityView view) {
+ activityViewDestroyedLatch.countDown();
+ }
+ });
+ waitForCallback(activityViewReadyLatch);
+
+ final Context context = instrumentation.getContext();
+ Intent intent = new Intent(context, ActivityInActivityView.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ activityView.startActivity(intent);
+ waitForCallback(singleTaskDisplayDrawnLatch);
+ assertEquals(1, singleTaskDisplayEmptyLatch.getCount());
+
+ activityView.release();
+ waitForCallback(activityViewDestroyedLatch);
+ waitForCallback(singleTaskDisplayEmptyLatch);
+ }
+
/**
* Starts the provided activity and returns the started instance.
*/