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

Commit fffbaa38 authored by Jack He's avatar Jack He Committed by Andre Eisenbach
Browse files

Support battery level reporting via Plantronics XEVENT (2/2)

* Add parser for Plantronics AT+XEVENT=BATTERY* AT command
* Record the received battery level information through DeviceProperties
* Add unit test for getBatteryLevelFromXEventVsc() and intent handler

Bug: 35874078
Test: make, test with supporting headsets, unit tests
runtest -c com.android.bluetooth.btservice.RemoteDevicesTest bluetooth
Change-Id: I2972c525a43ff0dc415b63fcbf0f08e7bc0b21a8
parent 428fd96f
Loading
Loading
Loading
Loading
+95 −0
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.bluetooth.btservice;
package com.android.bluetooth.btservice;


import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadset;
@@ -65,6 +66,9 @@ final class RemoteDevices {
                case BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED:
                case BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED:
                    onHfIndicatorValueChanged(intent);
                    onHfIndicatorValueChanged(intent);
                    break;
                    break;
                case BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT:
                    onVendorSpecificHeadsetEvent(intent);
                    break;
                default:
                default:
                    Log.w(TAG, "Unhandled intent: " + intent);
                    Log.w(TAG, "Unhandled intent: " + intent);
                    break;
                    break;
@@ -86,6 +90,9 @@ final class RemoteDevices {
    void init() {
    void init() {
        IntentFilter filter = new IntentFilter();
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED);
        filter.addAction(BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED);
        filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
        filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
                + BluetoothAssignedNumbers.PLANTRONICS);
        mAdapterService.registerReceiver(mReceiver, filter);
        mAdapterService.registerReceiver(mReceiver, filter);
    }
    }


