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.AndroidTestingRunner 25 import android.testing.TestableLooper 26 import android.testing.TestableLooper.RunWithLooper 27 import android.testing.ViewUtils 28 import android.view.MotionEvent 29 import android.view.View 30 import android.view.ViewGroupOverlay 31 import android.widget.LinearLayout 32 import androidx.annotation.ColorInt 33 import androidx.test.filters.SmallTest 34 import com.android.systemui.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(AndroidTestingRunner::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 69 fun setUp() { 70 looper = TestableLooper.get(this) 71 whenever(darkDispatcher.darkChangeFlow()).thenReturn(darkChange) 72 } 73 74 @Test 75 fun onHoverStarted_addsOverlay() { 76 view.setUpHoverListener() 77 78 view.hoverStarted() 79 80 assertThat(overlayDrawable).isNotNull() 81 } 82 83 @Test 84 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 94 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 104 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 115 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 126 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 136 private fun View.setUpHoverListener() { 137 setOnHoverListener(factory.createListener(view)) 138 attachView(view) 139 } 140 141 private fun View.setUpDarkAwareHoverListener() { 142 setOnHoverListener(factory.createDarkAwareListener(view)) 143 attachView(view) 144 } 145 146 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 161 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 166 private fun TestableStatusContainer.hoverStarted() { 167 injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_ENTER)) 168 } 169 170 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 177 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