1 /*
2  * Copyright (C) 2023 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.phone
18 
19 import android.content.Context
20 import android.graphics.Color
21 import android.graphics.drawable.Drawable
22 import android.graphics.drawable.PaintDrawable
23 import android.os.SystemClock
24 import android.testing.TestableLooper
25 import android.testing.TestableLooper.RunWithLooper
26 import android.testing.ViewUtils
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.ViewGroupOverlay
30 import android.widget.LinearLayout
31 import androidx.annotation.ColorInt
32 import androidx.test.ext.junit.runners.AndroidJUnit4
33 import androidx.test.filters.SmallTest
34 import com.android.systemui.res.R
35 import com.android.systemui.SysuiTestCase
36 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
37 import com.android.systemui.statusbar.policy.FakeConfigurationController
38 import com.android.systemui.util.mockito.argumentCaptor
39 import com.android.systemui.util.mockito.mock
40 import com.android.systemui.util.mockito.whenever
41 import com.google.common.truth.Truth.assertThat
42 import kotlinx.coroutines.flow.MutableStateFlow
43 import org.junit.Before
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 import org.mockito.Mockito.verify
47 
48 @RunWith(AndroidJUnit4::class)
49 @RunWithLooper(setAsMainLooper = true)
50 @SmallTest
51 class StatusOverlayHoverListenerTest : SysuiTestCase() {
52 
53     private val viewOverlay = mock<ViewGroupOverlay>()
54     private val overlayCaptor = argumentCaptor<Drawable>()
55     private val darkDispatcher = mock<SysuiDarkIconDispatcher>()
56     private val darkChange: MutableStateFlow<DarkChange> = MutableStateFlow(DarkChange.EMPTY)
57 
58     private val factory =
59         StatusOverlayHoverListenerFactory(
60             context.resources,
61             FakeConfigurationController(),
62             darkDispatcher
63         )
64     private val view = TestableStatusContainer(context, viewOverlay)
65 
66     private lateinit var looper: TestableLooper
67 
68     @Before
setUpnull69     fun setUp() {
70         looper = TestableLooper.get(this)
71         whenever(darkDispatcher.darkChangeFlow()).thenReturn(darkChange)
72     }
73 
74     @Test
onHoverStarted_addsOverlaynull75     fun onHoverStarted_addsOverlay() {
76         view.setUpHoverListener()
77 
78         view.hoverStarted()
79 
80         assertThat(overlayDrawable).isNotNull()
81     }
82 
83     @Test
onHoverEnded_removesOverlaynull84     fun onHoverEnded_removesOverlay() {
85         view.setUpHoverListener()
86 
87         view.hoverStarted() // stopped callback will be called only if hover has started
88         view.hoverStopped()
89 
90         verify(viewOverlay).clear()
91     }
92 
93     @Test
onHoverStarted_overlayHasLightColornull94     fun onHoverStarted_overlayHasLightColor() {
95         view.setUpHoverListener()
96 
97         view.hoverStarted()
98 
99         assertThat(overlayColor)
100             .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
101     }
102 
103     @Test
onDarkAwareHoverStarted_withBlackIcons_overlayHasDarkColornull104     fun onDarkAwareHoverStarted_withBlackIcons_overlayHasDarkColor() {
105         view.setUpDarkAwareHoverListener()
106         setIconsTint(Color.BLACK)
107 
108         view.hoverStarted()
109 
110         assertThat(overlayColor)
111             .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_dark))
112     }
113 
114     @Test
onHoverStarted_withBlackIcons_overlayHasLightColornull115     fun onHoverStarted_withBlackIcons_overlayHasLightColor() {
116         view.setUpHoverListener()
117         setIconsTint(Color.BLACK)
118 
119         view.hoverStarted()
120 
121         assertThat(overlayColor)
122             .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
123     }
124 
125     @Test
onDarkAwareHoverStarted_withWhiteIcons_overlayHasLightColornull126     fun onDarkAwareHoverStarted_withWhiteIcons_overlayHasLightColor() {
127         view.setUpDarkAwareHoverListener()
128         setIconsTint(Color.WHITE)
129 
130         view.hoverStarted()
131 
132         assertThat(overlayColor)
133             .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
134     }
135 
Viewnull136     private fun View.setUpHoverListener() {
137         setOnHoverListener(factory.createListener(view))
138         attachView(view)
139     }
140 
setUpDarkAwareHoverListenernull141     private fun View.setUpDarkAwareHoverListener() {
142         setOnHoverListener(factory.createDarkAwareListener(view))
143         attachView(view)
144     }
145 
attachViewnull146     private fun attachView(view: View) {
147         ViewUtils.attachView(view)
148         // attaching is async so processAllMessages is required for view.repeatWhenAttached to run
149         looper.processAllMessages()
150     }
151 
152     private val overlayDrawable: Drawable
153         get() {
154             verify(viewOverlay).add(overlayCaptor.capture())
155             return overlayCaptor.value
156         }
157 
158     private val overlayColor
159         get() = (overlayDrawable as PaintDrawable).paint.color
160 
setIconsTintnull161     private fun setIconsTint(@ColorInt color: Int) {
162         // passing empty ArrayList is equivalent to just accepting passed color as icons color
163         darkChange.value = DarkChange(/* areas= */ ArrayList(), /* darkIntensity= */ 1f, color)
164     }
165 
TestableStatusContainernull166     private fun TestableStatusContainer.hoverStarted() {
167         injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_ENTER))
168     }
169 
TestableStatusContainernull170     private fun TestableStatusContainer.hoverStopped() {
171         injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_EXIT))
172     }
173 
174     class TestableStatusContainer(context: Context, private val mockOverlay: ViewGroupOverlay) :
175         LinearLayout(context) {
176 
injectHoverEventnull177         fun injectHoverEvent(event: MotionEvent) = dispatchHoverEvent(event)
178 
179         override fun getOverlay() = mockOverlay
180     }
181 
182     private fun hoverEvent(action: Int): MotionEvent {
183         return MotionEvent.obtain(
184             /* downTime= */ SystemClock.uptimeMillis(),
185             /* eventTime= */ SystemClock.uptimeMillis(),
186             /* action= */ action,
187             /* x= */ 0f,
188             /* y= */ 0f,
189             /* metaState= */ 0
190         )
191     }
192 }
193