summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BatteryLevelsInfo.kt28
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java88
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java76
3 files changed, 192 insertions, 0 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BatteryLevelsInfo.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BatteryLevelsInfo.kt
new file mode 100644
index 000000000000..b52a9017fc16
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BatteryLevelsInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 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
+
+/**
+ * BatteryLevelsInfo contains the battery levels of different components of a bluetooth device.
+ * The range of a valid battery level is [0-100], and -1 if the battery level is not applicable.
+ */
+data class BatteryLevelsInfo(
+ val leftBatteryLevel: Int,
+ val rightBatteryLevel: Int,
+ val caseBatteryLevel: Int,
+ val overallBatteryLevel: Int,
+) \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index bb96041739eb..3646842d36ef 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -44,10 +44,12 @@ import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
+import android.view.InputDevice;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
import com.android.internal.util.ArrayUtils;
import com.android.settingslib.R;
@@ -62,6 +64,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.sql.Timestamp;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -150,6 +153,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private boolean mIsHearingAidProfileConnectedFail = false;
private boolean mIsLeAudioProfileConnectedFail = false;
private boolean mUnpairing;
+ @Nullable
+ private final InputDevice mInputDevice;
+ private final boolean mIsDeviceStylus;
// Group second device for Hearing Aid
private CachedBluetoothDevice mSubDevice;
@@ -193,6 +199,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
initDrawableCache();
mUnpairing = false;
+ mInputDevice = BluetoothUtils.getInputDevice(mContext, getAddress());
+ mIsDeviceStylus = BluetoothUtils.isDeviceStylus(mInputDevice, this);
}
/** Clears any pending messages in the message queue. */
@@ -1622,6 +1630,86 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
}
+ /**
+ * Returns the battery levels of all components of the bluetooth device. If no battery info is
+ * available then returns null.
+ */
+ @WorkerThread
+ @Nullable
+ public BatteryLevelsInfo getBatteryLevelsInfo() {
+ // Try getting the battery information from metadata.
+ BatteryLevelsInfo metadataSourceBattery = getBatteryFromMetadata();
+ if (metadataSourceBattery != null) {
+ return metadataSourceBattery;
+ }
+ // Get the battery information from Bluetooth service.
+ return getBatteryFromBluetoothService();
+ }
+
+ @Nullable
+ private BatteryLevelsInfo getBatteryFromMetadata() {
+ if (BluetoothUtils.getBooleanMetaData(mDevice,
+ BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
+ // The device is untethered headset, containing both earbuds and case.
+ int leftBattery =
+ BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
+ int rightBattery =
+ BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
+ int caseBattery =
+ BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY);
+
+ if (leftBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ && rightBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ && caseBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ Log.d(TAG, "No battery info from metadata is available for untethered device "
+ + mDevice.getAnonymizedAddress());
+ return null;
+ } else {
+ int overallBattery =
+ Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery})
+ .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+ .min()
+ .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ Log.d(TAG, "Acquired battery info from metadata for untethered device "
+ + mDevice.getAnonymizedAddress()
+ + " left earbud battery: " + leftBattery
+ + " right earbud battery: " + rightBattery
+ + " case battery: " + caseBattery
+ + " overall battery: " + overallBattery);
+ return new BatteryLevelsInfo(
+ leftBattery, rightBattery, caseBattery, overallBattery);
+ }
+ } else if (mInputDevice != null || mIsDeviceStylus) {
+ // The device is input device, using METADATA_MAIN_BATTERY field to get battery info.
+ int overallBattery = BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_MAIN_BATTERY);
+ if (overallBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ Log.d(TAG, "No battery info from metadata is available for input device "
+ + mDevice.getAnonymizedAddress());
+ return null;
+ } else {
+ Log.d(TAG, "Acquired battery info from metadata for input device "
+ + mDevice.getAnonymizedAddress()
+ + " overall battery: " + overallBattery);
+ return new BatteryLevelsInfo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ overallBattery);
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private BatteryLevelsInfo getBatteryFromBluetoothService() {
+ // TODO(b/397847825): Implement the logic to get battery from Bluetooth service.
+ return null;
+ }
+
private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery,
int lowBatteryColorRes) {
// Since there doesn't seem to be a way to use format strings to add the
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index f6e26a7200ef..ed53d8d04988 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -40,12 +40,14 @@ import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
+import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
import android.util.LruCache;
+import android.view.InputDevice;
import com.android.settingslib.R;
import com.android.settingslib.media.flags.Flags;
@@ -77,8 +79,10 @@ public class CachedBluetoothDeviceTest {
private static final String DEVICE_ALIAS_NEW = "TestAliasNew";
private static final String TWS_BATTERY_LEFT = "15";
private static final String TWS_BATTERY_RIGHT = "25";
+ private static final String TWS_BATTERY_CASE = "10";
private static final String TWS_LOW_BATTERY_THRESHOLD_LOW = "10";
private static final String TWS_LOW_BATTERY_THRESHOLD_HIGH = "25";
+ private static final String MAIN_BATTERY = "80";
private static final String TEMP_BOND_METADATA =
"<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>";
private static final short RSSI_1 = 10;
@@ -87,6 +91,8 @@ public class CachedBluetoothDeviceTest {
private static final boolean JUSTDISCOVERED_2 = false;
private static final int LOW_BATTERY_COLOR = android.R.color.holo_red_dark;
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+ private static final int TEST_DEVICE_ID = 123;
+ private final InputDevice mInputDevice = mock(InputDevice.class);
@Mock
private LocalBluetoothProfileManager mProfileManager;
@Mock
@@ -116,6 +122,8 @@ public class CachedBluetoothDeviceTest {
private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock
private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState;
+ @Mock
+ private InputManager mInputManager;
private CachedBluetoothDevice mCachedDevice;
private CachedBluetoothDevice mSubCachedDevice;
private AudioManager mAudioManager;
@@ -2175,6 +2183,74 @@ public class CachedBluetoothDeviceTest {
assertThat(mCachedDevice.isHearingDevice()).isFalse();
}
+ @Test
+ public void getBatteryLevelsInfo_untetheredHeadsetWithBattery_returnBatteryLevelsInfo() {
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+ "true".getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY)).thenReturn(
+ TWS_BATTERY_LEFT.getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY)).thenReturn(
+ TWS_BATTERY_RIGHT.getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY)).thenReturn(
+ TWS_BATTERY_CASE.getBytes());
+
+ BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
+
+ assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_LEFT));
+ assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_RIGHT));
+ assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_CASE));
+ assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_CASE));
+ }
+
+ @Test
+ public void getBatteryLevelsInfo_inputDeviceWithBattery_returnBatteryLevelsInfo() {
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+ "false".getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
+ MAIN_BATTERY.getBytes());
+ when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
+ when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{TEST_DEVICE_ID});
+ when(mInputManager.getInputDeviceBluetoothAddress(TEST_DEVICE_ID)).thenReturn(
+ DEVICE_ADDRESS);
+ when(mInputManager.getInputDevice(TEST_DEVICE_ID)).thenReturn(mInputDevice);
+
+ BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
+
+ assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo(
+ Integer.parseInt(MAIN_BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevelsInfo_stylusDeviceWithBattery_returnBatteryLevelsInfo() {
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+ "false".getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
+ BluetoothDevice.DEVICE_TYPE_STYLUS.getBytes());
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
+ MAIN_BATTERY.getBytes());
+
+ BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
+
+ assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo(
+ Integer.parseInt(MAIN_BATTERY));
+ }
+
private void updateProfileStatus(LocalBluetoothProfile profile, int status) {
doReturn(status).when(profile).getConnectionStatus(mDevice);
mCachedDevice.onProfileStateChanged(profile, status);