Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 54603d45 authored by Vania Januar's avatar Vania Januar Committed by Android (Google) Code Review
Browse files

Merge "Open USI details page from USI low battery notification." into tm-qpr-dev

parents fa2d8664 382a00b5
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -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()
    }
+38 −9
Original line number Diff line number Diff line
@@ -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"
    }
}
+25 −0
Original line number Diff line number Diff line
/*
 * 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
}
+19 −4
Original line number Diff line number Diff line
@@ -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
+56 −17
Original line number Diff line number Diff line
@@ -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<Notification>

    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<Intent>()
        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())
    }
}