Bluetooth: Display battery level of connected devices
* Add handler for BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED intent
* Check battery level information when UI is updated
* Show battery level in Quick Settings connected device summary line
* Show battery level in Bluetooth Settings connected device summary line
* Show battery level in Bluetooth device details page device summary
line
* Add unit test for CachedBluetoothDevice, change HeadsetProfile and
HidProfile to not final to enable mocking
Bug: 35874078
Test: make, unit test, connect to remote devices, connect/disconnect
profiles
Change-Id: I729048cace73aab29337a8002a2897d2acf22fa6
This commit is contained in:
@@ -102,6 +102,7 @@ public class BluetoothEventManager {
|
||||
// Fine-grained state broadcasts
|
||||
addHandler(BluetoothDevice.ACTION_CLASS_CHANGED, new ClassChangedHandler());
|
||||
addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler());
|
||||
addHandler(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED, new BatteryLevelChangedHandler());
|
||||
|
||||
// Dock event broadcasts
|
||||
addHandler(Intent.ACTION_DOCK_EVENT, new DockEventHandler());
|
||||
@@ -376,6 +377,17 @@ public class BluetoothEventManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BatteryLevelChangedHandler implements Handler {
|
||||
public void onReceive(Context context, Intent intent,
|
||||
BluetoothDevice device) {
|
||||
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
|
||||
if (cachedDevice != null) {
|
||||
cachedDevice.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean readPairedDevices() {
|
||||
Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices();
|
||||
if (bondedDevices == null) {
|
||||
|
||||
@@ -419,6 +419,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get battery level from remote device
|
||||
* @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
|
||||
*/
|
||||
public int getBatteryLevel() {
|
||||
return mDevice.getBatteryLevel();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
dispatchAttributesChanged();
|
||||
}
|
||||
@@ -837,7 +845,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|
||||
/**
|
||||
* @return resource for string that discribes the connection state of this device.
|
||||
*/
|
||||
public int getConnectionSummary() {
|
||||
public String getConnectionSummary() {
|
||||
boolean profileConnected = false; // at least one profile is connected
|
||||
boolean a2dpNotConnected = false; // A2DP is preferred but not connected
|
||||
boolean hfpNotConnected = false; // HFP is preferred but not connected
|
||||
@@ -848,7 +856,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|
||||
switch (connectionStatus) {
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
case BluetoothProfile.STATE_DISCONNECTING:
|
||||
return Utils.getConnectionStateSummary(connectionStatus);
|
||||
return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
|
||||
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
profileConnected = true;
|
||||
@@ -868,18 +876,54 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|
||||
}
|
||||
}
|
||||
|
||||
String batteryLevelPercentageString = null;
|
||||
// Android framework should only set mBatteryLevel to valid range [0-100] or
|
||||
// BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
|
||||
// Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
|
||||
// be valid
|
||||
final int batteryLevel = getBatteryLevel();
|
||||
if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
|
||||
// TODO: name com.android.settingslib.bluetooth.Utils something different
|
||||
batteryLevelPercentageString =
|
||||
com.android.settingslib.Utils.formatPercentage(batteryLevel);
|
||||
}
|
||||
|
||||
if (profileConnected) {
|
||||
if (a2dpNotConnected && hfpNotConnected) {
|
||||
return R.string.bluetooth_connected_no_headset_no_a2dp;
|
||||
if (batteryLevelPercentageString != null) {
|
||||
return mContext.getString(
|
||||
R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
|
||||
batteryLevelPercentageString);
|
||||
} else {
|
||||
return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
|
||||
}
|
||||
|
||||
} else if (a2dpNotConnected) {
|
||||
return R.string.bluetooth_connected_no_a2dp;
|
||||
if (batteryLevelPercentageString != null) {
|
||||
return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
|
||||
batteryLevelPercentageString);
|
||||
} else {
|
||||
return mContext.getString(R.string.bluetooth_connected_no_a2dp);
|
||||
}
|
||||
|
||||
} else if (hfpNotConnected) {
|
||||
return R.string.bluetooth_connected_no_headset;
|
||||
if (batteryLevelPercentageString != null) {
|
||||
return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
|
||||
batteryLevelPercentageString);
|
||||
} else {
|
||||
return mContext.getString(R.string.bluetooth_connected_no_headset);
|
||||
}
|
||||
} else {
|
||||
return R.string.bluetooth_connected;
|
||||
if (batteryLevelPercentageString != null) {
|
||||
return mContext.getString(R.string.bluetooth_connected_battery_level,
|
||||
batteryLevelPercentageString);
|
||||
} else {
|
||||
return mContext.getString(R.string.bluetooth_connected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
|
||||
return getBondState() == BluetoothDevice.BOND_BONDING ?
|
||||
mContext.getString(R.string.bluetooth_pairing) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.List;
|
||||
/**
|
||||
* HeadsetProfile handles Bluetooth HFP and Headset profiles.
|
||||
*/
|
||||
public final class HeadsetProfile implements LocalBluetoothProfile {
|
||||
public class HeadsetProfile implements LocalBluetoothProfile {
|
||||
private static final String TAG = "HeadsetProfile";
|
||||
private static boolean V = true;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.List;
|
||||
/**
|
||||
* HidProfile handles Bluetooth HID profile.
|
||||
*/
|
||||
public final class HidProfile implements LocalBluetoothProfile {
|
||||
public class HidProfile implements LocalBluetoothProfile {
|
||||
private static final String TAG = "HidProfile";
|
||||
private static boolean V = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.settingslib.bluetooth;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.TestConfig;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, resourceDir =
|
||||
"../../res")
|
||||
public class CachedBluetoothDeviceTest {
|
||||
@Mock
|
||||
private LocalBluetoothAdapter mAdapter;
|
||||
@Mock
|
||||
private LocalBluetoothProfileManager mProfileManager;
|
||||
@Mock
|
||||
private HeadsetProfile mHfpProfile;
|
||||
@Mock
|
||||
private A2dpProfile mA2dpProfile;
|
||||
@Mock
|
||||
private HidProfile mHidProfile;
|
||||
@Mock
|
||||
private BluetoothDevice mDevice;
|
||||
private CachedBluetoothDevice mCachedDevice;
|
||||
private Context mContext;
|
||||
private int mBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.application;
|
||||
when(mAdapter.getBluetoothState()).thenReturn(BluetoothAdapter.STATE_ON);
|
||||
when(mHfpProfile.isProfileReady()).thenReturn(true);
|
||||
when(mA2dpProfile.isProfileReady()).thenReturn(true);
|
||||
when(mHidProfile.isProfileReady()).thenReturn(true);
|
||||
mCachedDevice = spy(
|
||||
new CachedBluetoothDevice(mContext, mAdapter, mProfileManager, mDevice));
|
||||
doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to verify the current test context object works so that we are not checking null
|
||||
* against null
|
||||
*/
|
||||
@Test
|
||||
public void testContextMock() {
|
||||
assertThat(mContext.getString(R.string.bluetooth_connected)).isEqualTo("Connected");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConnectionSummary_testSingleProfileConnectDisconnect() {
|
||||
// Test without battery level
|
||||
// Set HID profile to be connected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected));
|
||||
|
||||
// Set HID profile to be disconnected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isNull();
|
||||
|
||||
// Test with battery level
|
||||
mBatteryLevel = 10;
|
||||
// Set HID profile to be connected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected_battery_level,
|
||||
com.android.settingslib.Utils.formatPercentage(mBatteryLevel)));
|
||||
|
||||
// Set HID profile to be disconnected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isNull();
|
||||
|
||||
// Test with BluetoothDevice.BATTERY_LEVEL_UNKNOWN battery level
|
||||
mBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
|
||||
|
||||
// Set HID profile to be connected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected));
|
||||
|
||||
// Set HID profile to be disconnected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConnectionSummary_testMultipleProfileConnectDisconnect() {
|
||||
mBatteryLevel = 10;
|
||||
|
||||
// Set HFP, A2DP and HID profile to be connected and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
mCachedDevice.onProfileStateChanged(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected_battery_level,
|
||||
com.android.settingslib.Utils.formatPercentage(mBatteryLevel)));
|
||||
|
||||
// Disconnect HFP only and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected_no_headset_battery_level,
|
||||
com.android.settingslib.Utils.formatPercentage(mBatteryLevel)));
|
||||
|
||||
// Disconnect A2DP only and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_CONNECTED);
|
||||
mCachedDevice.onProfileStateChanged(mA2dpProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected_no_a2dp_battery_level,
|
||||
com.android.settingslib.Utils.formatPercentage(mBatteryLevel)));
|
||||
|
||||
// Disconnect both HFP and A2DP and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(mContext.getString(
|
||||
R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
|
||||
com.android.settingslib.Utils.formatPercentage(mBatteryLevel)));
|
||||
|
||||
// Disconnect all profiles and test connection state summary
|
||||
mCachedDevice.onProfileStateChanged(mHidProfile, BluetoothProfile.STATE_DISCONNECTED);
|
||||
assertThat(mCachedDevice.getConnectionSummary()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import android.widget.Switch;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
@@ -271,7 +272,14 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
|
||||
int state = mController.getMaxConnectionState(device);
|
||||
if (state == BluetoothProfile.STATE_CONNECTED) {
|
||||
item.icon = R.drawable.ic_qs_bluetooth_connected;
|
||||
item.line2 = mContext.getString(R.string.quick_settings_connected);
|
||||
int batteryLevel = device.getBatteryLevel();
|
||||
if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
|
||||
item.line2 = mContext.getString(
|
||||
R.string.quick_settings_connected_battery_level,
|
||||
Utils.formatPercentage(batteryLevel));
|
||||
} else {
|
||||
item.line2 = mContext.getString(R.string.quick_settings_connected);
|
||||
}
|
||||
item.canDisconnect = true;
|
||||
items.add(connectedDevices, item);
|
||||
connectedDevices++;
|
||||
|
||||
Reference in New Issue
Block a user