@@ -575,6 +582,94 @@ final class RemoteDevices {
        }
        }
    }
    }


    /**
     * Handle {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
     * @param intent must be {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
     */
    @VisibleForTesting
    void onVendorSpecificHeadsetEvent(Intent intent) {
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        if (device == null) {
            Log.e(TAG, "onVendorSpecificHeadsetEvent() remote device is null");
            return;
        }
        String cmd =
                intent.getStringExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
        if (cmd == null) {
            Log.e(TAG, "onVendorSpecificHeadsetEvent() command is null");
            return;
        }
        int cmdType = intent.getIntExtra(
                BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, -1);
        // Only process set command
        if (cmdType != BluetoothHeadset.AT_CMD_TYPE_SET) {
            debugLog("onVendorSpecificHeadsetEvent() only SET command is processed");
            return;
        }
        Object[] args = (Object[]) intent.getExtras().get(
                BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
        if (args == null) {
            Log.e(TAG, "onVendorSpecificHeadsetEvent() arguments are null");
            return;
        }
        int batteryPercent = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        switch (cmd) {
            case BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT:
                batteryPercent = getBatteryLevelFromXEventVsc(args);
                break;
        }
        if (batteryPercent != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
            updateBatteryLevel(device, batteryPercent);
            infoLog("Updated device " + device + " battery level to "
                    + String.valueOf(batteryPercent) + "%");
        }
    }

    /**
     * Parse
     *      AT+XEVENT=BATTERY,[Level],[NumberOfLevel],[MinutesOfTalk],[IsCharging]
     * vendor specific event
     * @param args Array of arguments on the right side of SET command
     * @return Battery level in percents, [0-100], {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
     *         when there is an error parsing the arguments
     */
    @VisibleForTesting
    static int getBatteryLevelFromXEventVsc(Object[] args) {
        if (args.length == 0) {
            Log.w(TAG, "getBatteryLevelFromXEventVsc() empty arguments");
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        Object eventNameObj = args[0];
        if (!(eventNameObj instanceof String)) {
            Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event name");
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        String eventName = (String) eventNameObj;
        if (!eventName.equals(
                    BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL)) {
            infoLog("getBatteryLevelFromXEventVsc() skip none BATTERY event: " + eventName);
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        if (args.length != 5) {
            Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong battery level event length: "
                            + String.valueOf(args.length));
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer)) {
            Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event values");
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        int batteryLevel = (Integer) args[1];
        int numberOfLevels = (Integer) args[2];
        if (batteryLevel < 0 || numberOfLevels < 0 || batteryLevel > numberOfLevels) {
            Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong event value, batteryLevel="
                            + String.valueOf(batteryLevel) + ", numberOfLevels="
                            + String.valueOf(numberOfLevels));
            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
        }
        return batteryLevel * 100 / numberOfLevels;
    }

    private final Handler mHandler = new Handler() {
    private final Handler mHandler = new Handler() {
        @Override
        @Override
        public void handleMessage(Message msg) {
        public void handleMessage(Message msg) {
+6 −2
Original line number Original line Diff line number Diff line
@@ -205,8 +205,12 @@ final class HeadsetStateMachine extends StateMachine {
        classInitNative();
        classInitNative();


        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID = new HashMap<String, Integer>();
        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID = new HashMap<String, Integer>();
        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put("+XEVENT", BluetoothAssignedNumbers.PLANTRONICS);
        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put("+ANDROID", BluetoothAssignedNumbers.GOOGLE);
                BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT,
                BluetoothAssignedNumbers.PLANTRONICS);
        VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
                BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID,
                BluetoothAssignedNumbers.GOOGLE);
    }
    }


    private HeadsetStateMachine(HeadsetService context) {
    private HeadsetStateMachine(HeadsetService context) {
+53 −0
Original line number Original line Diff line number Diff line
@@ -9,6 +9,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.when;


import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadset;
import android.content.Intent;
import android.content.Intent;
@@ -26,6 +27,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;


import java.util.ArrayList;

@RunWith(AndroidJUnit4.class)
@RunWith(AndroidJUnit4.class)
public class RemoteDevicesTest {
public class RemoteDevicesTest {
    private static final String TEST_BT_ADDR_1 = "00:11:22:33:44:55";
    private static final String TEST_BT_ADDR_1 = "00:11:22:33:44:55";
@@ -242,6 +245,33 @@ public class RemoteDevicesTest {
        Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
        Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
    }
    }


    @Test
    public void testOnVendorSpecificHeadsetEvent_testCorrectPlantronicsXEvent() {
        // Verify that device property is null initially
        Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));

        // Verify that correct ACTION_VENDOR_SPECIFIC_HEADSET_EVENT updates battery level
        mRemoteDevices.onVendorSpecificHeadsetEvent(getVendorSpecificHeadsetEventIntent(
                BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT,
                BluetoothAssignedNumbers.PLANTRONICS, BluetoothHeadset.AT_CMD_TYPE_SET,
                getXEventArray(3, 8), mDevice1));
        verify(mAdapterService).sendBroadcast(mIntentArgument.capture(), mStringArgument.capture());
        verfyBatteryLevelChangedIntent(mDevice1, 37, mIntentArgument);
        Assert.assertEquals(AdapterService.BLUETOOTH_PERM, mStringArgument.getValue());
    }

    @Test
    public void testGetBatteryLevelFromXEventVsc() {
        Assert.assertEquals(37, RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(3, 8)));
        Assert.assertEquals(100, RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(1, 1)));
        Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
                RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(3, 1)));
        Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
                RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(-1, 1)));
        Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
                RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(-1, -1)));
    }

    private static void verfyBatteryLevelChangedIntent(
    private static void verfyBatteryLevelChangedIntent(
            BluetoothDevice device, int batteryLevel, ArgumentCaptor<Intent> intentArgument) {
            BluetoothDevice device, int batteryLevel, ArgumentCaptor<Intent> intentArgument) {
        Assert.assertEquals(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED,
        Assert.assertEquals(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED,
@@ -262,4 +292,27 @@ public class RemoteDevicesTest {
        intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, batteryLevel);
        intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, batteryLevel);
        return intent;
        return intent;
    }
    }

    private static Intent getVendorSpecificHeadsetEventIntent(String command, int companyId,
            int commandType, Object[] arguments, BluetoothDevice device) {
        Intent intent = new Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
        intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, command);
        intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, commandType);
        // assert: all elements of args are Serializable
        intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        intent.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
                + Integer.toString(companyId));
        return intent;
    }

    private static Object[] getXEventArray(int batteryLevel, int numLevels) {
        ArrayList<Object> list = new ArrayList<>();
        list.add(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL);
        list.add(batteryLevel);
        list.add(numLevels);
        list.add(0);
        list.add(0);
        return list.toArray();
    }
}
}