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:
Jack He
2017-06-29 17:01:23 -07:00
parent 386d8133c4
commit 6258aae548
6 changed files with 229 additions and 10 deletions

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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++;