diff options
4 files changed, 661 insertions, 66 deletions
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 7119145a1fa8..48c974a33f12 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 @@ -117,6 +117,8 @@ public class NotificationContentView extends FrameLayout implements Notification protected HybridNotificationView mSingleLineView; @Nullable public DisposableHandle mContractedBinderHandle; + @Nullable public DisposableHandle mExpandedBinderHandle; + @Nullable public DisposableHandle mHeadsUpBinderHandle; private RemoteInputView mExpandedRemoteInput; private RemoteInputView mHeadsUpRemoteInput; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index a5cd2a2de085..c342bcd2706b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -46,6 +46,9 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP @@ -286,11 +289,15 @@ constructor( } FLAG_CONTENT_VIEW_EXPANDED -> row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) { + row.privateLayout.mExpandedBinderHandle?.dispose() + row.privateLayout.mExpandedBinderHandle = null row.privateLayout.setExpandedChild(null) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED) } FLAG_CONTENT_VIEW_HEADS_UP -> row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) { + row.privateLayout.mHeadsUpBinderHandle?.dispose() + row.privateLayout.mHeadsUpBinderHandle = null row.privateLayout.setHeadsUpChild(null) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP) row.privateLayout.setHeadsUpInflatedSmartReplies(null) @@ -499,17 +506,87 @@ constructor( } } - if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) { + val richOngoingContentModel = inflationProgress.contentModel.richOngoingContentModel + + if ( + richOngoingContentModel != null && + reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0 + ) { logger.logAsyncTaskProgress(entry, "inflating RON view") - inflationProgress.richOngoingNotificationViewHolder = - inflationProgress.contentModel.richOngoingContentModel?.let { + val inflateContractedView = reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0 + val inflateExpandedView = reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0 + val inflateHeadsUpView = reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 + + inflationProgress.contractedRichOngoingNotificationViewHolder = + if (inflateContractedView) { ronInflater.inflateView( - contentModel = it, + contentModel = richOngoingContentModel, existingView = row.privateLayout.contractedChild, entry = entry, systemUiContext = context, - parentView = row.privateLayout + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.Contracted + ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.contractedChild, + viewType = RichOngoingNotificationViewType.Contracted + ) + ) { + KeepExistingView + } else { + NullContentView + } + } + + inflationProgress.expandedRichOngoingNotificationViewHolder = + if (inflateExpandedView) { + ronInflater.inflateView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.expandedChild, + entry = entry, + systemUiContext = context, + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.Expanded ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.expandedChild, + viewType = RichOngoingNotificationViewType.Expanded + ) + ) { + KeepExistingView + } else { + NullContentView + } + } + + inflationProgress.headsUpRichOngoingNotificationViewHolder = + if (inflateHeadsUpView) { + ronInflater.inflateView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.headsUpChild, + entry = entry, + systemUiContext = context, + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.HeadsUp + ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.headsUpChild, + viewType = RichOngoingNotificationViewType.HeadsUp + ) + ) { + KeepExistingView + } else { + NullContentView + } } } @@ -618,7 +695,9 @@ constructor( var inflatedSmartReplyState: InflatedSmartReplyState? = null var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null - var richOngoingNotificationViewHolder: InflatedContentViewHolder? = null + var contractedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null + var expandedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null + var headsUpRichOngoingNotificationViewHolder: ContentViewInflationResult? = null // Inflated SingleLineView that lacks the UI State var inflatedSingleLineView: HybridNotificationView? = null @@ -1428,14 +1507,21 @@ constructor( logger.logAsyncTaskProgress(entry, "finishing") // before updating the content model, stop existing binding if necessary - val hasRichOngoingContentModel = result.contentModel.richOngoingContentModel != null - val requestedRichOngoing = reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0 - val rejectedRichOngoing = requestedRichOngoing && !hasRichOngoingContentModel - if (result.richOngoingNotificationViewHolder != null || rejectedRichOngoing) { + if (result.contractedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { row.privateLayout.mContractedBinderHandle?.dispose() row.privateLayout.mContractedBinderHandle = null } + if (result.expandedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { + row.privateLayout.mExpandedBinderHandle?.dispose() + row.privateLayout.mExpandedBinderHandle = null + } + + if (result.headsUpRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { + row.privateLayout.mHeadsUpBinderHandle?.dispose() + row.privateLayout.mHeadsUpBinderHandle = null + } + // set the content model after disposal and before setting new rich ongoing view entry.setContentModel(result.contentModel) result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } @@ -1477,19 +1563,53 @@ constructor( } } - // after updating the content model, set the view, then start the new binder - result.richOngoingNotificationViewHolder?.let { viewHolder -> - row.privateLayout.contractedChild = viewHolder.view - row.privateLayout.expandedChild = null - row.privateLayout.headsUpChild = null - row.privateLayout.setExpandedInflatedSmartReplies(null) - row.privateLayout.setHeadsUpInflatedSmartReplies(null) - row.privateLayout.mContractedBinderHandle = - viewHolder.binder.setupContentViewBinder() - row.setExpandable(false) + val hasRichOngoingViewHolder = + result.contractedRichOngoingNotificationViewHolder != null || + result.expandedRichOngoingNotificationViewHolder != null || + result.headsUpRichOngoingNotificationViewHolder != null + + if (hasRichOngoingViewHolder) { + // after updating the content model, set the view, then start the new binder + result.contractedRichOngoingNotificationViewHolder?.let { contractedViewHolder -> + if (contractedViewHolder is InflatedContentViewHolder) { + row.privateLayout.contractedChild = contractedViewHolder.view + row.privateLayout.mContractedBinderHandle = + contractedViewHolder.binder.setupContentViewBinder() + } else if (contractedViewHolder == NullContentView) { + row.privateLayout.contractedChild = null + } + } + + result.expandedRichOngoingNotificationViewHolder?.let { expandedViewHolder -> + if (expandedViewHolder is InflatedContentViewHolder) { + row.privateLayout.expandedChild = expandedViewHolder.view + row.privateLayout.mExpandedBinderHandle = + expandedViewHolder.binder.setupContentViewBinder() + } else if (expandedViewHolder == NullContentView) { + row.privateLayout.expandedChild = null + } + } + + result.headsUpRichOngoingNotificationViewHolder?.let { headsUpViewHolder -> + if (headsUpViewHolder is InflatedContentViewHolder) { + row.privateLayout.headsUpChild = headsUpViewHolder.view + row.privateLayout.mHeadsUpBinderHandle = + headsUpViewHolder.binder.setupContentViewBinder() + } else if (headsUpViewHolder == NullContentView) { + row.privateLayout.headsUpChild = null + } + } + + // clean remoteViewCache when we don't keep existing views. remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP) + + // Since RONs don't support smart reply, remove them from HUNs and Expanded. + row.privateLayout.setExpandedInflatedSmartReplies(null) + row.privateLayout.setHeadsUpInflatedSmartReplies(null) + + row.setExpandable(row.privateLayout.expandedChild != null) } Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt index e9c4960a4011..828fc219e773 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt @@ -24,6 +24,9 @@ import android.view.ViewGroup import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel @@ -39,7 +42,35 @@ fun interface DeferredContentViewBinder { fun setupContentViewBinder(): DisposableHandle } -class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) +enum class RichOngoingNotificationViewType { + Contracted, + Expanded, + HeadsUp, +} + +/** + * * Supertype of the 3 different possible result types of + * [RichOngoingNotificationViewInflater.inflateView]. + */ +sealed interface ContentViewInflationResult { + + /** Indicates that the content view should be removed if present. */ + data object NullContentView : ContentViewInflationResult + + /** + * Indicates that the content view (which *must be* present) should be unmodified during this + * inflation. + */ + data object KeepExistingView : ContentViewInflationResult + + /** + * Contains the new view and binder that should replace any existing content view for this slot. + */ + data class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) : + ContentViewInflationResult +} + +fun ContentViewInflationResult?.shouldDisposeViewBinder() = this !is KeepExistingView /** * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is @@ -52,7 +83,14 @@ interface RichOngoingNotificationViewInflater { entry: NotificationEntry, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult + + fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean } @SysUISingleton @@ -68,8 +106,9 @@ constructor( entry: NotificationEntry, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? { - if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return null + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return NullContentView val component = viewModelComponentFactory.create(entry) return when (contentModel) { is TimerContentModel -> @@ -77,28 +116,55 @@ constructor( existingView, component::createTimerViewModel, systemUiContext, - parentView + parentView, + viewType ) is StopwatchContentModel -> TODO("Not yet implemented") } } + override fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean { + if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false + return when (contentModel) { + is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType) + is StopwatchContentModel -> TODO("Not yet implemented") + } + } + private fun inflateTimerView( existingView: View?, createViewModel: () -> TimerViewModel, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? { - if (existingView is TimerView && !existingView.isReinflateNeeded()) return null - val newView = - LayoutInflater.from(systemUiContext) - .inflate( - R.layout.rich_ongoing_timer_notification, - parentView, - /* attachToRoot= */ false - ) as TimerView - return InflatedContentViewHolder(newView) { - TimerViewBinder.bindWhileAttached(newView, createViewModel()) + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (existingView is TimerView && !existingView.isReinflateNeeded()) return KeepExistingView + + return when (viewType) { + RichOngoingNotificationViewType.Contracted -> { + val newView = + LayoutInflater.from(systemUiContext) + .inflate( + R.layout.rich_ongoing_timer_notification, + parentView, + /* attachToRoot= */ false + ) as TimerView + InflatedContentViewHolder(newView) { + TimerViewBinder.bindWhileAttached(newView, createViewModel()) + } + } + RichOngoingNotificationViewType.Expanded, + RichOngoingNotificationViewType.HeadsUp -> NullContentView } } + + private fun canKeepTimerView( + contentModel: TimerContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = true } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 491919a16a4e..30a1214d69d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -35,6 +35,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED @@ -74,6 +77,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @SmallTest @@ -125,8 +129,10 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { ): RichOngoingContentModel? = fakeRonContentModel } - private var fakeRonViewHolder: InflatedContentViewHolder? = null - private val fakeRonViewInflater = + private var fakeContractedRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeExpandedRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeHeadsUpRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeRonViewInflater = spy( object : RichOngoingNotificationViewInflater { override fun inflateView( @@ -134,8 +140,20 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { existingView: View?, entry: NotificationEntry, systemUiContext: Context, - parentView: ViewGroup - ): InflatedContentViewHolder? = fakeRonViewHolder + parentView: ViewGroup, + viewType: RichOngoingNotificationViewType + ): ContentViewInflationResult = + when (viewType) { + RichOngoingNotificationViewType.Contracted -> fakeContractedRonViewHolder + RichOngoingNotificationViewType.Expanded -> fakeExpandedRonViewHolder + RichOngoingNotificationViewType.HeadsUp -> fakeHeadsUpRonViewHolder + } + + override fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = false } ) @@ -149,6 +167,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { .setContentText("Text") .setStyle(Notification.BigTextStyle().bigText("big text")) testHelper = NotificationTestHelper(mContext, mDependency) + testHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL) row = spy(testHelper.createRow(builder.build())) notificationInflater = NotificationRowContentBinderImpl( @@ -388,15 +407,62 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testRonModelRequiredForRonView() { fakeRonContentModel = null + val contractedRonView = View(context) + val expandedRonView = View(context) + val headsUpRonView = View(context) + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = contractedRonView, binder = mock()) + fakeExpandedRonViewHolder = + InflatedContentViewHolder(view = expandedRonView, binder = mock()) + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = headsUpRonView, binder = mock()) + + // WHEN inflater inflates + val contentToInflate = + FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP + inflateAndWait(notificationInflater, contentToInflate, row) + verifyZeroInteractions(fakeRonViewInflater) + } + + @Test + fun testRonModelCleansUpRemoteViews() { val ronView = View(context) - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock()) + + val entry = row.entry + + fakeRonContentModel = mock<TimerContentModel>() + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = ronView, binder = mock<DeferredContentViewBinder>()) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) - verify(fakeRonViewInflater, never()).inflateView(any(), any(), any(), any(), any()) + + // VERIFY + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_CONTRACTED)) + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_EXPANDED)) + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_HEADS_UP)) } @Test - fun testRonModelTriggersInflationOfRonView() { + fun testRonModelCleansUpSmartReplies() { + val ronView = View(context) + + val privateLayout = spy(row.privateLayout) + + row.privateLayout = privateLayout + + fakeRonContentModel = mock<TimerContentModel>() + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock()) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // VERIFY + verify(privateLayout).setExpandedInflatedSmartReplies(eq(null)) + verify(privateLayout).setHeadsUpInflatedSmartReplies(eq(null)) + } + + @Test + fun testRonModelTriggersInflationOfContractedRonView() { val mockRonModel = mock<TimerContentModel>() val ronView = View(context) val mockBinder = mock<DeferredContentViewBinder>() @@ -405,18 +471,229 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { val privateLayout = row.privateLayout fakeRonContentModel = mockRonModel - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + // VERIFY that the inflater is invoked verify(fakeRonViewInflater) - .inflateView(eq(mockRonModel), any(), eq(entry), any(), eq(privateLayout)) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.Contracted) + ) assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView) verify(mockBinder).setupContentViewBinder() } @Test - fun ronViewAppliesElementsInOrder() { + fun testRonModelTriggersInflationOfExpandedRonView() { + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + val entry = row.entry + val privateLayout = row.privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // VERIFY that the inflater is invoked + verify(fakeRonViewInflater) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.Expanded) + ) + assertThat(row.privateLayout.expandedChild).isSameInstanceAs(ronView) + verify(mockBinder).setupContentViewBinder() + } + + @Test + fun testRonModelTriggersInflationOfHeadsUpRonView() { + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + val entry = row.entry + val privateLayout = row.privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // VERIFY that the inflater is invoked + verify(fakeRonViewInflater) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.HeadsUp) + ) + assertThat(row.privateLayout.headsUpChild).isSameInstanceAs(ronView) + verify(mockBinder).setupContentViewBinder() + } + + @Test + fun keepExistingViewForContractedRonNotChangingContractedChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mContractedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeContractedRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // THEN do not dispose old contracted binder handle and change contracted child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setContractedChild(any()) + } + + @Test + fun keepExistingViewForExpandedRonNotChangingExpandedChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // THEN do not dispose old expanded binder handle and change expanded child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setExpandedChild(any()) + } + + @Test + fun keepExistingViewForHeadsUpRonNotChangingHeadsUpChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // THEN - do not dispose old heads up binder handle and change heads up child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setHeadsUpChild(any()) + } + + @Test + fun nullContentViewForContractedRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mContractedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeContractedRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setContractedChild(eq(null)) + } + } + + @Test + fun nullContentViewForExpandedRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setExpandedChild(eq(null)) + } + } + + @Test + fun nullContentViewForHeadsUpRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setHeadsUpChild(eq(null)) + } + } + + @Test + fun contractedRonViewAppliesElementsInOrder() { val oldHandle = mock<DisposableHandle>() val mockRonModel = mock<TimerContentModel>() val ronView = View(context) @@ -429,7 +706,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.privateLayout = privateLayout fakeRonContentModel = mockRonModel - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) @@ -443,16 +721,89 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } @Test - fun testRonNotReinflating() { - val handle0 = mock<DisposableHandle>() - val handle1 = mock<DisposableHandle>() + fun expandedRonViewAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, mockBinder) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setExpandedChild(eq(ronView)) + verify(mockBinder).setupContentViewBinder() + } + } + + @Test + fun headsUpRonViewAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, mockBinder) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setHeadsUpChild(eq(ronView)) + verify(mockBinder).setupContentViewBinder() + } + } + + @Test + fun testRonNotReinflating() { + val oldContractedBinderHandle = mock<DisposableHandle>() + val oldExpandedBinderHandle = mock<DisposableHandle>() + val oldHeadsUpBinderHandle = mock<DisposableHandle>() + + val contractedBinderHandle = mock<DisposableHandle>() + val expandedBinderHandle = mock<DisposableHandle>() + val headsUpBinderHandle = mock<DisposableHandle>() + + val contractedRonView = View(context) + val expandedRonView = View(context) + val headsUpRonView = View(context) + val mockRonModel1 = mock<TimerContentModel>() val mockRonModel2 = mock<TimerContentModel>() - val mockBinder1 = mock<DeferredContentViewBinder>() - doReturn(handle1).whenever(mockBinder1).setupContentViewBinder() - row.privateLayout.mContractedBinderHandle = handle0 + val mockContractedViewBinder = mock<DeferredContentViewBinder>() + val mockExpandedViewBinder = mock<DeferredContentViewBinder>() + val mockHeadsUpViewBinder = mock<DeferredContentViewBinder>() + + doReturn(contractedBinderHandle).whenever(mockContractedViewBinder).setupContentViewBinder() + doReturn(expandedBinderHandle).whenever(mockExpandedViewBinder).setupContentViewBinder() + doReturn(headsUpBinderHandle).whenever(mockHeadsUpViewBinder).setupContentViewBinder() + + row.privateLayout.mContractedBinderHandle = oldContractedBinderHandle + row.privateLayout.mExpandedBinderHandle = oldExpandedBinderHandle + row.privateLayout.mHeadsUpBinderHandle = oldHeadsUpBinderHandle val entry = spy(row.entry) row.entry = entry val privateLayout = spy(row.privateLayout) @@ -460,31 +811,87 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { // WHEN inflater inflates both a model and a view fakeRonContentModel = mockRonModel1 - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder1) - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = contractedRonView, binder = mockContractedViewBinder) + fakeExpandedRonViewHolder = + InflatedContentViewHolder(view = expandedRonView, binder = mockExpandedViewBinder) + fakeHeadsUpRonViewHolder = + InflatedContentViewHolder(view = headsUpRonView, binder = mockHeadsUpViewBinder) + + val contentToInflate = + FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP + inflateAndWait(notificationInflater, contentToInflate, row) // Validate that these 4 steps happen in this precise order - inOrder(handle0, entry, privateLayout, mockBinder1, handle1) { - verify(handle0).dispose() + inOrder( + oldContractedBinderHandle, + oldExpandedBinderHandle, + oldHeadsUpBinderHandle, + entry, + privateLayout, + mockContractedViewBinder, + mockExpandedViewBinder, + mockHeadsUpViewBinder, + contractedBinderHandle, + expandedBinderHandle, + headsUpBinderHandle + ) { + verify(oldContractedBinderHandle).dispose() + verify(oldExpandedBinderHandle).dispose() + verify(oldHeadsUpBinderHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 }) - verify(privateLayout).setContractedChild(eq(ronView)) - verify(mockBinder1).setupContentViewBinder() - verify(handle1, never()).dispose() + + verify(privateLayout).setContractedChild(eq(contractedRonView)) + verify(mockContractedViewBinder).setupContentViewBinder() + + verify(privateLayout).setExpandedChild(eq(expandedRonView)) + verify(mockExpandedViewBinder).setupContentViewBinder() + + verify(privateLayout).setHeadsUpChild(eq(headsUpRonView)) + verify(mockHeadsUpViewBinder).setupContentViewBinder() + + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() } - clearInvocations(handle0, entry, privateLayout, mockBinder1, handle1) + clearInvocations( + oldContractedBinderHandle, + oldExpandedBinderHandle, + oldHeadsUpBinderHandle, + entry, + privateLayout, + mockContractedViewBinder, + mockExpandedViewBinder, + mockHeadsUpViewBinder, + contractedBinderHandle, + expandedBinderHandle, + headsUpBinderHandle + ) // THEN when the inflater inflates just a model fakeRonContentModel = mockRonModel2 - fakeRonViewHolder = null - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + fakeContractedRonViewHolder = KeepExistingView + fakeExpandedRonViewHolder = KeepExistingView + fakeHeadsUpRonViewHolder = KeepExistingView + + inflateAndWait(notificationInflater, contentToInflate, row) // Validate that for reinflation, the only thing we do us update the model - verify(handle1, never()).dispose() + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 }) verify(privateLayout, never()).setContractedChild(any()) - verify(mockBinder1, never()).setupContentViewBinder() - verify(handle1, never()).dispose() + verify(privateLayout, never()).setExpandedChild(any()) + verify(privateLayout, never()).setHeadsUpChild(any()) + verify(mockContractedViewBinder, never()).setupContentViewBinder() + verify(mockExpandedViewBinder, never()).setupContentViewBinder() + verify(mockHeadsUpViewBinder, never()).setupContentViewBinder() + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() } @Test |