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