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.SysuiTestCase 35 import com.android.systemui.res.R 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 setUpnull81 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 updateBatteryState_capacityZero_noopnull105 fun updateBatteryState_capacityZero_noop() { 106 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f)) 107 108 verifyNoMoreInteractions(notificationManager) 109 } 110 111 @Test updateBatteryState_capacityNaN_cancelsNotificationnull112 fun updateBatteryState_capacityNaN_cancelsNotification() { 113 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(Float.NaN)) 114 115 verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 116 verifyNoMoreInteractions(notificationManager) 117 } 118 119 @Test updateBatteryState_capacityBelowThreshold_notifiesnull120 fun updateBatteryState_capacityBelowThreshold_notifies() { 121 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 122 123 verify(notificationManager, times(1)) 124 .notify(eq(R.string.stylus_battery_low_percentage), any()) 125 verifyNoMoreInteractions(notificationManager) 126 } 127 128 @Test updateBatteryState_capacityAboveThreshold_cancelsNotificattionnull129 fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { 130 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) 131 132 verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 133 verifyNoMoreInteractions(notificationManager) 134 } 135 136 @Test updateBatteryState_capacitySame_inputDeviceChanges_updatesInputDeviceIdnull137 fun updateBatteryState_capacitySame_inputDeviceChanges_updatesInputDeviceId() { 138 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 139 stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.1f)) 140 141 assertThat(stylusUsiPowerUi.inputDeviceId).isEqualTo(1) 142 verify(notificationManager, times(1)) 143 .notify(eq(R.string.stylus_battery_low_percentage), any()) 144 } 145 146 @Test updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotificationnull147 fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() { 148 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 149 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) 150 151 inOrder(notificationManager).let { 152 it.verify(notificationManager, times(1)) 153 .notify(eq(R.string.stylus_battery_low_percentage), any()) 154 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 155 it.verifyNoMoreInteractions() 156 } 157 } 158 159 @Test updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotificationnull160 fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() { 161 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 162 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f)) 163 164 verify(notificationManager, times(2)) 165 .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) 166 assertEquals( 167 notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), 168 context.getString(R.string.stylus_battery_low_percentage, "15%") 169 ) 170 assertEquals( 171 notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), 172 context.getString(R.string.stylus_battery_low_subtitle) 173 ) 174 verifyNoMoreInteractions(notificationManager) 175 } 176 177 @Test updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotificationnull178 fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() { 179 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 180 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f)) 181 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 182 183 inOrder(notificationManager).let { 184 it.verify(notificationManager, times(1)) 185 .notify(eq(R.string.stylus_battery_low_percentage), any()) 186 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 187 it.verify(notificationManager, times(1)) 188 .notify(eq(R.string.stylus_battery_low_percentage), any()) 189 it.verifyNoMoreInteractions() 190 } 191 } 192 193 @Test updateSuppression_noExistingNotification_cancelsNotificationnull194 fun updateSuppression_noExistingNotification_cancelsNotification() { 195 stylusUsiPowerUi.updateSuppression(true) 196 197 verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 198 verifyNoMoreInteractions(notificationManager) 199 } 200 201 @Test updateSuppression_existingNotification_cancelsNotificationnull202 fun updateSuppression_existingNotification_cancelsNotification() { 203 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 204 205 stylusUsiPowerUi.updateSuppression(true) 206 207 inOrder(notificationManager).let { 208 it.verify(notificationManager, times(1)) 209 .notify(eq(R.string.stylus_battery_low_percentage), any()) 210 it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) 211 it.verifyNoMoreInteractions() 212 } 213 } 214 215 @Test refresh_hasConnectedBluetoothStylus_existingNotification_doesNothingnull216 fun refresh_hasConnectedBluetoothStylus_existingNotification_doesNothing() { 217 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 218 whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) 219 clearInvocations(notificationManager) 220 221 stylusUsiPowerUi.refresh() 222 223 verifyNoMoreInteractions(notificationManager) 224 } 225 226 @Test updateBatteryState_showsNotification_logsNotificationShownnull227 fun updateBatteryState_showsNotification_logsNotificationShown() { 228 stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) 229 230 verify(uiEventLogger, times(1)) 231 .logWithInstanceIdAndPosition( 232 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_SHOWN, 233 uid, 234 contextSpy.packageName, 235 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 236 10 237 ) 238 } 239 240 @Test broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivitynull241 fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() { 242 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 243 val activityIntentCaptor = argumentCaptor<Intent>() 244 stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f)) 245 broadcastReceiver.onReceive(contextSpy, intent) 246 247 verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture()) 248 assertThat(activityIntentCaptor.value.action) 249 .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS) 250 val args = 251 activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS) 252 as Bundle 253 assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1) 254 } 255 256 @Test broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivitynull257 fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() { 258 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 259 broadcastReceiver.onReceive(contextSpy, intent) 260 261 verify(contextSpy, never()).startActivity(any()) 262 } 263 264 @Test broadcastReceiver_clicked_logsNotificationClickednull265 fun broadcastReceiver_clicked_logsNotificationClicked() { 266 val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) 267 broadcastReceiver.onReceive(contextSpy, intent) 268 269 verify(uiEventLogger, times(1)) 270 .logWithInstanceIdAndPosition( 271 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_CLICKED, 272 uid, 273 contextSpy.packageName, 274 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 275 100 276 ) 277 } 278 279 @Test broadcastReceiver_dismissed_logsNotificationDismissednull280 fun broadcastReceiver_dismissed_logsNotificationDismissed() { 281 val intent = Intent(StylusUsiPowerUI.ACTION_DISMISSED_LOW_BATTERY) 282 broadcastReceiver.onReceive(contextSpy, intent) 283 284 verify(uiEventLogger, times(1)) 285 .logWithInstanceIdAndPosition( 286 StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_DISMISSED, 287 uid, 288 contextSpy.packageName, 289 InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId), 290 100 291 ) 292 } 293 } 294