1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.row
18 
19 import android.annotation.DimenRes
20 import android.content.res.Resources
21 import android.os.UserHandle
22 import android.service.notification.StatusBarNotification
23 import android.testing.TestableLooper
24 import android.testing.ViewUtils
25 import android.view.NotificationHeaderView
26 import android.view.View
27 import android.view.ViewGroup
28 import android.widget.FrameLayout
29 import android.widget.ImageView
30 import android.widget.LinearLayout
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import androidx.test.filters.SmallTest
33 import com.android.internal.R
34 import com.android.internal.widget.NotificationActionListLayout
35 import com.android.internal.widget.NotificationExpandButton
36 import com.android.systemui.SysuiTestCase
37 import com.android.systemui.statusbar.notification.FeedbackIcon
38 import com.android.systemui.statusbar.notification.collection.NotificationEntry
39 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
40 import com.android.systemui.util.mockito.any
41 import com.android.systemui.util.mockito.mock
42 import com.android.systemui.util.mockito.whenever
43 import junit.framework.Assert.assertEquals
44 import junit.framework.Assert.assertFalse
45 import junit.framework.Assert.assertTrue
46 import org.junit.After
47 import org.junit.Before
48 import org.junit.Test
49 import org.junit.runner.RunWith
50 import org.mockito.Mock
51 import org.mockito.Mockito
52 import org.mockito.Mockito.anyBoolean
53 import org.mockito.Mockito.anyInt
54 import org.mockito.Mockito.clearInvocations
55 import org.mockito.Mockito.doReturn
56 import org.mockito.Mockito.never
57 import org.mockito.Mockito.spy
58 import org.mockito.Mockito.times
59 import org.mockito.Mockito.verify
60 import org.mockito.MockitoAnnotations.initMocks
61 
62 @SmallTest
63 @RunWith(AndroidJUnit4::class)
64 @TestableLooper.RunWithLooper
65 class NotificationContentViewTest : SysuiTestCase() {
66 
67     private lateinit var row: ExpandableNotificationRow
68     private lateinit var fakeParent: ViewGroup
69     @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
70 
71     private val testableResources = mContext.getOrCreateTestableResources()
72     private val contractedHeight =
73         px(com.android.systemui.res.R.dimen.min_notification_layout_height)
74     private val expandedHeight = px(com.android.systemui.res.R.dimen.notification_max_height)
75     private val notificationContentMargin = px(R.dimen.notification_content_margin)
76 
77     @Before
78     fun setup() {
79         initMocks(this)
80         fakeParent =
81             spy(FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE })
82         val mockEntry = createMockNotificationEntry()
83         row =
84             spy(
85                 ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply {
86                     entry = mockEntry
87                 }
88             )
89         ViewUtils.attachView(fakeParent)
90     }
91 
92     @After
93     fun teardown() {
94         fakeParent.removeAllViews()
95         ViewUtils.detachView(fakeParent)
96     }
97 
98     @Test
99     fun contractedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
100         // GIVEN the shade is closed
101         fakeParent.visibility = View.GONE
102 
103         // WHEN a collapsed content is created
104         val view = createContentView(isSystemExpanded = false)
105 
106         // THEN the contractedWrapper is set
107         assertEquals(view.contractedWrapper, view.visibleWrapper)
108         // AND the contractedWrapper is visible, but NOT shown
109         verify(view.contractedWrapper).setVisible(true)
110         verify(view.contractedWrapper, never()).onContentShown(anyBoolean())
111     }
112 
113     @Test
114     fun contractedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
115         // GIVEN the shade is open
116         fakeParent.visibility = View.VISIBLE
117 
118         // WHEN a collapsed content is created
119         val view = createContentView(isSystemExpanded = false)
120 
121         // THEN the contractedWrapper is set
122         assertEquals(view.contractedWrapper, view.visibleWrapper)
123         // AND the contractedWrapper is visible and shown
124         verify(view.contractedWrapper, Mockito.atLeastOnce()).setVisible(true)
125         verify(view.contractedWrapper, times(1)).onContentShown(true)
126     }
127 
128     @Test
129     fun shadeOpens_collapsedWrapperIsSelected_wrapperNotified() {
130         // GIVEN the shade is closed
131         fakeParent.visibility = View.GONE
132         // AND a collapsed content is created
133         val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
134 
135         // WHEN the shade opens
136         fakeParent.visibility = View.VISIBLE
137         view.onVisibilityAggregated(true)
138 
139         // THEN the contractedWrapper is set
140         assertEquals(view.contractedWrapper, view.visibleWrapper)
141         // AND the contractedWrapper is shown
142         verify(view.contractedWrapper, times(1)).onContentShown(true)
143     }
144 
145     @Test
146     fun shadeCloses_collapsedWrapperIsShown_wrapperNotified() {
147         // GIVEN the shade is closed
148         fakeParent.visibility = View.VISIBLE
149         // AND a collapsed content is created
150         val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
151 
152         // WHEN the shade opens
153         fakeParent.visibility = View.GONE
154         view.onVisibilityAggregated(false)
155 
156         // THEN the contractedWrapper is set
157         assertEquals(view.contractedWrapper, view.visibleWrapper)
158         // AND the contractedWrapper is NOT shown
159         verify(view.contractedWrapper, times(1)).onContentShown(false)
160     }
161 
162     @Test
163     fun expandedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
164         // GIVEN the shade is closed
165         fakeParent.visibility = View.GONE
166 
167         // WHEN a system-expanded content is created
168         val view = createContentView(isSystemExpanded = true)
169 
170         // THEN the contractedWrapper is set
171         assertEquals(view.expandedWrapper, view.visibleWrapper)
172         // AND the contractedWrapper is visible, but NOT shown
173         verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
174         verify(view.expandedWrapper, never()).onContentShown(anyBoolean())
175     }
176 
177     @Test
178     fun expandedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
179         // GIVEN the shade is open
180         fakeParent.visibility = View.VISIBLE
181 
182         // WHEN an system-expanded content is created
183         val view = createContentView(isSystemExpanded = true)
184 
185         // THEN the expandedWrapper is set
186         assertEquals(view.expandedWrapper, view.visibleWrapper)
187         // AND the expandedWrapper is visible and shown
188         verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
189         verify(view.expandedWrapper, times(1)).onContentShown(true)
190     }
191 
192     @Test
193     fun shadeOpens_expandedWrapperIsSelected_wrapperNotified() {
194         // GIVEN the shade is closed
195         fakeParent.visibility = View.GONE
196         // AND a system-expanded content is created
197         val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
198 
199         // WHEN the shade opens
200         fakeParent.visibility = View.VISIBLE
201         view.onVisibilityAggregated(true)
202 
203         // THEN the expandedWrapper is set
204         assertEquals(view.expandedWrapper, view.visibleWrapper)
205         // AND the expandedWrapper is shown
206         verify(view.expandedWrapper, times(1)).onContentShown(true)
207     }
208 
209     @Test
210     fun shadeCloses_expandedWrapperIsShown_wrapperNotified() {
211         // GIVEN the shade is open
212         fakeParent.visibility = View.VISIBLE
213         // AND a system-expanded content is created
214         val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
215 
216         // WHEN the shade opens
217         fakeParent.visibility = View.GONE
218         view.onVisibilityAggregated(false)
219 
220         // THEN the expandedWrapper is set
221         assertEquals(view.expandedWrapper, view.visibleWrapper)
222         // AND the expandedWrapper is NOT shown
223         verify(view.expandedWrapper, times(1)).onContentShown(false)
224     }
225 
226     @Test
227     fun expandCollapsedNotification_expandedWrapperShown() {
228         // GIVEN the shade is open
229         fakeParent.visibility = View.VISIBLE
230         // AND a collapsed content is created
231         val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
232 
233         // WHEN we collapse the notification
234         whenever(row.intrinsicHeight).thenReturn(expandedHeight)
235         view.contentHeight = expandedHeight
236 
237         // THEN the wrappers are updated
238         assertEquals(view.expandedWrapper, view.visibleWrapper)
239         verify(view.contractedWrapper, times(1)).onContentShown(false)
240         verify(view.contractedWrapper).setVisible(false)
241         verify(view.expandedWrapper, times(1)).onContentShown(true)
242         verify(view.expandedWrapper).setVisible(true)
243     }
244 
245     @Test
246     fun collapseExpandedNotification_expandedWrapperShown() {
247         // GIVEN the shade is open
248         fakeParent.visibility = View.VISIBLE
249         // AND a system-expanded content is created
250         val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
251 
252         // WHEN we collapse the notification
253         whenever(row.intrinsicHeight).thenReturn(contractedHeight)
254         view.contentHeight = contractedHeight
255 
256         // THEN the wrappers are updated
257         assertEquals(view.contractedWrapper, view.visibleWrapper)
258         verify(view.expandedWrapper, times(1)).onContentShown(false)
259         verify(view.expandedWrapper).setVisible(false)
260         verify(view.contractedWrapper, times(1)).onContentShown(true)
261         verify(view.contractedWrapper).setVisible(true)
262     }
263 
264     @Test
265     fun testSetFeedbackIcon() {
266         // Given: contractedChild, enpandedChild, and headsUpChild being set
267         val view = createContentView(isSystemExpanded = false)
268 
269         // When: FeedBackIcon is set
270         val icon =
271             FeedbackIcon(
272                 R.drawable.ic_feedback_alerted,
273                 R.string.notification_feedback_indicator_alerted
274             )
275         view.setFeedbackIcon(icon)
276 
277         // Then: contractedChild, enpandedChild, and headsUpChild is updated with the feedbackIcon
278         verify(view.contractedWrapper).setFeedbackIcon(icon)
279         verify(view.expandedWrapper).setFeedbackIcon(icon)
280         verify(view.headsUpWrapper).setFeedbackIcon(icon)
281     }
282 
283     @Test
284     fun testExpandButtonFocusIsCalled() {
285         val mockContractedEB = mock<NotificationExpandButton>()
286         val mockContracted = createMockNotificationHeaderView(contractedHeight, mockContractedEB)
287 
288         val mockExpandedEB = mock<NotificationExpandButton>()
289         val mockExpanded = createMockNotificationHeaderView(expandedHeight, mockExpandedEB)
290 
291         val mockHeadsUpEB = mock<NotificationExpandButton>()
292         val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB)
293 
294         val view =
295             createContentView(
296                 isSystemExpanded = false,
297             )
298 
299         // Update all 3 child forms
300         view.apply {
301             contractedChild = mockContracted
302             expandedChild = mockExpanded
303             headsUpChild = mockHeadsUp
304 
305             expandedWrapper = spy(expandedWrapper)
306         }
307 
308         // This is required to call requestAccessibilityFocus()
309         view.setFocusOnVisibilityChange()
310 
311         // The following will initialize the view and switch from not visible to expanded.
312         // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
313         view.setHeadsUp(true)
314         assertEquals(view.expandedWrapper, view.visibleWrapper)
315         verify(mockContractedEB, never()).requestAccessibilityFocus()
316         verify(mockExpandedEB).requestAccessibilityFocus()
317         verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
318     }
319 
320     private fun createMockNotificationHeaderView(
321         height: Int,
322         mockExpandedEB: NotificationExpandButton
323     ) =
324         spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height })
325             .apply {
326                 whenever(this.animate()).thenReturn(mock())
327                 whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
328             }
329 
330     @Test
331     fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
332         val mockContracted = spy(createViewWithHeight(contractedHeight))
333 
334         val mockExpandedActions = mock<NotificationActionListLayout>()
335         val mockExpanded = spy(createViewWithHeight(expandedHeight))
336         whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
337 
338         val mockHeadsUpActions = mock<NotificationActionListLayout>()
339         val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
340         whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
341 
342         val view =
343             createContentView(
344                 isSystemExpanded = false,
345                 contractedView = mockContracted,
346                 expandedView = mockExpanded,
347                 headsUpView = mockHeadsUp
348             )
349 
350         view.setRemoteInputVisible(true)
351 
352         verify(mockContracted, never()).findViewById<View>(0)
353         verify(mockExpandedActions).importantForAccessibility =
354             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
355         verify(mockHeadsUpActions).importantForAccessibility =
356             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
357     }
358 
359     @Test
360     fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
361         val mockContracted = spy(createViewWithHeight(contractedHeight))
362 
363         val mockExpandedActions = mock<NotificationActionListLayout>()
364         val mockExpanded = spy(createViewWithHeight(expandedHeight))
365         whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
366 
367         val mockHeadsUpActions = mock<NotificationActionListLayout>()
368         val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
369         whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
370 
371         val view =
372             createContentView(
373                 isSystemExpanded = false,
374                 contractedView = mockContracted,
375                 expandedView = mockExpanded,
376                 headsUpView = mockHeadsUp
377             )
378 
379         view.setRemoteInputVisible(false)
380 
381         verify(mockContracted, never()).findViewById<View>(0)
382         verify(mockExpandedActions).importantForAccessibility =
383             View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
384         verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
385     }
386 
387     @Test
388     fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
389         // Given: bottom margin of actionListMarginTarget is notificationContentMargin
390         // Bubble button should not be shown for the given NotificationEntry
391         val mockNotificationEntry = createMockNotificationEntry()
392         val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
393         val actionListMarginTarget =
394             spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
395         val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
396         whenever(
397                 mockExpandedChild.findViewById<LinearLayout>(
398                     R.id.notification_action_list_margin_target
399                 )
400             )
401             .thenReturn(actionListMarginTarget)
402         val view = createContentView(isSystemExpanded = false)
403 
404         view.setContainingNotification(mockContainingNotification) // maybe not needed
405 
406         // When: call NotificationContentView.setExpandedChild() to set the expandedChild
407         view.expandedChild = mockExpandedChild
408 
409         // Then: bottom margin of actionListMarginTarget should not change,
410         // still be notificationContentMargin
411         assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
412     }
413 
414     @Test
415     fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
416         // Given: bottom margin of actionListMarginTarget is notificationContentMargin
417         // Bubble button should be shown for the given NotificationEntry
418         val mockNotificationEntry = createMockNotificationEntry()
419         val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
420         val actionListMarginTarget =
421             spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
422         val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
423         whenever(
424                 mockExpandedChild.findViewById<LinearLayout>(
425                     R.id.notification_action_list_margin_target
426                 )
427             )
428             .thenReturn(actionListMarginTarget)
429         val view = createContentView(isSystemExpanded = false)
430 
431         view.setContainingNotification(mockContainingNotification)
432 
433         // Given: controller says bubbles are enabled for the user
434         view.setBubblesEnabledForUser(true)
435 
436         // When: call NotificationContentView.setExpandedChild() to set the expandedChild
437         view.expandedChild = mockExpandedChild
438 
439         // Then: bottom margin of actionListMarginTarget should be set to 0
440         assertEquals(0, getMarginBottom(actionListMarginTarget))
441     }
442 
443     @Test
444     fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
445         // Given: bottom margin of actionListMarginTarget is notificationContentMargin
446         val mockNotificationEntry = createMockNotificationEntry()
447         val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
448         val actionListMarginTarget =
449             spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
450         val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
451         whenever(
452                 mockExpandedChild.findViewById<LinearLayout>(
453                     R.id.notification_action_list_margin_target
454                 )
455             )
456             .thenReturn(actionListMarginTarget)
457         val view = createContentView(isSystemExpanded = false)
458 
459         view.setContainingNotification(mockContainingNotification)
460         view.expandedChild = mockExpandedChild
461         assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
462 
463         // When: call NotificationContentView.onNotificationUpdated() to update the
464         // NotificationEntry, which should not show bubble button
465         view.onNotificationUpdated(createMockNotificationEntry())
466 
467         // Then: bottom margin of actionListMarginTarget should not change, still be 20
468         assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
469     }
470 
471     @Test
472     fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
473         // Given: bottom margin of actionListMarginTarget is notificationContentMargin
474         val mockNotificationEntry = createMockNotificationEntry()
475         val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
476         val actionListMarginTarget =
477             spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
478         val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
479         whenever(
480                 mockExpandedChild.findViewById<LinearLayout>(
481                     R.id.notification_action_list_margin_target
482                 )
483             )
484             .thenReturn(actionListMarginTarget)
485         val view = createContentView(isSystemExpanded = false, expandedView = mockExpandedChild)
486 
487         view.setContainingNotification(mockContainingNotification)
488         assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
489 
490         // When: call NotificationContentView.onNotificationUpdated() to update the
491         // NotificationEntry, which should show bubble button
492         view.onNotificationUpdated(createMockNotificationEntry(/*true*/ ))
493 
494         // Then: no bubble yet
495         assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
496 
497         // Given: controller says bubbles are enabled for the user
498         view.setBubblesEnabledForUser(true)
499 
500         // Then: bottom margin of actionListMarginTarget should not change, still be 20
501         assertEquals(0, getMarginBottom(actionListMarginTarget))
502     }
503 
504     @Test
505     fun onSetAnimationRunning() {
506         // Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
507         val view = createContentView(isSystemExpanded = false)
508 
509         // When: we set content animation running.
510         assertTrue(view.setContentAnimationRunning(true))
511 
512         // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
513         // called on them.
514         verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
515         verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
516         verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
517 
518         // When: we set content animation running true _again_.
519         assertFalse(view.setContentAnimationRunning(true))
520 
521         // Then: the children should not have setAnimationRunning called on them again.
522         // Verify counts number of calls so far on the object, so these still register as 1.
523         verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
524         verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
525         verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
526     }
527 
528     @Test
529     fun onSetAnimationStopped() {
530         // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
531         val view = createContentView(isSystemExpanded = false)
532 
533         // When: we set content animation running.
534         assertTrue(view.setContentAnimationRunning(true))
535 
536         // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
537         // called on them.
538         verify(view.contractedWrapper).setAnimationsRunning(true)
539         verify(view.expandedWrapper).setAnimationsRunning(true)
540         verify(view.headsUpWrapper).setAnimationsRunning(true)
541 
542         // When: we set content animation running false, the state changes, so the function
543         // returns true.
544         assertTrue(view.setContentAnimationRunning(false))
545 
546         // Then: the children have their animations stopped.
547         verify(view.contractedWrapper).setAnimationsRunning(false)
548         verify(view.expandedWrapper).setAnimationsRunning(false)
549         verify(view.headsUpWrapper).setAnimationsRunning(false)
550     }
551 
552     @Test
553     fun onSetAnimationInitStopped() {
554         // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
555         val view = createContentView(isSystemExpanded = false)
556 
557         // When: we try to stop the animations before they've been started.
558         assertFalse(view.setContentAnimationRunning(false))
559 
560         // Then: the children should not have setAnimationRunning called on them again.
561         verify(view.contractedWrapper, never()).setAnimationsRunning(false)
562         verify(view.expandedWrapper, never()).setAnimationsRunning(false)
563         verify(view.headsUpWrapper, never()).setAnimationsRunning(false)
564     }
565 
566     @Test
567     fun notifySubtreeAccessibilityStateChanged_notifiesParent() {
568         // Given: a contentView is created
569         val view = createContentView()
570         clearInvocations(fakeParent)
571 
572         // When: the contentView is notified for an A11y change
573         view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
574 
575         // Then: the contentView propagates the event to its parent
576         verify(fakeParent).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
577     }
578 
579     @Test
580     fun notifySubtreeAccessibilityStateChanged_animatingContentView_dontNotifyParent() {
581         // Given: a collapsed contentView is created
582         val view = createContentView()
583         clearInvocations(fakeParent)
584 
585         // And: it is animating to expanded
586         view.setAnimationStartVisibleType(NotificationContentView.VISIBLE_TYPE_EXPANDED)
587 
588         // When: the contentView is notified for an A11y change
589         view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
590 
591         // Then: the contentView DOESN'T propagates the event to its parent
592         verify(fakeParent, never()).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
593     }
594 
595     private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
596         mock<ExpandableNotificationRow>().apply {
597             whenever(this.entry).thenReturn(notificationEntry)
598             whenever(this.context).thenReturn(mContext)
599             whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
600         }
601 
602     private fun createMockNotificationEntry() =
603         mock<NotificationEntry>().apply {
604             whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
605                 .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
606             whenever(this.bubbleMetadata).thenReturn(mock())
607             val sbnMock: StatusBarNotification = mock()
608             val userMock: UserHandle = mock()
609             whenever(this.sbn).thenReturn(sbnMock)
610             whenever(sbnMock.user).thenReturn(userMock)
611         }
612 
613     private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
614         val outerLayout = LinearLayout(mContext)
615         val innerLayout = LinearLayout(mContext)
616         outerLayout.addView(innerLayout)
617         val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams
618         mlp.setMargins(0, 0, 0, bottomMargin)
619         return innerLayout
620     }
621 
622     private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
623         spy(createViewWithHeight(expandedHeight)).apply {
624             whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
625             whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
626             whenever(this.context).thenReturn(mContext)
627 
628             val resourcesMock: Resources = mock()
629             whenever(resourcesMock.configuration).thenReturn(mock())
630             whenever(this.resources).thenReturn(resourcesMock)
631         }
632 
633     private fun createContentView(
634         isSystemExpanded: Boolean = false,
635         contractedView: View = createViewWithHeight(contractedHeight),
636         expandedView: View = createViewWithHeight(expandedHeight),
637         headsUpView: View = createViewWithHeight(contractedHeight),
638         row: ExpandableNotificationRow = this.row
639     ): NotificationContentView {
640         val height = if (isSystemExpanded) expandedHeight else contractedHeight
641         doReturn(height).whenever(row).intrinsicHeight
642 
643         return spy(NotificationContentView(mContext, /* attrs= */ null))
644             .apply {
645                 initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock(), mock())
646                 setContainingNotification(row)
647                 setHeights(
648                     /* smallHeight= */ contractedHeight,
649                     /* headsUpMaxHeight= */ contractedHeight,
650                     /* maxHeight= */ expandedHeight
651                 )
652                 contractedChild = contractedView
653                 expandedChild = expandedView
654                 headsUpChild = headsUpView
655                 contractedWrapper = spy(contractedWrapper)
656                 expandedWrapper = spy(expandedWrapper)
657                 headsUpWrapper = spy(headsUpWrapper)
658 
659                 if (isSystemExpanded) {
660                     contentHeight = expandedHeight
661                 }
662             }
663             .also { contentView ->
664                 fakeParent.addView(contentView)
665                 contentView.mockRequestLayout()
666             }
667     }
668 
669     private fun createViewWithHeight(height: Int) =
670         View(mContext, /* attrs= */ null).apply { minimumHeight = height }
671 
672     private fun getMarginBottom(layout: LinearLayout): Int =
673         (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
674 
675     private fun px(@DimenRes id: Int): Int = testableResources.resources.getDimensionPixelSize(id)
676 }
677 
mockRequestLayoutnull678 private fun NotificationContentView.mockRequestLayout() {
679     measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
680     layout(0, 0, measuredWidth, measuredHeight)
681 }
682 
clearInvocationsnull683 private fun NotificationContentView.clearInvocations() {
684     clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
685 }
686