diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 14a9161ac2917..5a8850a9f89b0 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -52,8 +52,8 @@ constructor( eventTimeMillis: Long, batteryState: BatteryState ) { - if (batteryState.isPresent) { - stylusUsiPowerUi.updateBatteryState(batteryState) + if (batteryState.isPresent && batteryState.capacity > 0f) { + stylusUsiPowerUi.updateBatteryState(deviceId, batteryState) } } @@ -61,6 +61,7 @@ constructor( if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return if (!hostDeviceSupportsStylusInput()) return + stylusUsiPowerUi.init() stylusManager.registerCallback(this) stylusManager.startListener() } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index e8216576811a7..8d5e01c5b7822 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -18,17 +18,21 @@ package com.android.systemui.stylus import android.Manifest import android.app.PendingIntent +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.hardware.BatteryState import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.os.UserHandle +import android.util.Log import android.view.InputDevice import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -53,6 +57,7 @@ constructor( // These values must only be accessed on the handler. private var batteryCapacity = 1.0f private var suppressed = false + private var inputDeviceId: Int? = null fun init() { val filter = @@ -87,10 +92,12 @@ constructor( } } - fun updateBatteryState(batteryState: BatteryState) { + fun updateBatteryState(deviceId: Int, batteryState: BatteryState) { handler.post updateBattery@{ - if (batteryState.capacity == batteryCapacity) return@updateBattery + if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f) + return@updateBattery + inputDeviceId = deviceId batteryCapacity = batteryState.capacity refresh() } @@ -150,23 +157,41 @@ constructor( } private fun getPendingBroadcast(action: String): PendingIntent? { - return PendingIntent.getBroadcastAsUser( + return PendingIntent.getBroadcast( context, 0, - Intent(action), + Intent(action).setPackage(context.packageName), PendingIntent.FLAG_IMMUTABLE, - UserHandle.CURRENT ) } - private val receiver: BroadcastReceiver = + @VisibleForTesting + internal val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true) ACTION_CLICKED_LOW_BATTERY -> { updateSuppression(true) - // TODO(b/261584943): open USI device details page + if (inputDeviceId == null) return + + val args = Bundle() + args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!) + try { + context.startActivity( + Intent(ACTION_STYLUS_USI_DETAILS) + .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } catch (e: ActivityNotFoundException) { + // In the rare scenario where the Settings app manifest doesn't contain + // the USI details activity, ignore the intent. + Log.e( + StylusUsiPowerUI::class.java.simpleName, + "Cannot open USI details page." + ) + } } } } @@ -179,7 +204,11 @@ constructor( private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage - private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" - private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" + @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting + const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS" + @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id" + @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt new file mode 100644 index 0000000000000..7e010886c3946 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.stylus + +import android.hardware.BatteryState + +class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { + override fun getCapacity() = capacity + override fun getStatus() = 0 + override fun isPresent() = true +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index 117e00dd9b2a1..1cccd65c8dbc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -84,6 +84,13 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { verifyZeroInteractions(stylusManager) } + @Test + fun start_initStylusUsiPowerUi() { + startable.start() + + verify(stylusUsiPowerUi, times(1)).init() + } + @Test fun onStylusBluetoothConnected_refreshesNotification() { startable.onStylusBluetoothConnected(STYLUS_DEVICE_ID, "ANY") @@ -99,13 +106,21 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() { - val batteryState = mock(BatteryState::class.java) - whenever(batteryState.isPresent).thenReturn(true) + fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() { + val batteryState = FixedCapacityBatteryState(0.1f) startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) - verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState) + verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState) + } + + @Test + fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() { + val batteryState = FixedCapacityBatteryState(0f) + + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + + verifyNoMoreInteractions(stylusUsiPowerUi) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt index a7951f4fa0682..96e115951aaa1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt @@ -17,8 +17,11 @@ package com.android.systemui.stylus import android.app.Notification -import android.hardware.BatteryState +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice @@ -27,8 +30,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Ignore @@ -37,7 +42,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.doNothing import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -53,11 +61,16 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Captor lateinit var notificationCaptor: ArgumentCaptor private lateinit var stylusUsiPowerUi: StylusUsiPowerUI + private lateinit var broadcastReceiver: BroadcastReceiver + private lateinit var contextSpy: Context @Before fun setUp() { MockitoAnnotations.initMocks(this) + contextSpy = spy(mContext) + doNothing().whenever(contextSpy).startActivity(any()) + whenever(handler.post(any())).thenAnswer { (it.arguments[0] as Runnable).run() true @@ -68,12 +81,20 @@ class StylusUsiPowerUiTest : SysuiTestCase() { whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) // whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES") - stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler) + stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler) + broadcastReceiver = stylusUsiPowerUi.receiver + } + + @Test + fun updateBatteryState_capacityZero_noop() { + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f)) + + verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityBelowThreshold_notifies() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) verify(notificationManager, times(1)) .notify(eq(R.string.stylus_battery_low_percentage), any()) @@ -82,7 +103,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) @@ -90,8 +111,8 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) inOrder(notificationManager).let { it.verify(notificationManager, times(1)) @@ -103,8 +124,8 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f)) verify(notificationManager, times(2)) .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) @@ -121,9 +142,9 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) inOrder(notificationManager).let { it.verify(notificationManager, times(1)) @@ -145,7 +166,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateSuppression_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateSuppression(true) @@ -170,7 +191,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test @Ignore("TODO(b/257936830): get bt address once input api available") fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() @@ -178,9 +199,27 @@ class StylusUsiPowerUiTest : SysuiTestCase() { verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } - class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { - override fun getCapacity() = capacity - override fun getStatus() = 0 - override fun isPresent() = true + @Test + fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + val activityIntentCaptor = argumentCaptor() + stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f)) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture()) + assertThat(activityIntentCaptor.value.action) + .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS) + val args = + activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS) + as Bundle + assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1) + } + + @Test + fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, never()).startActivity(any()) } }