diff options
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); |