Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9bec9819 authored by Ze Li's avatar Ze Li
Browse files

[Battery refactor] Function to get battery information from metadata.

Test: com.android.settingslib.bluetooth.CachedBluetoothDeviceTest
Bug: 397847825
Flag: EXEMPT utils function
Change-Id: I4b769efc19a968f4f7f1aaf5e8425210c3e55cb8
parent fed08931
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
/*
 * 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
+88 −0
Original line number Diff line number Diff line
@@ -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
+76 −0
Original line number Diff line number Diff line
@@ -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);