Loading packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +73 −7 Original line number Diff line number Diff line Loading @@ -18,24 +18,26 @@ package com.android.systemui.qs.tiles; import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Switch; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; Loading @@ -50,6 +52,7 @@ import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.policy.BluetoothController; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; Loading @@ -60,8 +63,14 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); private static final String TAG = BluetoothTile.class.getSimpleName(); private final BluetoothController mController; private CachedBluetoothDevice mMetadataRegisteredDevice = null; private final Executor mExecutor; @Inject public BluetoothTile( QSHost host, Loading @@ -78,6 +87,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { statusBarStateController, activityStarter, qsLogger); mController = bluetoothController; mController.observe(getLifecycle(), mCallback); mExecutor = new HandlerExecutor(mainHandler); } @Override Loading Loading @@ -116,6 +126,15 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { return mContext.getString(R.string.quick_settings_bluetooth_label); } @Override protected void handleSetListening(boolean listening) { super.handleSetListening(listening); if (!listening) { stopListeningToStaleDeviceMetadata(); } } @Override protected void handleUpdateState(BooleanState state, Object arg) { checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH); Loading @@ -125,6 +144,9 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { final boolean connecting = mController.isBluetoothConnecting(); state.isTransient = transientEnabling || connecting || mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON; if (!enabled || !connected || state.isTransient) { stopListeningToStaleDeviceMetadata(); } state.dualTarget = true; state.value = enabled; if (state.slash == null) { Loading Loading @@ -187,23 +209,32 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices(); if (enabled && connected && !connectedDevices.isEmpty()) { if (connectedDevices.size() > 1) { stopListeningToStaleDeviceMetadata(); return icuMessageFormat(mContext.getResources(), R.string.quick_settings_hotspot_secondary_label_num_devices, connectedDevices.size()); } CachedBluetoothDevice lastDevice = connectedDevices.get(0); final int batteryLevel = lastDevice.getBatteryLevel(); CachedBluetoothDevice device = connectedDevices.get(0); // Use battery level provided by FastPair metadata if available. // If not, fallback to the default battery level from bluetooth. int batteryLevel = getMetadataBatteryLevel(device); if (batteryLevel > BluetoothUtils.META_INT_ERROR) { listenToMetadata(device); } else { stopListeningToStaleDeviceMetadata(); batteryLevel = device.getBatteryLevel(); } if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { return mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(batteryLevel)); } else { final BluetoothClass bluetoothClass = lastDevice.getBtClass(); final BluetoothClass bluetoothClass = device.getBtClass(); if (bluetoothClass != null) { if (lastDevice.isHearingAidDevice()) { if (device.isHearingAidDevice()) { return mContext.getString( R.string.quick_settings_bluetooth_secondary_label_hearing_aids); } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { Loading Loading @@ -233,6 +264,36 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { return mController.isBluetoothSupported(); } private int getMetadataBatteryLevel(CachedBluetoothDevice device) { return BluetoothUtils.getIntMetaData(device.getDevice(), BluetoothDevice.METADATA_MAIN_BATTERY); } private void listenToMetadata(CachedBluetoothDevice cachedDevice) { if (cachedDevice == mMetadataRegisteredDevice) return; stopListeningToStaleDeviceMetadata(); try { mController.addOnMetadataChangedListener(cachedDevice, mExecutor, mMetadataChangedListener); mMetadataRegisteredDevice = cachedDevice; } catch (IllegalArgumentException e) { Log.e(TAG, "Battery metadata listener already registered for device."); } } private void stopListeningToStaleDeviceMetadata() { if (mMetadataRegisteredDevice == null) return; try { mController.removeOnMetadataChangedListener( mMetadataRegisteredDevice, mMetadataChangedListener); mMetadataRegisteredDevice = null; } catch (IllegalArgumentException e) { Log.e(TAG, "Battery metadata listener already unregistered for device."); } } private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { @Override public void onBluetoothStateChange(boolean enabled) { Loading @@ -244,4 +305,9 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { refreshState(); } }; private final BluetoothAdapter.OnMetadataChangedListener mMetadataChangedListener = (device, key, value) -> { if (key == BluetoothDevice.METADATA_MAIN_BATTERY) refreshState(); }; } packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java +8 −0 Original line number Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.policy; import android.bluetooth.BluetoothAdapter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.BluetoothController.Callback; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; public interface BluetoothController extends CallbackController<Callback>, Dumpable { boolean isBluetoothSupported(); Loading @@ -44,6 +47,11 @@ public interface BluetoothController extends CallbackController<Callback>, Dumpa int getBondState(CachedBluetoothDevice device); List<CachedBluetoothDevice> getConnectedDevices(); void addOnMetadataChangedListener(CachedBluetoothDevice device, Executor executor, BluetoothAdapter.OnMetadataChangedListener listener); void removeOnMetadataChangedListener(CachedBluetoothDevice device, BluetoothAdapter.OnMetadataChangedListener listener); public interface Callback { void onBluetoothStateChange(boolean enabled); void onBluetoothDevicesChanged(); Loading packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +29 −1 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.Executor; import javax.inject.Inject; Loading Loading @@ -78,6 +79,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa private final H mHandler; private int mState; private final BluetoothAdapter mAdapter; /** */ @Inject Loading @@ -88,7 +90,8 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa BluetoothLogger logger, @Background Looper bgLooper, @Main Looper mainLooper, @Nullable LocalBluetoothManager localBluetoothManager) { @Nullable LocalBluetoothManager localBluetoothManager, @Nullable BluetoothAdapter bluetoothAdapter) { mDumpManager = dumpManager; mLogger = logger; mLocalBluetoothManager = localBluetoothManager; Loading @@ -103,6 +106,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mCurrentUser = userTracker.getUserId(); mDumpManager.registerDumpable(TAG, this); mAdapter = bluetoothAdapter; } @Override Loading Loading @@ -412,6 +416,30 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } public void addOnMetadataChangedListener( @NonNull CachedBluetoothDevice cachedDevice, Executor executor, BluetoothAdapter.OnMetadataChangedListener listener ) { if (mAdapter == null) return; mAdapter.addOnMetadataChangedListener( cachedDevice.getDevice(), executor, listener ); } public void removeOnMetadataChangedListener( @NonNull CachedBluetoothDevice cachedDevice, BluetoothAdapter.OnMetadataChangedListener listener ) { if (mAdapter == null) return; mAdapter.removeOnMetadataChangedListener( cachedDevice.getDevice(), listener ); } private ActuallyCachedState getCachedState(CachedBluetoothDevice device) { ActuallyCachedState state = mCachedState.get(device); if (state == null) { Loading packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +142 −54 Original line number Diff line number Diff line package com.android.systemui.qs.tiles import android.content.Context import android.bluetooth.BluetoothDevice import android.os.Handler import android.os.Looper import android.os.UserManager Loading @@ -10,6 +10,8 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.Utils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake Loading @@ -21,14 +23,18 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl 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 org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) Loading @@ -36,21 +42,13 @@ import org.mockito.MockitoAnnotations @SmallTest class BluetoothTileTest : SysuiTestCase() { @Mock private lateinit var mockContext: Context @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger private val falsingManager = FalsingManagerFake() @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var bluetoothController: BluetoothController @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var bluetoothController: BluetoothController private val uiEventLogger = UiEventLoggerFake() private lateinit var testableLooper: TestableLooper Loading @@ -61,10 +59,11 @@ class BluetoothTileTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) Mockito.`when`(qsHost.context).thenReturn(mockContext) Mockito.`when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) whenever(qsHost.context).thenReturn(mContext) whenever(qsHost.uiEventLogger).thenReturn(uiEventLogger) tile = FakeBluetoothTile( tile = FakeBluetoothTile( qsHost, testableLooper.looper, Handler(testableLooper.looper), Loading @@ -73,7 +72,7 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController, activityStarter, qsLogger, bluetoothController bluetoothController, ) tile.initialize() Loading Loading @@ -141,6 +140,75 @@ class BluetoothTileTest : SysuiTestCase() { .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_search)) } @Test fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() listenToDeviceMetadata(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(bluetoothController) .addOnMetadataChangedListener(eq(cachedDevice), any(), any()) } @Test fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) val cachedDevice2 = mock<CachedBluetoothDevice>() val btDevice = mock<BluetoothDevice>() whenever(cachedDevice2.device).thenReturn(btDevice) whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(null) whenever(cachedDevice2.batteryLevel).thenReturn(25) addConnectedDevice(cachedDevice2) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel) .isEqualTo( mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(25) ) ) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test fun testMetadataListener_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) disableBluetooth() tile.handleUpdateState(state, null) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test fun testMetadataListener_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) tile.handleSetListening(false) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } private class FakeBluetoothTile( qsHost: QSHost, backgroundLooper: Looper, Loading @@ -150,8 +218,9 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, qsLogger: QSLogger, bluetoothController: BluetoothController ) : BluetoothTile( bluetoothController: BluetoothController, ) : BluetoothTile( qsHost, backgroundLooper, mainHandler, Loading @@ -160,7 +229,7 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController, activityStarter, qsLogger, bluetoothController bluetoothController, ) { var restrictionChecked: String? = null Loading @@ -173,25 +242,44 @@ class BluetoothTileTest : SysuiTestCase() { } fun enableBluetooth() { `when`(bluetoothController.isBluetoothEnabled).thenReturn(true) whenever(bluetoothController.isBluetoothEnabled).thenReturn(true) } fun disableBluetooth() { `when`(bluetoothController.isBluetoothEnabled).thenReturn(false) whenever(bluetoothController.isBluetoothEnabled).thenReturn(false) } fun setBluetoothDisconnected() { `when`(bluetoothController.isBluetoothConnecting).thenReturn(false) `when`(bluetoothController.isBluetoothConnected).thenReturn(false) whenever(bluetoothController.isBluetoothConnecting).thenReturn(false) whenever(bluetoothController.isBluetoothConnected).thenReturn(false) } fun setBluetoothConnected() { `when`(bluetoothController.isBluetoothConnecting).thenReturn(false) `when`(bluetoothController.isBluetoothConnected).thenReturn(true) whenever(bluetoothController.isBluetoothConnecting).thenReturn(false) whenever(bluetoothController.isBluetoothConnected).thenReturn(true) } fun setBluetoothConnecting() { `when`(bluetoothController.isBluetoothConnected).thenReturn(false) `when`(bluetoothController.isBluetoothConnecting).thenReturn(true) whenever(bluetoothController.isBluetoothConnected).thenReturn(false) whenever(bluetoothController.isBluetoothConnecting).thenReturn(true) } fun addConnectedDevice(device: CachedBluetoothDevice) { whenever(bluetoothController.connectedDevices).thenReturn(listOf(device)) } fun listenToDeviceMetadata( state: QSTile.BooleanState, cachedDevice: CachedBluetoothDevice, batteryLevel: Int ) { val btDevice = mock<BluetoothDevice>() whenever(cachedDevice.device).thenReturn(btDevice) whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)) .thenReturn(batteryLevel.toString().toByteArray()) enableBluetooth() setBluetoothConnected() addConnectedDevice(cachedDevice) tile.handleUpdateState(state, /* arg= */ null) } } packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +42 −7 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -44,6 +45,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; Loading @@ -51,6 +54,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper Loading @@ -60,10 +64,11 @@ public class BluetoothControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; private LocalBluetoothManager mMockBluetoothManager; private CachedBluetoothDeviceManager mMockDeviceManager; private LocalBluetoothAdapter mMockAdapter; private LocalBluetoothAdapter mMockLocalAdapter; private TestableLooper mTestableLooper; private DumpManager mMockDumpManager; private BluetoothControllerImpl mBluetoothControllerImpl; private BluetoothAdapter mMockAdapter; private List<CachedBluetoothDevice> mDevices; Loading @@ -74,10 +79,11 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mDevices = new ArrayList<>(); mUserTracker = mock(UserTracker.class); mMockDeviceManager = mock(CachedBluetoothDeviceManager.class); mMockAdapter = mock(BluetoothAdapter.class); when(mMockDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices); when(mMockBluetoothManager.getCachedDeviceManager()).thenReturn(mMockDeviceManager); mMockAdapter = mock(LocalBluetoothAdapter.class); when(mMockBluetoothManager.getBluetoothAdapter()).thenReturn(mMockAdapter); mMockLocalAdapter = mock(LocalBluetoothAdapter.class); when(mMockBluetoothManager.getBluetoothAdapter()).thenReturn(mMockLocalAdapter); when(mMockBluetoothManager.getEventManager()).thenReturn(mock(BluetoothEventManager.class)); when(mMockBluetoothManager.getProfileManager()) .thenReturn(mock(LocalBluetoothProfileManager.class)); Loading @@ -89,7 +95,8 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mock(BluetoothLogger.class), mTestableLooper.getLooper(), mTestableLooper.getLooper(), mMockBluetoothManager); mMockBluetoothManager, mMockAdapter); } @Test Loading @@ -98,7 +105,8 @@ public class BluetoothControllerImplTest extends SysuiTestCase { when(device.isConnected()).thenReturn(true); when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED); mDevices.add(device); when(mMockAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_DISCONNECTED); when(mMockLocalAdapter.getConnectionState()) .thenReturn(BluetoothAdapter.STATE_DISCONNECTED); mBluetoothControllerImpl.onConnectionStateChanged(null, BluetoothAdapter.STATE_DISCONNECTED); Loading Loading @@ -163,7 +171,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Test public void testOnServiceConnected_updatesConnectionState() { when(mMockAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); mBluetoothControllerImpl.onServiceConnected(); Loading @@ -184,7 +192,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Test public void testOnBluetoothStateChange_updatesConnectionState() { when(mMockAdapter.getConnectionState()).thenReturn( when(mMockLocalAdapter.getConnectionState()).thenReturn( BluetoothAdapter.STATE_CONNECTING, BluetoothAdapter.STATE_DISCONNECTED); Loading Loading @@ -240,6 +248,33 @@ public class BluetoothControllerImplTest extends SysuiTestCase { assertTrue(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()); } @Test public void testAddOnMetadataChangedListener_registersListenerOnAdapter() { CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice device = mock(BluetoothDevice.class); when(cachedDevice.getDevice()).thenReturn(device); Executor executor = new FakeExecutor(new FakeSystemClock()); BluetoothAdapter.OnMetadataChangedListener listener = (bluetoothDevice, i, bytes) -> { }; mBluetoothControllerImpl.addOnMetadataChangedListener(cachedDevice, executor, listener); verify(mMockAdapter, times(1)).addOnMetadataChangedListener(device, executor, listener); } @Test public void testRemoveOnMetadataChangedListener_removesListenerFromAdapter() { CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice device = mock(BluetoothDevice.class); when(cachedDevice.getDevice()).thenReturn(device); BluetoothAdapter.OnMetadataChangedListener listener = (bluetoothDevice, i, bytes) -> { }; mBluetoothControllerImpl.removeOnMetadataChangedListener(cachedDevice, listener); verify(mMockAdapter, times(1)).removeOnMetadataChangedListener(device, listener); } /** Regression test for b/246876230. */ @Test public void testOnActiveDeviceChanged_null_noCrash() { Loading Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +73 −7 Original line number Diff line number Diff line Loading @@ -18,24 +18,26 @@ package com.android.systemui.qs.tiles; import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Switch; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; Loading @@ -50,6 +52,7 @@ import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.policy.BluetoothController; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; Loading @@ -60,8 +63,14 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); private static final String TAG = BluetoothTile.class.getSimpleName(); private final BluetoothController mController; private CachedBluetoothDevice mMetadataRegisteredDevice = null; private final Executor mExecutor; @Inject public BluetoothTile( QSHost host, Loading @@ -78,6 +87,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { statusBarStateController, activityStarter, qsLogger); mController = bluetoothController; mController.observe(getLifecycle(), mCallback); mExecutor = new HandlerExecutor(mainHandler); } @Override Loading Loading @@ -116,6 +126,15 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { return mContext.getString(R.string.quick_settings_bluetooth_label); } @Override protected void handleSetListening(boolean listening) { super.handleSetListening(listening); if (!listening) { stopListeningToStaleDeviceMetadata(); } } @Override protected void handleUpdateState(BooleanState state, Object arg) { checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH); Loading @@ -125,6 +144,9 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { final boolean connecting = mController.isBluetoothConnecting(); state.isTransient = transientEnabling || connecting || mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON; if (!enabled || !connected || state.isTransient) { stopListeningToStaleDeviceMetadata(); } state.dualTarget = true; state.value = enabled; if (state.slash == null) { Loading Loading @@ -187,23 +209,32 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices(); if (enabled && connected && !connectedDevices.isEmpty()) { if (connectedDevices.size() > 1) { stopListeningToStaleDeviceMetadata(); return icuMessageFormat(mContext.getResources(), R.string.quick_settings_hotspot_secondary_label_num_devices, connectedDevices.size()); } CachedBluetoothDevice lastDevice = connectedDevices.get(0); final int batteryLevel = lastDevice.getBatteryLevel(); CachedBluetoothDevice device = connectedDevices.get(0); // Use battery level provided by FastPair metadata if available. // If not, fallback to the default battery level from bluetooth. int batteryLevel = getMetadataBatteryLevel(device); if (batteryLevel > BluetoothUtils.META_INT_ERROR) { listenToMetadata(device); } else { stopListeningToStaleDeviceMetadata(); batteryLevel = device.getBatteryLevel(); } if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { return mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(batteryLevel)); } else { final BluetoothClass bluetoothClass = lastDevice.getBtClass(); final BluetoothClass bluetoothClass = device.getBtClass(); if (bluetoothClass != null) { if (lastDevice.isHearingAidDevice()) { if (device.isHearingAidDevice()) { return mContext.getString( R.string.quick_settings_bluetooth_secondary_label_hearing_aids); } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { Loading Loading @@ -233,6 +264,36 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { return mController.isBluetoothSupported(); } private int getMetadataBatteryLevel(CachedBluetoothDevice device) { return BluetoothUtils.getIntMetaData(device.getDevice(), BluetoothDevice.METADATA_MAIN_BATTERY); } private void listenToMetadata(CachedBluetoothDevice cachedDevice) { if (cachedDevice == mMetadataRegisteredDevice) return; stopListeningToStaleDeviceMetadata(); try { mController.addOnMetadataChangedListener(cachedDevice, mExecutor, mMetadataChangedListener); mMetadataRegisteredDevice = cachedDevice; } catch (IllegalArgumentException e) { Log.e(TAG, "Battery metadata listener already registered for device."); } } private void stopListeningToStaleDeviceMetadata() { if (mMetadataRegisteredDevice == null) return; try { mController.removeOnMetadataChangedListener( mMetadataRegisteredDevice, mMetadataChangedListener); mMetadataRegisteredDevice = null; } catch (IllegalArgumentException e) { Log.e(TAG, "Battery metadata listener already unregistered for device."); } } private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { @Override public void onBluetoothStateChange(boolean enabled) { Loading @@ -244,4 +305,9 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { refreshState(); } }; private final BluetoothAdapter.OnMetadataChangedListener mMetadataChangedListener = (device, key, value) -> { if (key == BluetoothDevice.METADATA_MAIN_BATTERY) refreshState(); }; }
packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java +8 −0 Original line number Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.policy; import android.bluetooth.BluetoothAdapter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.BluetoothController.Callback; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; public interface BluetoothController extends CallbackController<Callback>, Dumpable { boolean isBluetoothSupported(); Loading @@ -44,6 +47,11 @@ public interface BluetoothController extends CallbackController<Callback>, Dumpa int getBondState(CachedBluetoothDevice device); List<CachedBluetoothDevice> getConnectedDevices(); void addOnMetadataChangedListener(CachedBluetoothDevice device, Executor executor, BluetoothAdapter.OnMetadataChangedListener listener); void removeOnMetadataChangedListener(CachedBluetoothDevice device, BluetoothAdapter.OnMetadataChangedListener listener); public interface Callback { void onBluetoothStateChange(boolean enabled); void onBluetoothDevicesChanged(); Loading
packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +29 −1 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.Executor; import javax.inject.Inject; Loading Loading @@ -78,6 +79,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa private final H mHandler; private int mState; private final BluetoothAdapter mAdapter; /** */ @Inject Loading @@ -88,7 +90,8 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa BluetoothLogger logger, @Background Looper bgLooper, @Main Looper mainLooper, @Nullable LocalBluetoothManager localBluetoothManager) { @Nullable LocalBluetoothManager localBluetoothManager, @Nullable BluetoothAdapter bluetoothAdapter) { mDumpManager = dumpManager; mLogger = logger; mLocalBluetoothManager = localBluetoothManager; Loading @@ -103,6 +106,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mCurrentUser = userTracker.getUserId(); mDumpManager.registerDumpable(TAG, this); mAdapter = bluetoothAdapter; } @Override Loading Loading @@ -412,6 +416,30 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED); } public void addOnMetadataChangedListener( @NonNull CachedBluetoothDevice cachedDevice, Executor executor, BluetoothAdapter.OnMetadataChangedListener listener ) { if (mAdapter == null) return; mAdapter.addOnMetadataChangedListener( cachedDevice.getDevice(), executor, listener ); } public void removeOnMetadataChangedListener( @NonNull CachedBluetoothDevice cachedDevice, BluetoothAdapter.OnMetadataChangedListener listener ) { if (mAdapter == null) return; mAdapter.removeOnMetadataChangedListener( cachedDevice.getDevice(), listener ); } private ActuallyCachedState getCachedState(CachedBluetoothDevice device) { ActuallyCachedState state = mCachedState.get(device); if (state == null) { Loading
packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +142 −54 Original line number Diff line number Diff line package com.android.systemui.qs.tiles import android.content.Context import android.bluetooth.BluetoothDevice import android.os.Handler import android.os.Looper import android.os.UserManager Loading @@ -10,6 +10,8 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.Utils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake Loading @@ -21,14 +23,18 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl 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 org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) Loading @@ -36,21 +42,13 @@ import org.mockito.MockitoAnnotations @SmallTest class BluetoothTileTest : SysuiTestCase() { @Mock private lateinit var mockContext: Context @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger private val falsingManager = FalsingManagerFake() @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var bluetoothController: BluetoothController @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var bluetoothController: BluetoothController private val uiEventLogger = UiEventLoggerFake() private lateinit var testableLooper: TestableLooper Loading @@ -61,10 +59,11 @@ class BluetoothTileTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) Mockito.`when`(qsHost.context).thenReturn(mockContext) Mockito.`when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) whenever(qsHost.context).thenReturn(mContext) whenever(qsHost.uiEventLogger).thenReturn(uiEventLogger) tile = FakeBluetoothTile( tile = FakeBluetoothTile( qsHost, testableLooper.looper, Handler(testableLooper.looper), Loading @@ -73,7 +72,7 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController, activityStarter, qsLogger, bluetoothController bluetoothController, ) tile.initialize() Loading Loading @@ -141,6 +140,75 @@ class BluetoothTileTest : SysuiTestCase() { .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_search)) } @Test fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() { val cachedDevice = mock<CachedBluetoothDevice>() val state = QSTile.BooleanState() listenToDeviceMetadata(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(bluetoothController) .addOnMetadataChangedListener(eq(cachedDevice), any(), any()) } @Test fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) val cachedDevice2 = mock<CachedBluetoothDevice>() val btDevice = mock<BluetoothDevice>() whenever(cachedDevice2.device).thenReturn(btDevice) whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(null) whenever(cachedDevice2.batteryLevel).thenReturn(25) addConnectedDevice(cachedDevice2) tile.handleUpdateState(state, /* arg= */ null) assertThat(state.secondaryLabel) .isEqualTo( mContext.getString( R.string.quick_settings_bluetooth_secondary_label_battery_level, Utils.formatPercentage(25) ) ) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test fun testMetadataListener_whenDisconnected_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) disableBluetooth() tile.handleUpdateState(state, null) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } @Test fun testMetadataListener_whenTileNotListening_isUnregistered() { val state = QSTile.BooleanState() val cachedDevice = mock<CachedBluetoothDevice>() listenToDeviceMetadata(state, cachedDevice, 50) tile.handleSetListening(false) verify(bluetoothController, times(1)) .removeOnMetadataChangedListener(eq(cachedDevice), any()) } private class FakeBluetoothTile( qsHost: QSHost, backgroundLooper: Looper, Loading @@ -150,8 +218,9 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, qsLogger: QSLogger, bluetoothController: BluetoothController ) : BluetoothTile( bluetoothController: BluetoothController, ) : BluetoothTile( qsHost, backgroundLooper, mainHandler, Loading @@ -160,7 +229,7 @@ class BluetoothTileTest : SysuiTestCase() { statusBarStateController, activityStarter, qsLogger, bluetoothController bluetoothController, ) { var restrictionChecked: String? = null Loading @@ -173,25 +242,44 @@ class BluetoothTileTest : SysuiTestCase() { } fun enableBluetooth() { `when`(bluetoothController.isBluetoothEnabled).thenReturn(true) whenever(bluetoothController.isBluetoothEnabled).thenReturn(true) } fun disableBluetooth() { `when`(bluetoothController.isBluetoothEnabled).thenReturn(false) whenever(bluetoothController.isBluetoothEnabled).thenReturn(false) } fun setBluetoothDisconnected() { `when`(bluetoothController.isBluetoothConnecting).thenReturn(false) `when`(bluetoothController.isBluetoothConnected).thenReturn(false) whenever(bluetoothController.isBluetoothConnecting).thenReturn(false) whenever(bluetoothController.isBluetoothConnected).thenReturn(false) } fun setBluetoothConnected() { `when`(bluetoothController.isBluetoothConnecting).thenReturn(false) `when`(bluetoothController.isBluetoothConnected).thenReturn(true) whenever(bluetoothController.isBluetoothConnecting).thenReturn(false) whenever(bluetoothController.isBluetoothConnected).thenReturn(true) } fun setBluetoothConnecting() { `when`(bluetoothController.isBluetoothConnected).thenReturn(false) `when`(bluetoothController.isBluetoothConnecting).thenReturn(true) whenever(bluetoothController.isBluetoothConnected).thenReturn(false) whenever(bluetoothController.isBluetoothConnecting).thenReturn(true) } fun addConnectedDevice(device: CachedBluetoothDevice) { whenever(bluetoothController.connectedDevices).thenReturn(listOf(device)) } fun listenToDeviceMetadata( state: QSTile.BooleanState, cachedDevice: CachedBluetoothDevice, batteryLevel: Int ) { val btDevice = mock<BluetoothDevice>() whenever(cachedDevice.device).thenReturn(btDevice) whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)) .thenReturn(batteryLevel.toString().toByteArray()) enableBluetooth() setBluetoothConnected() addConnectedDevice(cachedDevice) tile.handleUpdateState(state, /* arg= */ null) } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +42 −7 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -44,6 +45,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; Loading @@ -51,6 +54,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper Loading @@ -60,10 +64,11 @@ public class BluetoothControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; private LocalBluetoothManager mMockBluetoothManager; private CachedBluetoothDeviceManager mMockDeviceManager; private LocalBluetoothAdapter mMockAdapter; private LocalBluetoothAdapter mMockLocalAdapter; private TestableLooper mTestableLooper; private DumpManager mMockDumpManager; private BluetoothControllerImpl mBluetoothControllerImpl; private BluetoothAdapter mMockAdapter; private List<CachedBluetoothDevice> mDevices; Loading @@ -74,10 +79,11 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mDevices = new ArrayList<>(); mUserTracker = mock(UserTracker.class); mMockDeviceManager = mock(CachedBluetoothDeviceManager.class); mMockAdapter = mock(BluetoothAdapter.class); when(mMockDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices); when(mMockBluetoothManager.getCachedDeviceManager()).thenReturn(mMockDeviceManager); mMockAdapter = mock(LocalBluetoothAdapter.class); when(mMockBluetoothManager.getBluetoothAdapter()).thenReturn(mMockAdapter); mMockLocalAdapter = mock(LocalBluetoothAdapter.class); when(mMockBluetoothManager.getBluetoothAdapter()).thenReturn(mMockLocalAdapter); when(mMockBluetoothManager.getEventManager()).thenReturn(mock(BluetoothEventManager.class)); when(mMockBluetoothManager.getProfileManager()) .thenReturn(mock(LocalBluetoothProfileManager.class)); Loading @@ -89,7 +95,8 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mock(BluetoothLogger.class), mTestableLooper.getLooper(), mTestableLooper.getLooper(), mMockBluetoothManager); mMockBluetoothManager, mMockAdapter); } @Test Loading @@ -98,7 +105,8 @@ public class BluetoothControllerImplTest extends SysuiTestCase { when(device.isConnected()).thenReturn(true); when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED); mDevices.add(device); when(mMockAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_DISCONNECTED); when(mMockLocalAdapter.getConnectionState()) .thenReturn(BluetoothAdapter.STATE_DISCONNECTED); mBluetoothControllerImpl.onConnectionStateChanged(null, BluetoothAdapter.STATE_DISCONNECTED); Loading Loading @@ -163,7 +171,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Test public void testOnServiceConnected_updatesConnectionState() { when(mMockAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); mBluetoothControllerImpl.onServiceConnected(); Loading @@ -184,7 +192,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Test public void testOnBluetoothStateChange_updatesConnectionState() { when(mMockAdapter.getConnectionState()).thenReturn( when(mMockLocalAdapter.getConnectionState()).thenReturn( BluetoothAdapter.STATE_CONNECTING, BluetoothAdapter.STATE_DISCONNECTED); Loading Loading @@ -240,6 +248,33 @@ public class BluetoothControllerImplTest extends SysuiTestCase { assertTrue(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()); } @Test public void testAddOnMetadataChangedListener_registersListenerOnAdapter() { CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice device = mock(BluetoothDevice.class); when(cachedDevice.getDevice()).thenReturn(device); Executor executor = new FakeExecutor(new FakeSystemClock()); BluetoothAdapter.OnMetadataChangedListener listener = (bluetoothDevice, i, bytes) -> { }; mBluetoothControllerImpl.addOnMetadataChangedListener(cachedDevice, executor, listener); verify(mMockAdapter, times(1)).addOnMetadataChangedListener(device, executor, listener); } @Test public void testRemoveOnMetadataChangedListener_removesListenerFromAdapter() { CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice device = mock(BluetoothDevice.class); when(cachedDevice.getDevice()).thenReturn(device); BluetoothAdapter.OnMetadataChangedListener listener = (bluetoothDevice, i, bytes) -> { }; mBluetoothControllerImpl.removeOnMetadataChangedListener(cachedDevice, listener); verify(mMockAdapter, times(1)).removeOnMetadataChangedListener(device, listener); } /** Regression test for b/246876230. */ @Test public void testOnActiveDeviceChanged_null_noCrash() { Loading