| /* |
| * Copyright (C) 2022 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.row |
| |
| import android.content.res.Resources |
| import android.os.UserHandle |
| import android.service.notification.StatusBarNotification |
| import android.testing.AndroidTestingRunner |
| import android.view.NotificationHeaderView |
| import android.view.View |
| import android.view.ViewGroup |
| import android.widget.ImageView |
| import android.widget.LinearLayout |
| import androidx.test.filters.SmallTest |
| import com.android.internal.R |
| import com.android.internal.widget.NotificationActionListLayout |
| import com.android.internal.widget.NotificationExpandButton |
| import com.android.systemui.SysuiTestCase |
| import com.android.systemui.media.dialog.MediaOutputDialogFactory |
| import com.android.systemui.statusbar.notification.FeedbackIcon |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry |
| import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier |
| import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper |
| import com.android.systemui.util.mockito.mock |
| import com.android.systemui.util.mockito.whenever |
| import junit.framework.Assert.assertEquals |
| import junit.framework.Assert.assertFalse |
| import junit.framework.Assert.assertTrue |
| import org.junit.Before |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.mockito.Mock |
| import org.mockito.Mockito.doReturn |
| import org.mockito.Mockito.never |
| import org.mockito.Mockito.spy |
| import org.mockito.Mockito.times |
| import org.mockito.Mockito.verify |
| import org.mockito.MockitoAnnotations.initMocks |
| |
| @SmallTest |
| @RunWith(AndroidTestingRunner::class) |
| class NotificationContentViewTest : SysuiTestCase() { |
| private lateinit var view: NotificationContentView |
| |
| @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier |
| |
| private val notificationContentMargin = |
| mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin) |
| |
| @Before |
| fun setup() { |
| initMocks(this) |
| |
| mDependency.injectMockDependency(MediaOutputDialogFactory::class.java) |
| |
| view = spy(NotificationContentView(mContext, /* attrs= */ null)) |
| val row = ExpandableNotificationRow(mContext, /* attrs= */ null) |
| row.entry = createMockNotificationEntry(false) |
| val spyRow = spy(row) |
| doReturn(10).whenever(spyRow).intrinsicHeight |
| |
| with(view) { |
| initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock()) |
| setContainingNotification(spyRow) |
| setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30) |
| contractedChild = createViewWithHeight(10) |
| expandedChild = createViewWithHeight(20) |
| headsUpChild = createViewWithHeight(30) |
| measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) |
| layout(0, 0, view.measuredWidth, view.measuredHeight) |
| } |
| } |
| |
| private fun createViewWithHeight(height: Int) = |
| View(mContext, /* attrs= */ null).apply { minimumHeight = height } |
| |
| @Test |
| fun testSetFeedbackIcon() { |
| // Given: contractedChild, enpandedChild, and headsUpChild being set |
| val mockContracted = createMockNotificationHeaderView() |
| val mockExpanded = createMockNotificationHeaderView() |
| val mockHeadsUp = createMockNotificationHeaderView() |
| |
| with(view) { |
| contractedChild = mockContracted |
| expandedChild = mockExpanded |
| headsUpChild = mockHeadsUp |
| } |
| |
| // When: FeedBackIcon is set |
| view.setFeedbackIcon( |
| FeedbackIcon( |
| R.drawable.ic_feedback_alerted, |
| R.string.notification_feedback_indicator_alerted |
| ) |
| ) |
| |
| // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible |
| verify(mockContracted).visibility = View.VISIBLE |
| verify(mockExpanded).visibility = View.VISIBLE |
| verify(mockHeadsUp).visibility = View.VISIBLE |
| } |
| |
| private fun createMockNotificationHeaderView() = |
| mock<NotificationHeaderView>().apply { |
| whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this) |
| whenever(this.context).thenReturn(mContext) |
| } |
| |
| @Test |
| fun testExpandButtonFocusIsCalled() { |
| val mockContractedEB = mock<NotificationExpandButton>() |
| val mockContracted = createMockNotificationHeaderView(mockContractedEB) |
| |
| val mockExpandedEB = mock<NotificationExpandButton>() |
| val mockExpanded = createMockNotificationHeaderView(mockExpandedEB) |
| |
| val mockHeadsUpEB = mock<NotificationExpandButton>() |
| val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB) |
| |
| // Set up all 3 child forms |
| view.contractedChild = mockContracted |
| view.expandedChild = mockExpanded |
| view.headsUpChild = mockHeadsUp |
| |
| // This is required to call requestAccessibilityFocus() |
| view.setFocusOnVisibilityChange() |
| |
| // The following will initialize the view and switch from not visible to expanded. |
| // (heads-up is actually an alternate form of contracted, hence this enters expanded state) |
| view.setHeadsUp(true) |
| verify(mockContractedEB, never()).requestAccessibilityFocus() |
| verify(mockExpandedEB).requestAccessibilityFocus() |
| verify(mockHeadsUpEB, never()).requestAccessibilityFocus() |
| } |
| |
| private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) = |
| mock<NotificationHeaderView>().apply { |
| whenever(this.animate()).thenReturn(mock()) |
| whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) |
| whenever(this.context).thenReturn(mContext) |
| } |
| |
| @Test |
| fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() { |
| val mockContracted = mock<NotificationHeaderView>() |
| |
| val mockExpandedActions = mock<NotificationActionListLayout>() |
| val mockExpanded = mock<NotificationHeaderView>() |
| whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) |
| |
| val mockHeadsUpActions = mock<NotificationActionListLayout>() |
| val mockHeadsUp = mock<NotificationHeaderView>() |
| whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) |
| |
| with(view) { |
| contractedChild = mockContracted |
| expandedChild = mockExpanded |
| headsUpChild = mockHeadsUp |
| } |
| |
| view.setRemoteInputVisible(true) |
| |
| verify(mockContracted, never()).findViewById<View>(0) |
| verify(mockExpandedActions).importantForAccessibility = |
| View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS |
| verify(mockHeadsUpActions).importantForAccessibility = |
| View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS |
| } |
| |
| @Test |
| fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() { |
| val mockContracted = mock<NotificationHeaderView>() |
| |
| val mockExpandedActions = mock<NotificationActionListLayout>() |
| val mockExpanded = mock<NotificationHeaderView>() |
| whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) |
| |
| val mockHeadsUpActions = mock<NotificationActionListLayout>() |
| val mockHeadsUp = mock<NotificationHeaderView>() |
| whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) |
| |
| with(view) { |
| contractedChild = mockContracted |
| expandedChild = mockExpanded |
| headsUpChild = mockHeadsUp |
| } |
| |
| view.setRemoteInputVisible(false) |
| |
| verify(mockContracted, never()).findViewById<View>(0) |
| verify(mockExpandedActions).importantForAccessibility = |
| View.IMPORTANT_FOR_ACCESSIBILITY_AUTO |
| verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO |
| } |
| |
| @Test |
| fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { |
| // Given: bottom margin of actionListMarginTarget is notificationContentMargin |
| // Bubble button should not be shown for the given NotificationEntry |
| val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) |
| val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) |
| val actionListMarginTarget = |
| spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) |
| val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) |
| whenever( |
| mockExpandedChild.findViewById<LinearLayout>( |
| R.id.notification_action_list_margin_target |
| ) |
| ) |
| .thenReturn(actionListMarginTarget) |
| view.setContainingNotification(mockContainingNotification) |
| |
| // When: call NotificationContentView.setExpandedChild() to set the expandedChild |
| view.expandedChild = mockExpandedChild |
| |
| // Then: bottom margin of actionListMarginTarget should not change, |
| // still be notificationContentMargin |
| assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) |
| } |
| |
| @Test |
| fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { |
| // Given: bottom margin of actionListMarginTarget is notificationContentMargin |
| // Bubble button should be shown for the given NotificationEntry |
| val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true) |
| val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) |
| val actionListMarginTarget = |
| spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) |
| val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) |
| whenever( |
| mockExpandedChild.findViewById<LinearLayout>( |
| R.id.notification_action_list_margin_target |
| ) |
| ) |
| .thenReturn(actionListMarginTarget) |
| view.setContainingNotification(mockContainingNotification) |
| |
| // When: call NotificationContentView.setExpandedChild() to set the expandedChild |
| view.expandedChild = mockExpandedChild |
| |
| // Then: bottom margin of actionListMarginTarget should be set to 0 |
| assertEquals(0, getMarginBottom(actionListMarginTarget)) |
| } |
| |
| @Test |
| fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { |
| // Given: bottom margin of actionListMarginTarget is notificationContentMargin |
| val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) |
| val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) |
| val actionListMarginTarget = |
| spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) |
| val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) |
| whenever( |
| mockExpandedChild.findViewById<LinearLayout>( |
| R.id.notification_action_list_margin_target |
| ) |
| ) |
| .thenReturn(actionListMarginTarget) |
| view.setContainingNotification(mockContainingNotification) |
| view.expandedChild = mockExpandedChild |
| assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) |
| |
| // When: call NotificationContentView.onNotificationUpdated() to update the |
| // NotificationEntry, which should not show bubble button |
| view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false)) |
| |
| // Then: bottom margin of actionListMarginTarget should not change, still be 20 |
| assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) |
| } |
| |
| @Test |
| fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { |
| // Given: bottom margin of actionListMarginTarget is notificationContentMargin |
| val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) |
| val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) |
| val actionListMarginTarget = |
| spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) |
| val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) |
| whenever( |
| mockExpandedChild.findViewById<LinearLayout>( |
| R.id.notification_action_list_margin_target |
| ) |
| ) |
| .thenReturn(actionListMarginTarget) |
| view.setContainingNotification(mockContainingNotification) |
| view.expandedChild = mockExpandedChild |
| assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) |
| |
| // When: call NotificationContentView.onNotificationUpdated() to update the |
| // NotificationEntry, which should show bubble button |
| view.onNotificationUpdated(createMockNotificationEntry(true)) |
| |
| // Then: bottom margin of actionListMarginTarget should not change, still be 20 |
| assertEquals(0, getMarginBottom(actionListMarginTarget)) |
| } |
| |
| @Test |
| fun onSetAnimationRunning() { |
| // Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set |
| val mockContracted = mock<NotificationViewWrapper>() |
| val mockExpanded = mock<NotificationViewWrapper>() |
| val mockHeadsUp = mock<NotificationViewWrapper>() |
| |
| view.setContractedWrapper(mockContracted) |
| view.setExpandedWrapper(mockExpanded) |
| view.setHeadsUpWrapper(mockHeadsUp) |
| |
| // When: we set content animation running. |
| assertTrue(view.setContentAnimationRunning(true)) |
| |
| // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning |
| // called on them. |
| verify(mockContracted, times(1)).setAnimationsRunning(true) |
| verify(mockExpanded, times(1)).setAnimationsRunning(true) |
| verify(mockHeadsUp, times(1)).setAnimationsRunning(true) |
| |
| // When: we set content animation running true _again_. |
| assertFalse(view.setContentAnimationRunning(true)) |
| |
| // Then: the children should not have setAnimationRunning called on them again. |
| // Verify counts number of calls so far on the object, so these still register as 1. |
| verify(mockContracted, times(1)).setAnimationsRunning(true) |
| verify(mockExpanded, times(1)).setAnimationsRunning(true) |
| verify(mockHeadsUp, times(1)).setAnimationsRunning(true) |
| } |
| |
| @Test |
| fun onSetAnimationStopped() { |
| // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set |
| val mockContracted = mock<NotificationViewWrapper>() |
| val mockExpanded = mock<NotificationViewWrapper>() |
| val mockHeadsUp = mock<NotificationViewWrapper>() |
| |
| view.setContractedWrapper(mockContracted) |
| view.setExpandedWrapper(mockExpanded) |
| view.setHeadsUpWrapper(mockHeadsUp) |
| |
| // When: we set content animation running. |
| assertTrue(view.setContentAnimationRunning(true)) |
| |
| // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning |
| // called on them. |
| verify(mockContracted).setAnimationsRunning(true) |
| verify(mockExpanded).setAnimationsRunning(true) |
| verify(mockHeadsUp).setAnimationsRunning(true) |
| |
| // When: we set content animation running false, the state changes, so the function |
| // returns true. |
| assertTrue(view.setContentAnimationRunning(false)) |
| |
| // Then: the children have their animations stopped. |
| verify(mockContracted).setAnimationsRunning(false) |
| verify(mockExpanded).setAnimationsRunning(false) |
| verify(mockHeadsUp).setAnimationsRunning(false) |
| } |
| |
| @Test |
| fun onSetAnimationInitStopped() { |
| // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set |
| val mockContracted = mock<NotificationViewWrapper>() |
| val mockExpanded = mock<NotificationViewWrapper>() |
| val mockHeadsUp = mock<NotificationViewWrapper>() |
| |
| view.setContractedWrapper(mockContracted) |
| view.setExpandedWrapper(mockExpanded) |
| view.setHeadsUpWrapper(mockHeadsUp) |
| |
| // When: we try to stop the animations before they've been started. |
| assertFalse(view.setContentAnimationRunning(false)) |
| |
| // Then: the children should not have setAnimationRunning called on them again. |
| verify(mockContracted, never()).setAnimationsRunning(false) |
| verify(mockExpanded, never()).setAnimationsRunning(false) |
| verify(mockHeadsUp, never()).setAnimationsRunning(false) |
| } |
| |
| private fun createMockContainingNotification(notificationEntry: NotificationEntry) = |
| mock<ExpandableNotificationRow>().apply { |
| whenever(this.entry).thenReturn(notificationEntry) |
| whenever(this.context).thenReturn(mContext) |
| whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {}) |
| } |
| |
| private fun createMockNotificationEntry(showButton: Boolean) = |
| mock<NotificationEntry>().apply { |
| whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this)) |
| .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON) |
| whenever(this.bubbleMetadata).thenReturn(mock()) |
| val sbnMock: StatusBarNotification = mock() |
| val userMock: UserHandle = mock() |
| whenever(this.sbn).thenReturn(sbnMock) |
| whenever(sbnMock.user).thenReturn(userMock) |
| doReturn(showButton).whenever(view).shouldShowBubbleButton(this) |
| } |
| |
| private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { |
| val outerLayout = LinearLayout(mContext) |
| val innerLayout = LinearLayout(mContext) |
| outerLayout.addView(innerLayout) |
| val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams |
| mlp.setMargins(0, 0, 0, bottomMargin) |
| return innerLayout |
| } |
| |
| private fun createMockExpandedChild(notificationEntry: NotificationEntry) = |
| mock<ExpandableNotificationRow>().apply { |
| whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock()) |
| whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock()) |
| whenever(this.entry).thenReturn(notificationEntry) |
| whenever(this.context).thenReturn(mContext) |
| |
| val resourcesMock: Resources = mock() |
| whenever(resourcesMock.configuration).thenReturn(mock()) |
| whenever(this.resources).thenReturn(resourcesMock) |
| } |
| |
| private fun getMarginBottom(layout: LinearLayout): Int = |
| (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin |
| } |