Fix ExpandableView's transient container tracking; prevent crash when mismatched.
Fixes: 213038631
Test: manual dismissing; adding to groups
Signed-off-by: Jeff DeCew <jeffdq@google.com>
Change-Id: Iedad4a54fc44e9ff900357b3486cc62516701c45
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index ab36da5..6218c77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -520,26 +520,66 @@
}
/**
+ * Called when removing a view from its transient container, such as at the end of an animation.
+ * Generally, when operating on ExpandableView instances, this should be used rather than
+ * {@link ExpandableView#removeTransientView(View)} to ensure that the
+ * {@link #getTransientContainer() transient container} is correctly reset.
+ */
+ public void removeFromTransientContainer() {
+ final ViewGroup transientContainer = getTransientContainer();
+ if (transientContainer == null) {
+ return;
+ }
+ final ViewParent parent = getParent();
+ if (parent != transientContainer) {
+ Log.w(TAG, "Expandable view " + this
+ + " has transient container " + transientContainer
+ + " but different parent " + parent);
+ setTransientContainer(null);
+ return;
+ }
+ transientContainer.removeTransientView(this);
+ setTransientContainer(null);
+ }
+
+ /**
* Called before adding this view to a group, which would always throw an exception if this view
- * has a parent, so clean up the transient container and throw an exception if the parent isn't
- * a transient container. Provide as much detail in the event of a crash as possible.
+ * has a different parent, so clean up the transient container and throw an exception if the
+ * parent isn't a transient container. Provide as much detail as possible in the crash.
*/
public void removeFromTransientContainerForAdditionTo(ViewGroup newParent) {
final ViewParent parent = getParent();
+ final ViewGroup transientContainer = getTransientContainer();
if (parent == null) {
- // If this view has no parent, the add will succeed, so do nothing.
+ // If this view has no parent, the add will succeed, so just make sure the tracked
+ // transient container is in sync with the lack of a parent.
+ if (transientContainer != null) {
+ Log.w(TAG, "Expandable view " + this
+ + " has transient container " + transientContainer
+ + " but no parent");
+ setTransientContainer(null);
+ }
return;
}
- ViewGroup transientContainer = getTransientContainer();
if (transientContainer == null) {
throw new IllegalStateException(
"Can't add view " + this + " to container " + newParent + "; current parent "
+ parent + " is not a transient container");
}
if (transientContainer != parent) {
- throw new IllegalStateException(
- "Expandable view " + this + " has transient container " + transientContainer
- + " which is not the same as its parent " + parent);
+ String transientContainerOutOfSyncError = "Expandable view " + this
+ + " has transient container " + transientContainer
+ + " but different parent " + parent;
+ if (parent != newParent) {
+ // Crash with details before addView() crashes without any; the view is being added
+ // to a different parent, and the transient container isn't the parent, so we can't
+ // even (safely) clean that up.
+ throw new IllegalStateException(transientContainerOutOfSyncError);
+ } else {
+ Log.w(TAG, transientContainerOutOfSyncError);
+ setTransientContainer(null);
+ return;
+ }
}
if (parent != newParent) {
Log.w(TAG, "Moving view " + this + " from transient container "
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 88198f3..1d90780 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -215,8 +215,7 @@
// TODO: We should really cancel the active animations here. This will
// happen automatically when the view's intro animation starts, but
// it's a fragile link.
- header.transientContainer?.removeTransientView(header)
- header.transientContainer = null
+ header.removeFromTransientContainer()
parent.addView(header, target)
} else {
parent.changeViewPosition(header, target)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3e9ce25..943f05f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -3213,10 +3213,7 @@
// Clean up any potential transient views if the child has already been swiped
// out, as we won't be animating it further (due to its height already being
// clipped to 0.
- ViewGroup transientContainer = child.getTransientContainer();
- if (transientContainer != null) {
- transientContainer.removeTransientView(child);
- }
+ child.removeFromTransientContainer();
}
}
int animationType = childWasSwipedOut
@@ -3933,7 +3930,11 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void clearTemporaryViewsInGroup(ViewGroup viewGroup) {
while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
- viewGroup.removeTransientView(viewGroup.getTransientView(0));
+ final View transientView = viewGroup.getTransientView(0);
+ viewGroup.removeTransientView(transientView);
+ if (transientView instanceof ExpandableView) {
+ ((ExpandableView) transientView).setTransientContainer(null);
+ }
}
}
@@ -4102,7 +4103,7 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void clearTransient() {
for (ExpandableView view : mClearTransientViewsWhenFinished) {
- StackStateAnimator.removeTransientView(view);
+ view.removeFromTransientContainer();
}
mClearTransientViewsWhenFinished.clear();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 41a80c7..ff36c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -454,10 +454,7 @@
if (!row.isDismissed()) {
handleChildViewDismissed(view);
}
- ViewGroup transientContainer = row.getTransientContainer();
- if (transientContainer != null) {
- transientContainer.removeTransientView(view);
- }
+ row.removeFromTransientContainer();
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 2dc9276..1d0d374 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -322,9 +322,8 @@
private void onAnimationFinished() {
mHostLayout.onChildAnimationFinished();
- for (ExpandableView transientViewsToRemove : mTransientViewsToRemove) {
- transientViewsToRemove.getTransientContainer()
- .removeTransientView(transientViewsToRemove);
+ for (ExpandableView transientViewToRemove : mTransientViewsToRemove) {
+ transientViewToRemove.removeFromTransientContainer();
}
mTransientViewsToRemove.clear();
}
@@ -353,7 +352,7 @@
} else if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
if (changingView.getVisibility() != View.VISIBLE) {
- removeTransientView(changingView);
+ changingView.removeFromTransientContainer();
continue;
}
@@ -390,12 +389,11 @@
}
changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
0 /* delay */, translationDirection, false /* isHeadsUpAppear */,
- 0, () -> removeTransientView(changingView), null);
+ 0, changingView::removeFromTransientContainer, null);
} else if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
- if (mHostLayout.isFullySwipedOut(changingView)
- && changingView.getTransientContainer() != null) {
- changingView.getTransientContainer().removeTransientView(changingView);
+ if (mHostLayout.isFullySwipedOut(changingView)) {
+ changingView.removeFromTransientContainer();
}
} else if (event.animationType == NotificationStackScrollLayout
.AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
@@ -425,7 +423,7 @@
mHostLayout.addTransientView(changingView, 0);
changingView.setTransientContainer(mHostLayout);
mTmpState.initFrom(changingView);
- endRunnable = () -> removeTransientView(changingView);
+ endRunnable = changingView::removeFromTransientContainer;
}
float targetLocation = 0;
boolean needsAnimation = true;
@@ -468,12 +466,6 @@
}
}
- public static void removeTransientView(ExpandableView viewToRemove) {
- if (viewToRemove.getTransientContainer() != null) {
- viewToRemove.getTransientContainer().removeTransientView(viewToRemove);
- }
- }
-
public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
final boolean isRubberbanded) {
final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index 276f246..4d2c0c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -32,6 +32,7 @@
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -46,9 +47,7 @@
import androidx.test.filters.SmallTest;
-import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
@@ -56,7 +55,6 @@
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
-import com.android.systemui.statusbar.notification.people.PeopleHubViewAdapter;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent;
@@ -66,6 +64,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -266,8 +265,9 @@
// THEN the header is first removed from the transient parent before being added to the
// NSSL.
- verify(transientParent).removeTransientView(silentHeaderView);
- verify(mNssl).addView(silentHeaderView, 1);
+ final InOrder inOrder = inOrder(silentHeaderView, mNssl);
+ inOrder.verify(silentHeaderView).removeFromTransientContainer();
+ inOrder.verify(mNssl).addView(eq(silentHeaderView), eq(1));
}
@Test