1 /*
2  * 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.media.controls.ui
18 
19 import android.content.res.Configuration
20 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
21 import android.testing.AndroidTestingRunner
22 import android.testing.TestableLooper
23 import android.view.View
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.R
26 import com.android.systemui.SysuiTestCase
27 import com.android.systemui.media.controls.models.player.MediaViewHolder
28 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
29 import com.android.systemui.media.controls.util.MediaFlags
30 import com.android.systemui.util.animation.MeasurementInput
31 import com.android.systemui.util.animation.TransitionLayout
32 import com.android.systemui.util.animation.TransitionViewState
33 import com.android.systemui.util.animation.WidgetState
34 import junit.framework.Assert.assertTrue
35 import org.junit.Before
36 import org.junit.Test
37 import org.junit.runner.RunWith
38 import org.mockito.ArgumentMatchers.floatThat
39 import org.mockito.Mock
40 import org.mockito.Mockito.times
41 import org.mockito.Mockito.verify
42 import org.mockito.Mockito.`when` as whenever
43 import org.mockito.MockitoAnnotations
44 
45 @SmallTest
46 @TestableLooper.RunWithLooper(setAsMainLooper = true)
47 @RunWith(AndroidTestingRunner::class)
48 class MediaViewControllerTest : SysuiTestCase() {
49     private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
50     private val mediaHostStatesManager = MediaHostStatesManager()
51     private val configurationController =
52         com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
53     private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
54     private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
55     @Mock lateinit var logger: MediaViewLogger
56     @Mock private lateinit var mockViewState: TransitionViewState
57     @Mock private lateinit var mockCopiedState: TransitionViewState
58     @Mock private lateinit var detailWidgetState: WidgetState
59     @Mock private lateinit var controlWidgetState: WidgetState
60     @Mock private lateinit var mediaTitleWidgetState: WidgetState
61     @Mock private lateinit var mediaSubTitleWidgetState: WidgetState
62     @Mock private lateinit var mediaContainerWidgetState: WidgetState
63     @Mock private lateinit var mediaFlags: MediaFlags
64 
65     private val delta = 0.1F
66 
67     private lateinit var mediaViewController: MediaViewController
68 
69     @Before
70     fun setup() {
71         MockitoAnnotations.initMocks(this)
72         mediaViewController =
73             MediaViewController(
74                 context,
75                 configurationController,
76                 mediaHostStatesManager,
77                 logger,
78                 mediaFlags,
79             )
80     }
81 
82     @Test
83     fun testOrientationChanged_heightOfPlayerIsUpdated() {
84         val newConfig = Configuration()
85 
86         mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
87         // Change the height to see the effect of orientation change.
88         MediaViewHolder.backgroundIds.forEach { id ->
89             mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 10
90         }
91         newConfig.orientation = ORIENTATION_LANDSCAPE
92         configurationController.onConfigurationChanged(newConfig)
93 
94         MediaViewHolder.backgroundIds.forEach { id ->
95             assertTrue(
96                 mediaViewController.expandedLayout.getConstraint(id).layout.mHeight ==
97                     context.resources.getDimensionPixelSize(
98                         R.dimen.qs_media_session_height_expanded
99                     )
100             )
101         }
102     }
103 
104     @Test
105     fun testOrientationChanged_heightOfRecCardIsUpdated() {
106         val newConfig = Configuration()
107 
108         mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
109         // Change the height to see the effect of orientation change.
110         mediaViewController.expandedLayout
111             .getConstraint(RecommendationViewHolder.backgroundId)
112             .layout
113             .mHeight = 10
114         newConfig.orientation = ORIENTATION_LANDSCAPE
115         configurationController.onConfigurationChanged(newConfig)
116 
117         assertTrue(
118             mediaViewController.expandedLayout
119                 .getConstraint(RecommendationViewHolder.backgroundId)
120                 .layout
121                 .mHeight ==
122                 context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
123         )
124     }
125 
126     @Test
127     fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
128         mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
129         player.measureState =
130             TransitionViewState().apply {
131                 this.height = 100
132                 this.measureHeight = 100
133             }
134         mediaHostStateHolder.expansion = 1f
135         val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
136         val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
137         mediaHostStateHolder.measurementInput =
138             MeasurementInput(widthMeasureSpec, heightMeasureSpec)
139 
140         // Test no squish
141         mediaHostStateHolder.squishFraction = 1f
142         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
143         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100)
144 
145         // Test half squish
146         mediaHostStateHolder.squishFraction = 0.5f
147         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
148         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100)
149     }
150 
151     @Test
152     fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
153         mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
154         recommendation.measureState = TransitionViewState().apply { this.height = 100 }
155         mediaHostStateHolder.expansion = 1f
156         val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
157         val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
158         mediaHostStateHolder.measurementInput =
159             MeasurementInput(widthMeasureSpec, heightMeasureSpec)
160 
161         // Test no squish
162         mediaHostStateHolder.squishFraction = 1f
163         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
164         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100)
165 
166         // Test half squish
167         mediaHostStateHolder.squishFraction = 0.5f
168         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
169         assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100)
170     }
171 
172     @Test
173     fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
174         whenever(mockViewState.copy()).thenReturn(mockCopiedState)
175         whenever(mockCopiedState.widgetStates)
176             .thenReturn(
177                 mutableMapOf(
178                     R.id.media_progress_bar to controlWidgetState,
179                     R.id.header_artist to detailWidgetState
180                 )
181             )
182         whenever(mockCopiedState.measureHeight).thenReturn(200)
183         // detail widgets occupy [90, 100]
184         whenever(detailWidgetState.y).thenReturn(90F)
185         whenever(detailWidgetState.height).thenReturn(10)
186         // control widgets occupy [150, 170]
187         whenever(controlWidgetState.y).thenReturn(150F)
188         whenever(controlWidgetState.height).thenReturn(20)
189         // in current beizer, when the progress reach 0.38, the result will be 0.5
190         mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
191         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
192         verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
193         mediaViewController.squishViewState(mockViewState, 200F / 200F)
194         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
195         verify(detailWidgetState, times(2)).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
196     }
197 
198     @Test
199     fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
200         whenever(mockViewState.copy()).thenReturn(mockCopiedState)
201         whenever(mockCopiedState.widgetStates)
202             .thenReturn(
203                 mutableMapOf(
204                     R.id.media_title to mediaTitleWidgetState,
205                     R.id.media_subtitle to mediaSubTitleWidgetState,
206                     R.id.media_cover1_container to mediaContainerWidgetState
207                 )
208             )
209         whenever(mockCopiedState.measureHeight).thenReturn(360)
210         // media container widgets occupy [20, 300]
211         whenever(mediaContainerWidgetState.y).thenReturn(20F)
212         whenever(mediaContainerWidgetState.height).thenReturn(280)
213         // media title widgets occupy [320, 330]
214         whenever(mediaTitleWidgetState.y).thenReturn(320F)
215         whenever(mediaTitleWidgetState.height).thenReturn(10)
216         // media subtitle widgets occupy [340, 350]
217         whenever(mediaSubTitleWidgetState.y).thenReturn(340F)
218         whenever(mediaSubTitleWidgetState.height).thenReturn(10)
219 
220         // in current beizer, when the progress reach 0.38, the result will be 0.5
221         mediaViewController.squishViewState(mockViewState, 307.6F / 360F)
222         verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
223         mediaViewController.squishViewState(mockViewState, 320F / 360F)
224         verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
225         // media title and media subtitle are in same widget group, should be calculate together and
226         // have same alpha
227         mediaViewController.squishViewState(mockViewState, 353.8F / 360F)
228         verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
229         verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
230         mediaViewController.squishViewState(mockViewState, 360F / 360F)
231         verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
232         verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
233     }
234 }
235