Loading packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +51 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles; import static com.android.settingslib.flags.Flags.refactorBatteryLevelDisplay; import static com.android.settingslib.satellite.SatelliteDialogUtils.TYPE_IS_BLUETOOTH; import static com.android.systemui.Flags.iconRefresh2025; import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; Loading @@ -40,6 +41,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BatteryLevelsInfo; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.satellite.SatelliteDialogUtils; Loading Loading @@ -85,6 +87,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private final BluetoothController mController; private CachedBluetoothDevice mMetadataRegisteredDevice = null; private CachedBluetoothDevice mBatteryCallbackRegisteredDevice = null; private final Executor mExecutor; Loading Loading @@ -190,9 +193,13 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { super.handleSetListening(listening); if (!listening) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } } } @Override protected void handleUpdateState(BooleanState state, Object arg) { Loading @@ -211,8 +218,12 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { || mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF; } if (!enabled || !connected || state.isTransient) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } } state.dualTarget = true; state.value = enabled; state.label = mContext.getString(R.string.quick_settings_bluetooth_label); Loading Loading @@ -285,7 +296,11 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices(); if (enabled && connected && !connectedDevices.isEmpty()) { if (connectedDevices.size() > 1) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } return icuMessageFormat(mContext.getResources(), R.string.quick_settings_hotspot_secondary_label_num_devices, connectedDevices.size()); Loading @@ -293,15 +308,26 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { CachedBluetoothDevice device = connectedDevices.get(0); int batteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; if (refactorBatteryLevelDisplay()) { BatteryLevelsInfo batteryLevelsInfo = device.getBatteryLevelsInfo(); if (batteryLevelsInfo != null) { batteryLevel = batteryLevelsInfo.getOverallBatteryLevel(); registerBatteryChangedCallback(device); } else { unregisterBatteryChangedCallback(); } } else { // Use battery level provided by FastPair metadata if available. // If not, fallback to the default battery level from bluetooth. int batteryLevel = getMetadataBatteryLevel(device); batteryLevel = getMetadataBatteryLevel(device); if (batteryLevel > BluetoothUtils.META_INT_ERROR) { listenToMetadata(device); } else { stopListeningToStaleDeviceMetadata(); batteryLevel = device.getMinBatteryLevelWithMemberDevices(); } } if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { return mContext.getString( Loading Loading @@ -370,6 +396,19 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { } } private void registerBatteryChangedCallback(CachedBluetoothDevice cachedDevice) { if (cachedDevice.equals(mBatteryCallbackRegisteredDevice)) return; unregisterBatteryChangedCallback(); cachedDevice.registerCallback(mExecutor, mBatteryChangedCallback); mBatteryCallbackRegisteredDevice = cachedDevice; } private void unregisterBatteryChangedCallback() { if (mBatteryCallbackRegisteredDevice == null) return; mBatteryCallbackRegisteredDevice.unregisterCallback(mBatteryChangedCallback); mBatteryCallbackRegisteredDevice = null; } private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { @Override public void onBluetoothStateChange(boolean enabled) { Loading @@ -386,4 +425,6 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { (device, key, value) -> { if (key == BluetoothDevice.METADATA_MAIN_BATTERY) refreshState(); }; private final CachedBluetoothDevice.Callback mBatteryChangedCallback = this::refreshState; } packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +80 −3 Original line number Diff line number Diff line Loading @@ -18,7 +18,9 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.Utils import com.android.settingslib.bluetooth.BatteryLevelsInfo import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.flags.Flags.FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel Loading @@ -39,10 +41,7 @@ import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes import com.android.systemui.res.R import com.android.systemui.statusbar.policy.BluetoothController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import dagger.Lazy import kotlinx.coroutines.Job Loading @@ -56,6 +55,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters Loading Loading @@ -185,6 +187,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() Loading @@ -203,6 +206,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -227,6 +231,37 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryLevelsInfoAvailable_showBatteryLevelAndRegisterCallback() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel) .isEqualTo( mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(50), ) ) verify(cachedDevice).registerCallback(any(), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryLevelsInfoUnavailable_noBatteryLevel() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, -1) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel).isEqualTo("") } @Test @DisableFlags(QsDetailedView.FLAG_NAME) fun handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothing() { Loading Loading @@ -256,6 +291,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testMetadataListener_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -269,6 +305,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testMetadataListener_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -280,6 +317,31 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testCallbackRegister_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) disableBluetooth() tile.handleUpdateState(state, null) verify(cachedDevice, times(1)).unregisterCallback(any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testCallbackRegister_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) tile.handleSetListening(false) verify(cachedDevice, times(1)).unregisterCallback(any()) } @Test @EnableFlags(QSComposeFragment.FLAG_NAME) fun disableBluetooth_transientTurningOff() { Loading Loading @@ -399,6 +461,21 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { tile.handleUpdateState(state, /* arg= */ null) } private fun listenToDeviceBatteryLevelsInfo( state: QSTile.BooleanState, cachedDevice: CachedBluetoothDevice, batteryLevel: Int, ) { val btDevice = mock<BluetoothDevice>() whenever(cachedDevice.device).thenReturn(btDevice) whenever(cachedDevice.batteryLevelsInfo) .thenReturn(BatteryLevelsInfo(batteryLevel, batteryLevel, batteryLevel, batteryLevel)) enableBluetooth() setBluetoothConnected() addConnectedDevice(cachedDevice) tile.handleUpdateState(state, /* arg= */ null) } private fun createExpectedIcon(resId: Int): QSTile.Icon { return if (isEnabled) { DrawableIconWithRes(mContext.getDrawable(resId), resId) Loading Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +51 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles; import static com.android.settingslib.flags.Flags.refactorBatteryLevelDisplay; import static com.android.settingslib.satellite.SatelliteDialogUtils.TYPE_IS_BLUETOOTH; import static com.android.systemui.Flags.iconRefresh2025; import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; Loading @@ -40,6 +41,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BatteryLevelsInfo; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.satellite.SatelliteDialogUtils; Loading Loading @@ -85,6 +87,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private final BluetoothController mController; private CachedBluetoothDevice mMetadataRegisteredDevice = null; private CachedBluetoothDevice mBatteryCallbackRegisteredDevice = null; private final Executor mExecutor; Loading Loading @@ -190,9 +193,13 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { super.handleSetListening(listening); if (!listening) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } } } @Override protected void handleUpdateState(BooleanState state, Object arg) { Loading @@ -211,8 +218,12 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { || mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF; } if (!enabled || !connected || state.isTransient) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } } state.dualTarget = true; state.value = enabled; state.label = mContext.getString(R.string.quick_settings_bluetooth_label); Loading Loading @@ -285,7 +296,11 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices(); if (enabled && connected && !connectedDevices.isEmpty()) { if (connectedDevices.size() > 1) { if (refactorBatteryLevelDisplay()) { unregisterBatteryChangedCallback(); } else { stopListeningToStaleDeviceMetadata(); } return icuMessageFormat(mContext.getResources(), R.string.quick_settings_hotspot_secondary_label_num_devices, connectedDevices.size()); Loading @@ -293,15 +308,26 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { CachedBluetoothDevice device = connectedDevices.get(0); int batteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; if (refactorBatteryLevelDisplay()) { BatteryLevelsInfo batteryLevelsInfo = device.getBatteryLevelsInfo(); if (batteryLevelsInfo != null) { batteryLevel = batteryLevelsInfo.getOverallBatteryLevel(); registerBatteryChangedCallback(device); } else { unregisterBatteryChangedCallback(); } } else { // Use battery level provided by FastPair metadata if available. // If not, fallback to the default battery level from bluetooth. int batteryLevel = getMetadataBatteryLevel(device); batteryLevel = getMetadataBatteryLevel(device); if (batteryLevel > BluetoothUtils.META_INT_ERROR) { listenToMetadata(device); } else { stopListeningToStaleDeviceMetadata(); batteryLevel = device.getMinBatteryLevelWithMemberDevices(); } } if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { return mContext.getString( Loading Loading @@ -370,6 +396,19 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { } } private void registerBatteryChangedCallback(CachedBluetoothDevice cachedDevice) { if (cachedDevice.equals(mBatteryCallbackRegisteredDevice)) return; unregisterBatteryChangedCallback(); cachedDevice.registerCallback(mExecutor, mBatteryChangedCallback); mBatteryCallbackRegisteredDevice = cachedDevice; } private void unregisterBatteryChangedCallback() { if (mBatteryCallbackRegisteredDevice == null) return; mBatteryCallbackRegisteredDevice.unregisterCallback(mBatteryChangedCallback); mBatteryCallbackRegisteredDevice = null; } private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { @Override public void onBluetoothStateChange(boolean enabled) { Loading @@ -386,4 +425,6 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { (device, key, value) -> { if (key == BluetoothDevice.METADATA_MAIN_BATTERY) refreshState(); }; private final CachedBluetoothDevice.Callback mBatteryChangedCallback = this::refreshState; }
packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +80 −3 Original line number Diff line number Diff line Loading @@ -18,7 +18,9 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.Utils import com.android.settingslib.bluetooth.BatteryLevelsInfo import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.flags.Flags.FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel Loading @@ -39,10 +41,7 @@ import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes import com.android.systemui.res.R import com.android.systemui.statusbar.policy.BluetoothController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import dagger.Lazy import kotlinx.coroutines.Job Loading @@ -56,6 +55,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters Loading Loading @@ -185,6 +187,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() Loading @@ -203,6 +206,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -227,6 +231,37 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryLevelsInfoAvailable_showBatteryLevelAndRegisterCallback() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel) .isEqualTo( mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(50), ) ) verify(cachedDevice).registerCallback(any(), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testSecondaryLabel_whenBatteryLevelsInfoUnavailable_noBatteryLevel() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, -1) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel).isEqualTo("") } @Test @DisableFlags(QsDetailedView.FLAG_NAME) fun handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothing() { Loading Loading @@ -256,6 +291,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testMetadataListener_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -269,6 +305,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test @DisableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testMetadataListener_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() Loading @@ -280,6 +317,31 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testCallbackRegister_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) disableBluetooth() tile.handleUpdateState(state, null) verify(cachedDevice, times(1)).unregisterCallback(any()) } @Test @EnableFlags(FLAG_REFACTOR_BATTERY_LEVEL_DISPLAY) fun testCallbackRegister_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceBatteryLevelsInfo(state, cachedDevice, 50) tile.handleSetListening(false) verify(cachedDevice, times(1)).unregisterCallback(any()) } @Test @EnableFlags(QSComposeFragment.FLAG_NAME) fun disableBluetooth_transientTurningOff() { Loading Loading @@ -399,6 +461,21 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { tile.handleUpdateState(state, /* arg= */ null) } private fun listenToDeviceBatteryLevelsInfo( state: QSTile.BooleanState, cachedDevice: CachedBluetoothDevice, batteryLevel: Int, ) { val btDevice = mock<BluetoothDevice>() whenever(cachedDevice.device).thenReturn(btDevice) whenever(cachedDevice.batteryLevelsInfo) .thenReturn(BatteryLevelsInfo(batteryLevel, batteryLevel, batteryLevel, batteryLevel)) enableBluetooth() setBluetoothConnected() addConnectedDevice(cachedDevice) tile.handleUpdateState(state, /* arg= */ null) } private fun createExpectedIcon(resId: Int): QSTile.Icon { return if (isEnabled) { DrawableIconWithRes(mContext.getDrawable(resId), resId) Loading