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