1 /* 2 * Copyright (C) 2021 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.charging 18 19 import android.graphics.Rect 20 import android.view.Surface 21 import android.view.View 22 import android.view.WindowManager 23 import android.view.WindowMetrics 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SmallTest 26 import com.android.app.viewcapture.ViewCapture 27 import com.android.app.viewcapture.ViewCaptureAwareWindowManager 28 import com.android.internal.logging.UiEventLogger 29 import com.android.systemui.SysuiTestCase 30 import com.android.systemui.flags.FeatureFlags 31 import com.android.systemui.flags.Flags 32 import com.android.systemui.res.R 33 import com.android.systemui.statusbar.commandline.CommandRegistry 34 import com.android.systemui.statusbar.policy.BatteryController 35 import com.android.systemui.statusbar.policy.ConfigurationController 36 import com.android.systemui.surfaceeffects.ripple.RippleView 37 import com.android.systemui.util.mockito.whenever 38 import com.android.systemui.util.time.FakeSystemClock 39 import org.junit.Before 40 import org.junit.Test 41 import org.junit.runner.RunWith 42 import org.mockito.ArgumentCaptor 43 import org.mockito.ArgumentMatchers 44 import org.mockito.Mock 45 import org.mockito.Mockito.any 46 import org.mockito.Mockito.eq 47 import org.mockito.Mockito.never 48 import org.mockito.Mockito.reset 49 import org.mockito.Mockito.verify 50 import org.mockito.Mockito.`when` 51 import org.mockito.MockitoAnnotations 52 53 @SmallTest 54 @RunWith(AndroidJUnit4::class) 55 class WiredChargingRippleControllerTest : SysuiTestCase() { 56 private lateinit var controller: WiredChargingRippleController 57 @Mock private lateinit var commandRegistry: CommandRegistry 58 @Mock private lateinit var batteryController: BatteryController 59 @Mock private lateinit var featureFlags: FeatureFlags 60 @Mock private lateinit var configurationController: ConfigurationController 61 @Mock private lateinit var rippleView: RippleView 62 @Mock private lateinit var windowManager: WindowManager 63 @Mock private lateinit var uiEventLogger: UiEventLogger 64 @Mock private lateinit var windowMetrics: WindowMetrics 65 @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> 66 private val systemClock = FakeSystemClock() 67 68 @Before setUpnull69 fun setUp() { 70 MockitoAnnotations.initMocks(this) 71 `when`(featureFlags.isEnabled(Flags.CHARGING_RIPPLE)).thenReturn(true) 72 controller = WiredChargingRippleController( 73 commandRegistry, batteryController, configurationController, 74 featureFlags, context, windowManager, 75 ViewCaptureAwareWindowManager(windowManager, 76 lazyViewCapture, isViewCaptureEnabled = false), systemClock, uiEventLogger) 77 rippleView.setupShader() 78 controller.rippleView = rippleView // Replace the real ripple view with a mock instance 79 controller.registerCallbacks() 80 81 `when`(windowMetrics.bounds).thenReturn(Rect(0, 0, 100, 100)) 82 `when`(windowManager.currentWindowMetrics).thenReturn(windowMetrics) 83 } 84 85 @Test testTriggerRipple_UnlockedStatenull86 fun testTriggerRipple_UnlockedState() { 87 val captor = ArgumentCaptor 88 .forClass(BatteryController.BatteryStateChangeCallback::class.java) 89 verify(batteryController).addCallback(captor.capture()) 90 91 // Verify ripple added to window manager. 92 captor.value.onBatteryLevelChanged( 93 /* unusedBatteryLevel= */ 0, 94 /* plugged in= */ true, 95 /* charging= */ false) 96 val attachListenerCaptor = 97 ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) 98 verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture()) 99 verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>()) 100 101 // Verify ripple started 102 val runnableCaptor = 103 ArgumentCaptor.forClass(Runnable::class.java) 104 attachListenerCaptor.value.onViewAttachedToWindow(rippleView) 105 verify(rippleView).startRipple(runnableCaptor.capture()) 106 107 // Verify ripple removed 108 runnableCaptor.value.run() 109 verify(windowManager).removeView(rippleView) 110 111 // Verify event logged 112 verify(uiEventLogger).log( 113 WiredChargingRippleController.WiredChargingRippleEvent.CHARGING_RIPPLE_PLAYED) 114 } 115 116 @Test testUpdateRippleColornull117 fun testUpdateRippleColor() { 118 val captor = ArgumentCaptor 119 .forClass(ConfigurationController.ConfigurationListener::class.java) 120 verify(configurationController).addCallback(captor.capture()) 121 122 reset(rippleView) 123 captor.value.onThemeChanged() 124 verify(rippleView).setColor(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) 125 126 reset(rippleView) 127 captor.value.onUiModeChanged() 128 verify(rippleView).setColor(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) 129 } 130 131 @Test testDebounceRipplenull132 fun testDebounceRipple() { 133 var time: Long = 0 134 systemClock.setElapsedRealtime(time) 135 136 controller.startRippleWithDebounce() 137 verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) 138 139 reset(rippleView) 140 // Wait a short while and trigger. 141 time += 100 142 systemClock.setElapsedRealtime(time) 143 controller.startRippleWithDebounce() 144 145 // Verify the ripple is debounced. 146 verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) 147 148 // Trigger many times. 149 for (i in 0..100) { 150 time += 100 151 systemClock.setElapsedRealtime(time) 152 controller.startRippleWithDebounce() 153 } 154 // Verify all attempts are debounced. 155 verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) 156 157 // Wait a long while and trigger. 158 systemClock.setElapsedRealtime(time + 500000) 159 controller.startRippleWithDebounce() 160 // Verify that ripple is triggered. 161 verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) 162 } 163 164 @Test testRipple_whenDocked_doesNotPlayRipplenull165 fun testRipple_whenDocked_doesNotPlayRipple() { 166 `when`(batteryController.isChargingSourceDock).thenReturn(true) 167 val captor = ArgumentCaptor 168 .forClass(BatteryController.BatteryStateChangeCallback::class.java) 169 verify(batteryController).addCallback(captor.capture()) 170 171 captor.value.onBatteryLevelChanged( 172 /* unusedBatteryLevel= */ 0, 173 /* plugged in= */ true, 174 /* charging= */ false) 175 176 val attachListenerCaptor = 177 ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) 178 verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture()) 179 verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>()) 180 } 181 182 @Test testRipple_layoutsCorrectlynull183 fun testRipple_layoutsCorrectly() { 184 // Sets the correct ripple size. 185 val width = 100 186 val height = 200 187 whenever(windowMetrics.bounds).thenReturn(Rect(0, 0, width, height)) 188 189 // Trigger ripple. 190 val captor = ArgumentCaptor 191 .forClass(BatteryController.BatteryStateChangeCallback::class.java) 192 verify(batteryController).addCallback(captor.capture()) 193 194 captor.value.onBatteryLevelChanged( 195 /* unusedBatteryLevel= */ 0, 196 /* plugged in= */ true, 197 /* charging= */ false) 198 199 val attachListenerCaptor = 200 ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) 201 verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture()) 202 verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>()) 203 204 val runnableCaptor = 205 ArgumentCaptor.forClass(Runnable::class.java) 206 attachListenerCaptor.value.onViewAttachedToWindow(rippleView) 207 verify(rippleView).startRipple(runnableCaptor.capture()) 208 209 // Verify size and center position. 210 val maxSize = 400f // Double the max value between width and height. 211 verify(rippleView).setMaxSize(maxWidth = maxSize, maxHeight = maxSize) 212 213 val normalizedPortPosX = 214 context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_x) 215 val normalizedPortPosY = 216 context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y) 217 val expectedCenterX: Float 218 val expectedCenterY: Float 219 when (checkNotNull(context.display).rotation) { 220 Surface.ROTATION_90 -> { 221 expectedCenterX = width * normalizedPortPosY 222 expectedCenterY = height * (1 - normalizedPortPosX) 223 } 224 Surface.ROTATION_180 -> { 225 expectedCenterX = width * (1 - normalizedPortPosX) 226 expectedCenterY = height * (1 - normalizedPortPosY) 227 } 228 Surface.ROTATION_270 -> { 229 expectedCenterX = width * (1 - normalizedPortPosY) 230 expectedCenterY = height * normalizedPortPosX 231 } 232 else -> { // Surface.ROTATION_0 233 expectedCenterX = width * normalizedPortPosX 234 expectedCenterY = height * normalizedPortPosY 235 } 236 } 237 238 verify(rippleView).setCenter(expectedCenterX, expectedCenterY) 239 } 240 } 241