diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 7e70c2059e60f..fe2f1f3eefc5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -30,6 +30,7 @@ import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; +import com.android.systemui.statusbar.notification.DynamicChildBindController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; @@ -59,11 +60,12 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle private final Handler mHandler; - //TODO: change this top >? - private final HashMap> - mTmpChildOrderMap = new HashMap<>(); + /** Re-usable map of notifications to their sorted children.*/ + private final HashMap> mTmpChildOrderMap = + new HashMap<>(); // Dependencies: + private final DynamicChildBindController mDynamicChildBindController; protected final NotificationLockscreenUserManager mLockscreenUserManager; protected final NotificationGroupManager mGroupManager; protected final VisualStabilityManager mVisualStabilityManager; @@ -105,7 +107,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle KeyguardBypassController bypassController, BubbleController bubbleController, DynamicPrivacyController privacyController, - ForegroundServiceSectionController fgsSectionController) { + ForegroundServiceSectionController fgsSectionController, + DynamicChildBindController dynamicChildBindController) { mContext = context; mHandler = mainHandler; mLockscreenUserManager = notificationLockscreenUserManager; @@ -121,6 +124,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle mBubbleController = bubbleController; mDynamicPrivacyController = privacyController; privacyController.addListener(this); + mDynamicChildBindController = dynamicChildBindController; } public void setUpWithPresenter(NotificationPresenter presenter, @@ -175,13 +179,12 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle ent.getRow().setNeedsRedaction(needsRedaction); if (mGroupManager.isChildInGroupWithSummary(ent.getSbn())) { NotificationEntry summary = mGroupManager.getGroupSummary(ent.getSbn()); - List orderedChildren = - mTmpChildOrderMap.get(summary.getRow()); + List orderedChildren = mTmpChildOrderMap.get(summary); if (orderedChildren == null) { orderedChildren = new ArrayList<>(); - mTmpChildOrderMap.put(summary.getRow(), orderedChildren); + mTmpChildOrderMap.put(summary, orderedChildren); } - orderedChildren.add(ent.getRow()); + orderedChildren.add(ent); } else { toShow.add(ent.getRow()); } @@ -260,6 +263,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } + mDynamicChildBindController.updateChildContentViews(mTmpChildOrderMap); mVisualStabilityManager.onReorderingFinished(); // clear the map again for the next usage mTmpChildOrderMap.clear(); @@ -274,6 +278,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle private void addNotificationChildrenAndSort() { // Let's now add all notification children which are missing boolean orderChanged = false; + ArrayList orderedRows = new ArrayList<>(); for (int i = 0; i < mListContainer.getContainerChildCount(); i++) { View view = mListContainer.getContainerChildAt(i); if (!(view instanceof ExpandableNotificationRow)) { @@ -283,11 +288,11 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle ExpandableNotificationRow parent = (ExpandableNotificationRow) view; List children = parent.getNotificationChildren(); - List orderedChildren = mTmpChildOrderMap.get(parent); + List orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size(); childIndex++) { - ExpandableNotificationRow childView = orderedChildren.get(childIndex); + ExpandableNotificationRow childView = orderedChildren.get(childIndex).getRow(); if (children == null || !children.contains(childView)) { if (childView.getParent() != null) { Log.wtf(TAG, "trying to add a notification child that already has " + @@ -300,11 +305,13 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle parent.addChildNotification(childView, childIndex); mListContainer.notifyGroupChildAdded(childView); } + orderedRows.add(childView); } // Finally after removing and adding has been performed we can apply the order. - orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager, + orderChanged |= parent.applyChildOrder(orderedRows, mVisualStabilityManager, mEntryManager); + orderedRows.clear(); } if (orderChanged) { mListContainer.generateChildOrderChangedEvent(); @@ -323,13 +330,13 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle ExpandableNotificationRow parent = (ExpandableNotificationRow) view; List children = parent.getNotificationChildren(); - List orderedChildren = mTmpChildOrderMap.get(parent); + List orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); if (children != null) { toRemove.clear(); for (ExpandableNotificationRow childRow : children) { if ((orderedChildren == null - || !orderedChildren.contains(childRow)) + || !orderedChildren.contains(childRow.getEntry())) && !childRow.keepInParent()) { toRemove.add(childRow); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 10bb5a4bb57ee..4c99a90e7da09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationViewHierarchyManager; import com.android.systemui.statusbar.SmartReplyController; +import com.android.systemui.statusbar.notification.DynamicChildBindController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; @@ -138,7 +139,8 @@ public interface StatusBarDependenciesModule { KeyguardBypassController bypassController, BubbleController bubbleController, DynamicPrivacyController privacyController, - ForegroundServiceSectionController fgsSectionController) { + ForegroundServiceSectionController fgsSectionController, + DynamicChildBindController dynamicChildBindController) { return new NotificationViewHierarchyManager( context, mainHandler, @@ -150,7 +152,8 @@ public interface StatusBarDependenciesModule { bypassController, bubbleController, privacyController, - fgsSectionController); + fgsSectionController, + dynamicChildBindController); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java new file mode 100644 index 0000000000000..059d6ffa91801 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2019 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.systemui.statusbar.notification; + +import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_EXPANDED; +import static com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; + +import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.RowContentBindParams; +import com.android.systemui.statusbar.notification.row.RowContentBindStage; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +/** + * Controller that binds/unbinds views content views on notification group children. + * + * We currently only show a limited number of notification children even if more exist, so we + * can save memory by freeing content views when they're not visible and binding them again when + * they get close to being visible. + * + * Eventually, when {@link NotifPipeline} takes over as the new notification pipeline, we'll have + * more control over which notifications even make it to inflation in the first place and be able + * to enforce this at an earlier stage at the level of the {@link ExpandableNotificationRow}, but + * for now, we're just doing it at the level of content views. + */ +public class DynamicChildBindController { + private final RowContentBindStage mStage; + private final int mChildBindCutoff; + + @Inject + public DynamicChildBindController(RowContentBindStage stage) { + this(stage, CHILD_BIND_CUTOFF); + } + + /** + * @param childBindCutoff the cutoff where we no longer bother having content views bound + */ + DynamicChildBindController( + RowContentBindStage stage, + int childBindCutoff) { + mStage = stage; + mChildBindCutoff = childBindCutoff; + } + + /** + * Update the child content views, unbinding content views on children that won't be visible + * and binding content views on children that will be visible eventually. + * + * @param groupNotifs map of notification summaries to their children + */ + public void updateChildContentViews( + Map> groupNotifs) { + for (NotificationEntry entry : groupNotifs.keySet()) { + List children = groupNotifs.get(entry); + for (int j = 0; j < children.size(); j++) { + NotificationEntry childEntry = children.get(j); + if (j >= mChildBindCutoff) { + if (hasChildContent(childEntry)) { + freeChildContent(childEntry); + } + } else { + if (!hasChildContent(childEntry)) { + bindChildContent(childEntry); + } + } + } + } + } + + private boolean hasChildContent(NotificationEntry entry) { + ExpandableNotificationRow row = entry.getRow(); + return row.getPrivateLayout().getContractedChild() != null + || row.getPrivateLayout().getExpandedChild() != null; + } + + private void freeChildContent(NotificationEntry entry) { + RowContentBindParams params = mStage.getStageParams(entry); + params.freeContentViews(FLAG_CONTENT_VIEW_CONTRACTED); + params.freeContentViews(FLAG_CONTENT_VIEW_EXPANDED); + mStage.requestRebind(entry, null); + } + + private void bindChildContent(NotificationEntry entry) { + RowContentBindParams params = mStage.getStageParams(entry); + params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED); + params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); + mStage.requestRebind(entry, null); + } + + /** + * How big the buffer of extra views we keep around to be ready to show when we do need to + * dynamically inflate. + */ + private static final int EXTRA_VIEW_BUFFER_COUNT = 1; + + private static final int CHILD_BIND_CUTOFF = + NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED + EXTRA_VIEW_BUFFER_COUNT; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 004b56b194189..6d3f1267b38bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -18,7 +18,10 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -467,6 +470,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } }; switch (inflationFlag) { + case FLAG_CONTENT_VIEW_CONTRACTED: + getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, + freeViewRunnable); + break; + case FLAG_CONTENT_VIEW_EXPANDED: + getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, + freeViewRunnable); + break; case FLAG_CONTENT_VIEW_HEADS_UP: getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, freeViewRunnable); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index e3ca283d13ad3..6dd4ff9235c43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; import android.annotation.NonNull; @@ -191,6 +192,18 @@ public class NotificationContentInflater implements NotificationRowContentBinder private void freeNotificationView(NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag) { switch (inflateFlag) { + case FLAG_CONTENT_VIEW_CONTRACTED: + if (row.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_CONTRACTED)) { + row.getPrivateLayout().setContractedChild(null); + mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED); + } + break; + case FLAG_CONTENT_VIEW_EXPANDED: + if (row.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_EXPANDED)) { + row.getPrivateLayout().setExpandedChild(null); + mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); + } + break; case FLAG_CONTENT_VIEW_HEADS_UP: if (row.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_HEADSUP)) { row.getPrivateLayout().setHeadsUpChild(null); @@ -204,8 +217,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); } break; - case FLAG_CONTENT_VIEW_CONTRACTED: - case FLAG_CONTENT_VIEW_EXPANDED: default: break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index d1b9a87c1ddc5..27fd1b2c5aed6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -71,7 +71,13 @@ public class NotificationContentView extends FrameLayout { public static final int VISIBLE_TYPE_EXPANDED = 1; public static final int VISIBLE_TYPE_HEADSUP = 2; private static final int VISIBLE_TYPE_SINGLELINE = 3; - public static final int UNDEFINED = -1; + /** + * Used when there is no content on the view such as when we're a public layout but don't + * need to show. + */ + private static final int VISIBLE_TYPE_NONE = -1; + + private static final int UNDEFINED = -1; private final Rect mClipBounds = new Rect(); @@ -99,7 +105,7 @@ public class NotificationContentView extends FrameLayout { private HybridGroupManager mHybridGroupManager; private int mClipTopAmount; private int mContentHeight; - private int mVisibleType = VISIBLE_TYPE_CONTRACTED; + private int mVisibleType = VISIBLE_TYPE_NONE; private boolean mAnimate; private boolean mIsHeadsUp; private boolean mLegacy; @@ -141,7 +147,7 @@ public class NotificationContentView extends FrameLayout { /** The visible type at the start of a touch driven transformation */ private int mTransformationStartVisibleType; /** The visible type at the start of an animation driven transformation */ - private int mAnimationStartVisibleType = UNDEFINED; + private int mAnimationStartVisibleType = VISIBLE_TYPE_NONE; private boolean mUserExpanding; private int mSingleLineWidthIndention; private boolean mForceSelectNextLayout = true; @@ -386,7 +392,7 @@ public class NotificationContentView extends FrameLayout { mContractedChild = null; mContractedWrapper = null; if (mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED) { - mTransformationStartVisibleType = UNDEFINED; + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } return; } @@ -434,7 +440,7 @@ public class NotificationContentView extends FrameLayout { mExpandedChild = null; mExpandedWrapper = null; if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) { - mTransformationStartVisibleType = UNDEFINED; + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } if (mVisibleType == VISIBLE_TYPE_EXPANDED) { selectLayout(false /* animate */, true /* force */); @@ -472,7 +478,7 @@ public class NotificationContentView extends FrameLayout { mHeadsUpChild = null; mHeadsUpWrapper = null; if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { - mTransformationStartVisibleType = UNDEFINED; + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } if (mVisibleType == VISIBLE_TYPE_HEADSUP) { selectLayout(false /* animate */, true /* force */); @@ -597,7 +603,7 @@ public class NotificationContentView extends FrameLayout { } // Size change of the expanded version - if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0 + if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart != UNDEFINED && mExpandedChild != null) { return Math.min(mContentHeightAtAnimationStart, getViewHeight(VISIBLE_TYPE_EXPANDED)); } @@ -607,10 +613,12 @@ public class NotificationContentView extends FrameLayout { hint = getViewHeight(VISIBLE_TYPE_HEADSUP); } else if (mExpandedChild != null) { hint = getViewHeight(VISIBLE_TYPE_EXPANDED); - } else { + } else if (mContractedChild != null) { hint = getViewHeight(VISIBLE_TYPE_CONTRACTED) + mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_action_list_height); + } else { + hint = getMinHeight(); } if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) { @@ -646,7 +654,7 @@ public class NotificationContentView extends FrameLayout { if (mForceSelectNextLayout) { forceUpdateVisibilities(); } - if (mTransformationStartVisibleType != UNDEFINED + if (mTransformationStartVisibleType != VISIBLE_TYPE_NONE && mVisibleType != mTransformationStartVisibleType && getViewForVisibleType(mTransformationStartVisibleType) != null) { final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType); @@ -823,7 +831,7 @@ public class NotificationContentView extends FrameLayout { fireExpandedVisibleListenerIfVisible(); // forceUpdateVisibilities cancels outstanding animations without updating the // mAnimationStartVisibleType. Do so here instead. - mAnimationStartVisibleType = UNDEFINED; + mAnimationStartVisibleType = VISIBLE_TYPE_NONE; } private void fireExpandedVisibleListenerIfVisible() { @@ -898,7 +906,7 @@ public class NotificationContentView extends FrameLayout { fireExpandedVisibleListenerIfVisible(); // updateViewVisibilities cancels outstanding animations without updating the // mAnimationStartVisibleType. Do so here instead. - mAnimationStartVisibleType = UNDEFINED; + mAnimationStartVisibleType = VISIBLE_TYPE_NONE; } private void updateViewVisibility(int visibleType, int type, View view, @@ -924,7 +932,7 @@ public class NotificationContentView extends FrameLayout { if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) { hiddenView.setVisible(false); } - mAnimationStartVisibleType = UNDEFINED; + mAnimationStartVisibleType = VISIBLE_TYPE_NONE; } }); fireExpandedVisibleListenerIfVisible(); @@ -1041,8 +1049,10 @@ public class NotificationContentView extends FrameLayout { && (!mIsChildInGroup || isGroupExpanded() || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { return VISIBLE_TYPE_CONTRACTED; - } else { + } else if (!noExpandedChild) { return VISIBLE_TYPE_EXPANDED; + } else { + return VISIBLE_TYPE_NONE; } } } @@ -1423,7 +1433,8 @@ public class NotificationContentView extends FrameLayout { if (mExpandedChild != null && mExpandedChild.getHeight() != 0) { if ((!mIsHeadsUp && !mHeadsUpAnimatingAway) || mHeadsUpChild == null || !mContainingNotification.canShowHeadsUp()) { - if (mExpandedChild.getHeight() <= mContractedChild.getHeight()) { + if (mContractedChild == null + || mExpandedChild.getHeight() <= mContractedChild.getHeight()) { expandable = false; } } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) { @@ -1514,7 +1525,7 @@ public class NotificationContentView extends FrameLayout { if (userExpanding) { mTransformationStartVisibleType = mVisibleType; } else { - mTransformationStartVisibleType = UNDEFINED; + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; mVisibleType = calculateVisibleType(); updateViewVisibilities(mVisibleType); updateBackgroundColor(false); @@ -1558,6 +1569,7 @@ public class NotificationContentView extends FrameLayout { } public void setContentHeightAnimating(boolean animating) { + //TODO: It's odd that this does nothing when animating is true if (!animating) { mContentHeightAtAnimationStart = UNDEFINED; } @@ -1565,7 +1577,7 @@ public class NotificationContentView extends FrameLayout { @VisibleForTesting boolean isAnimatingVisibleType() { - return mAnimationStartVisibleType != UNDEFINED; + return mAnimationStartVisibleType != VISIBLE_TYPE_NONE; } public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { @@ -1758,17 +1770,25 @@ public class NotificationContentView extends FrameLayout { } public int getExpandHeight() { - int viewType = VISIBLE_TYPE_EXPANDED; - if (mExpandedChild == null) { + int viewType; + if (mExpandedChild != null) { + viewType = VISIBLE_TYPE_EXPANDED; + } else if (mContractedChild != null) { viewType = VISIBLE_TYPE_CONTRACTED; + } else { + return getMinHeight(); } return getViewHeight(viewType) + getExtraRemoteInputHeight(mExpandedRemoteInput); } public int getHeadsUpHeight(boolean forceNoHeader) { - int viewType = VISIBLE_TYPE_HEADSUP; - if (mHeadsUpChild == null) { + int viewType; + if (mHeadsUpChild != null) { + viewType = VISIBLE_TYPE_HEADSUP; + } else if (mContractedChild != null) { viewType = VISIBLE_TYPE_CONTRACTED; + } else { + return getMinHeight(); } // The headsUp remote input quickly switches to the expanded one, so lets also include that // one diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 75ceb0fa19899..d7c88e3a3f60f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -53,8 +53,7 @@ public class NotificationChildrenContainer extends ViewGroup { static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; @VisibleForTesting static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; - @VisibleForTesting - static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; + public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java index 8e87e0a802ef4..cc5f149eabd3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -41,11 +41,11 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; +import com.android.systemui.statusbar.notification.DynamicChildBindController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -106,17 +106,14 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase { mock(KeyguardBypassController.class), mock(BubbleController.class), mock(DynamicPrivacyController.class), - mock(ForegroundServiceSectionController.class)); + mock(ForegroundServiceSectionController.class), + mock(DynamicChildBindController.class)); mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer); } private NotificationEntry createEntry() throws Exception { ExpandableNotificationRow row = mHelper.createRow(); - NotificationEntry entry = new NotificationEntryBuilder() - .setSbn(row.getEntry().getSbn()) - .build(); - entry.setRow(row); - return entry; + return row.getEntry(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java new file mode 100644 index 0000000000000..bf2d59880ffb7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 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.systemui.statusbar.notification; + +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.ArrayMap; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.RowContentBindParams; +import com.android.systemui.statusbar.notification.row.RowContentBindStage; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class DynamicChildBindControllerTest extends SysuiTestCase { + + private DynamicChildBindController mDynamicChildBindController; + private Map> mGroupNotifs = new ArrayMap<>(); + private static final int TEST_CHILD_BIND_CUTOFF = 5; + + @Mock private RowContentBindStage mBindStage; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + allowTestableLooperAsMainThread(); + when(mBindStage.getStageParams(any())).thenReturn(new RowContentBindParams()); + mDynamicChildBindController = + new DynamicChildBindController(mBindStage, TEST_CHILD_BIND_CUTOFF); + } + + @Test + public void testContentViewsOfChildrenBeyondCutoffAreFreed() { + // GIVEN a group notification with one view beyond the cutoff with content bound + NotificationEntry summary = addGroup(TEST_CHILD_BIND_CUTOFF + 1); + NotificationEntry lastChild = mGroupNotifs.get(summary).get(TEST_CHILD_BIND_CUTOFF); + + RowContentBindParams bindParams = mock(RowContentBindParams.class); + when(mBindStage.getStageParams(lastChild)).thenReturn(bindParams); + + // WHEN the controller gets the list + mDynamicChildBindController.updateChildContentViews(mGroupNotifs); + + // THEN we free content views + verify(bindParams).freeContentViews(FLAG_CONTENT_VIEW_CONTRACTED); + verify(bindParams).freeContentViews(FLAG_CONTENT_VIEW_EXPANDED); + verify(mBindStage).requestRebind(eq(lastChild), any()); + } + + @Test + public void testContentViewsBeforeCutoffAreBound() { + // GIVEN a group notification with one view before the cutoff with content unbound + NotificationEntry summary = addGroup(TEST_CHILD_BIND_CUTOFF); + NotificationEntry lastChild = mGroupNotifs.get(summary).get(TEST_CHILD_BIND_CUTOFF - 1); + + lastChild.getRow().getPrivateLayout().setContractedChild(null); + lastChild.getRow().getPrivateLayout().setExpandedChild(null); + + RowContentBindParams bindParams = mock(RowContentBindParams.class); + when(mBindStage.getStageParams(lastChild)).thenReturn(bindParams); + + // WHEN the controller gets the list + mDynamicChildBindController.updateChildContentViews(mGroupNotifs); + + // THEN we bind content views + verify(bindParams).requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED); + verify(bindParams).requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); + verify(mBindStage).requestRebind(eq(lastChild), any()); + } + + private NotificationEntry addGroup(int size) { + NotificationEntry summary = new NotificationEntryBuilder().build(); + summary.setRow(createRow()); + ArrayList children = new ArrayList<>(); + for (int i = 0; i < size; i++) { + NotificationEntry child = new NotificationEntryBuilder().build(); + child.setRow(createRow()); + children.add(child); + } + mGroupNotifs.put(summary, children); + return summary; + } + + private ExpandableNotificationRow createRow() { + ExpandableNotificationRow row = (ExpandableNotificationRow) + LayoutInflater.from(mContext).inflate(R.layout.status_bar_notification_row, null); + row.getPrivateLayout().setContractedChild(new View(mContext)); + row.getPrivateLayout().setExpandedChild(new View(mContext)); + return row; + } +} +