1 /* 2 * Copyright (C) 2022 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.stylus 18 19 import android.app.ActivityManager 20 import android.app.Notification 21 import android.content.BroadcastReceiver 22 import android.content.Context 23 import android.content.Intent 24 import android.hardware.input.InputManager 25 import android.os.Bundle 26 import android.os.Handler 27 import android.testing.AndroidTestingRunner 28 import android.view.InputDevice 29 import androidx.core.app.NotificationManagerCompat 30 import androidx.test.filters.SmallTest 31 import com.android.internal.logging.InstanceId 32 import com.android.internal.logging.UiEventLogger 33 import com.android.systemui.InstanceIdSequenceFake 34 import com.android.systemui.R 35 import com.android.systemui.SysuiTestCase 36 import com.android.systemui.util.mockito.any 37 import com.android.systemui.util.mockito.argumentCaptor 38 import com.android.systemui.util.mockito.eq 39 import com.android.systemui.util.mockito.whenever 40 import com.google.common.truth.Truth.assertThat 41 import junit.framework.Assert.assertEquals 42 import org.junit.Before 43 import org.junit.Test 44 import org.junit.runner.RunWith 45 import org.mockito.ArgumentCaptor 46 import org.mockito.Captor 47 import org.mockito.Mock 48 import org.mockito.Mockito.clearInvocations 49 import org.mockito.Mockito.doNothing 50 import org.mockito.Mockito.inOrder 51 import org.mockito.Mockito.never 52 import org.mockito.Mockito.spy 53 import org.mockito.Mockito.times 54 import org.mockito.Mockito.verify 55 import org.mockito.Mockito.verifyNoMoreInteractions 56 import org.mockito.MockitoAnnotations 57 58 @RunWith(AndroidTestingRunner::class) 59 @SmallTest 60 class StylusUsiPowerUiTest : SysuiTestCase() { 61 @Mock lateinit var notificationManager: NotificationManagerCompat 62 63 @Mock lateinit var inputManager: InputManager 64 65 @Mock lateinit var handler: Handler 66 67 @Mock lateinit var btStylusDevice: InputDevice 68 69 @Mock lateinit var uiEventLogger: UiEventLogger 70 @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification> 71 72 private lateinit var stylusUsiPowerUi: StylusUsiPowerUI 73 private lateinit var broadcastReceiver: BroadcastReceiver 74 private lateinit var contextSpy: Context 75 76 private val instanceIdSequenceFake = InstanceIdSequenceFake(10) 77 78 private val uid = ActivityManager.getCurrentUser() 79 80 @Before 81 fun setUp() { 82 MockitoAnnotations.initMocks(this) 83 84 contextSpy = spy(mContext) 85 doNothing().whenever(contextSpy).startActivity(any()) 86 87 whenever(handler.post(any())).thenAnswer { 88 (it.arguments[0] as Runnable).run() 89 true 90 } 91 92 whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) 93 whenever(inputManager.getInputDevice(0)).thenReturn(btStylusDevice) 94 whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) 95 whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES") 96 97 stylusUsiPowerUi = 98 StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler, uiEventLogger) 99 stylusUsiPowerUi.instanceIdSequence = instanceIdSequenceFake 100 101 broadcastReceiver = stylusUsiPowerUi.receiver 102 } 103 104 @Test 105 fun updateBatteryState_capacityZero_noop() { 106 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f)) 107 108 verifyNoMoreInteractions(notificationManager) 109 } 110 111 @Test 112 fun updateBatteryState_capacityBelowThreshold_notifies() { 113 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 114 115 verify(notificationManager, times(1)) 116 .notify(eq(R.string.stylus_battery_low_percentage), any()) 117 verifyNoMoreInteractions(notificationManager) 118 } 119 120 @Test 121 fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { 122 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) 123 124 verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 125 verifyNoMoreInteractions(notificationManager) 126 } 127 128 @Test 129 fun updateBatteryState_capacitySame_inputDeviceChanges_updatesInputDeviceId() { 130 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 131 stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.1f)) 132 133 assertThat(stylusUsiPowerUi.inputDeviceId).isEqualTo(1) 134 verify(notificationManager, times(1)) 135 .notify(eq(R.string.stylus_battery_low_percentage), any()) 136 } 137 138 @Test 139 fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() { 140 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 141 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) 142 143 inOrder(notificationManager).let { 144 it.verify(notificationManager, times(1)) 145 .notify(eq(R.string.stylus_battery_low_percentage), any()) 146 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 147 it.verifyNoMoreInteractions() 148 } 149 } 150 151 @Test 152 fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() { 153 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 154 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f)) 155 156 verify(notificationManager, times(2)) 157 .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) 158 assertEquals( 159 notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), 160 context.getString(R.string.stylus_battery_low_percentage, "15%") 161 ) 162 assertEquals( 163 notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), 164 context.getString(R.string.stylus_battery_low_subtitle) 165 ) 166 verifyNoMoreInteractions(notificationManager) 167 } 168 169 @Test 170 fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() { 171 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 172 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f)) 173 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 174 175 inOrder(notificationManager).let { 176 it.verify(notificationManager, times(1)) 177 .notify(eq(R.string.stylus_battery_low_percentage), any()) 178 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 179 it.verify(notificationManager, times(1)) 180 .notify(eq(R.string.stylus_battery_low_percentage), any()) 181 it.verifyNoMoreInteractions() 182 } 183 } 184 185 @Test 186 fun updateSuppression_noExistingNotification_cancelsNotification() { 187 stylusUsiPowerUi.updateSuppression(true) 188 189 verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 190 verifyNoMoreInteractions(notificationManager) 191 } 192 193 @Test 194 fun updateSuppression_existingNotification_cancelsNotification() { 195 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 196 197 stylusUsiPowerUi.updateSuppression(true) 198 199 inOrder(notificationManager).let { 200 it.verify(notificationManager, times(1)) 201 .notify(eq(R.string.stylus_battery_low_percentage), any()) 202 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 203 it.verifyNoMoreInteractions() 204 } 205 } 206 207 @Test 208 fun refresh_hasConnectedBluetoothStylus_existingNotification_doesNothing() { 209 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 210 whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) 211 clearInvocations(notificationManager) 212 213 stylusUsiPowerUi.refresh() 214 215 verifyNoMoreInteractions(notificationManager) 216 } 217 218 @Test 219 fun updateBatteryState_showsNotification_logsNotificationShown() { 220 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 221 222 verify(uiEventLogger, times(1)) 223 .logWithInstanceIdAndPosition( 224 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_SHOWN, 225 uid, 226 contextSpy.packageName, 227 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 228 10 229 ) 230 } 231 232 @Test 233 fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() { 234 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 235 val activityIntentCaptor = argumentCaptor<Intent>() 236 stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f)) 237 broadcastReceiver.onReceive(contextSpy, intent) 238 239 verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture()) 240 assertThat(activityIntentCaptor.value.action) 241 .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS) 242 val args = 243 activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS) 244 as Bundle 245 assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1) 246 } 247 248 @Test 249 fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() { 250 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 251 broadcastReceiver.onReceive(contextSpy, intent) 252 253 verify(contextSpy, never()).startActivity(any()) 254 } 255 256 @Test 257 fun broadcastReceiver_clicked_logsNotificationClicked() { 258 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 259 broadcastReceiver.onReceive(contextSpy, intent) 260 261 verify(uiEventLogger, times(1)) 262 .logWithInstanceIdAndPosition( 263 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_CLICKED, 264 uid, 265 contextSpy.packageName, 266 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 267 100 268 ) 269 } 270 271 @Test 272 fun broadcastReceiver_dismissed_logsNotificationDismissed() { 273 val intent = Intent(StylusUsiPowerUI.ACTION_DISMISSED_LOW_BATTERY) 274 broadcastReceiver.onReceive(contextSpy, intent) 275 276 verify(uiEventLogger, times(1)) 277 .logWithInstanceIdAndPosition( 278 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_DISMISSED, 279 uid, 280 contextSpy.packageName, 281 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 282 100 283 ) 284 } 285 } 286