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