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

Commit aa63e449 authored by Jakub Tyszkowski's avatar Jakub Tyszkowski
Browse files

MediaControl: Allow basic GATT ops before authorized by LeAudioService

Before authorized by LeAudioService, the device should be able to read
GATT attributes without errors, but will get just some basic inf and
filtered-out data. This is to prevent some of the devices to giving up
on MCP control when they receive the first unathorized error response
for GATT operation on MCS.

Bug: 282938041
Test: atest BluetoothInstrumentationTests
Change-Id: I1bc365ab3a7cff662a7480fa2b895024cdd2c93c
parent 6653a60c
Loading
Loading
Loading
Loading
+198 −16
Original line number Diff line number Diff line
@@ -391,6 +391,119 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
        return mMcpService.getDeviceAuthorization(device);
    }

    private void onUnauthorizedCharRead(BluetoothDevice device, GattOpContext op) {
        UUID charUuid = op.mCharacteristic.getUuid();
        boolean allowToReadRealValue = false;
        byte[] buffer = null;

        if (charUuid.equals(UUID_PLAYER_NAME)) {
            allowToReadRealValue = true;

        } else if (charUuid.equals(UUID_PLAYER_ICON_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_PLAYER_ICON_URL)) {
            ByteBuffer bb = ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN);
            bb.put("".getBytes());
            buffer = bb.array();

        } else if (charUuid.equals(UUID_TRACK_CHANGED)) {
            // No read is available on this characteristic

        } else if (charUuid.equals(UUID_TRACK_TITLE)) {
            ByteBuffer bb = ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN);
            bb.put("".getBytes());
            buffer = bb.array();

        } else if (charUuid.equals(UUID_TRACK_DURATION)) {
            ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
            bb.putInt((int) TRACK_DURATION_UNAVAILABLE);
            buffer = bb.array();

        } else if (charUuid.equals(UUID_TRACK_POSITION)) {
            ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
            bb.putInt((int) TRACK_POSITION_UNAVAILABLE);
            buffer = bb.array();

        } else if (charUuid.equals(UUID_PLAYBACK_SPEED)) {
            ByteBuffer bb = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN);
            bb.put((byte) 1);
            buffer = bb.array();

        } else if (charUuid.equals(UUID_SEEKING_SPEED)) {
            ByteBuffer bb = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN);
            bb.put((byte) 1);
            buffer = bb.array();

        } else if (charUuid.equals(UUID_CURRENT_TRACK_SEGMENT_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_CURRENT_TRACK_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_NEXT_TRACK_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_CURRENT_GROUP_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_PARENT_GROUP_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_PLAYING_ORDER)) {
            ByteBuffer bb = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN);
            bb.put((byte) PlayingOrder.SINGLE_ONCE.getValue());
            buffer = bb.array();

        } else if (charUuid.equals(UUID_PLAYING_ORDER_SUPPORTED)) {
            ByteBuffer bb = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
            bb.putShort((short) SupportedPlayingOrder.SINGLE_ONCE);
            buffer = bb.array();

        } else if (charUuid.equals(UUID_MEDIA_STATE)) {
            allowToReadRealValue = true;

        } else if (charUuid.equals(UUID_MEDIA_CONTROL_POINT)) {
            // No read is available on this characteristic

        } else if (charUuid.equals(UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) {
            allowToReadRealValue = true;

        } else if (charUuid.equals(UUID_SEARCH_RESULT_OBJ_ID)) {
            buffer = objId2ByteArray(-1);

        } else if (charUuid.equals(UUID_SEARCH_CONTROL_POINT)) {
            // No read is available on this characteristic

        } else if (charUuid.equals(UUID_CONTENT_CONTROL_ID)) {
            allowToReadRealValue = true;
        }

        if (allowToReadRealValue) {
            if (op.mCharacteristic.getValue() != null) {
                buffer =
                        Arrays.copyOfRange(
                                op.mCharacteristic.getValue(),
                                op.mOffset,
                                op.mCharacteristic.getValue().length);
            }
        }

        if (buffer != null) {
            mBluetoothGattServer.sendResponse(
                    device, op.mRequestId, BluetoothGatt.GATT_SUCCESS, op.mOffset, buffer);
        } else {
            mEventLogger.loge(
                    TAG, "Missing characteristic value for char: " + mcsUuidToString(charUuid));
            mBluetoothGattServer.sendResponse(
                    device,
                    op.mRequestId,
                    BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH,
                    op.mOffset,
                    buffer);
        }
    }

    private void onUnauthorizedGattOperation(BluetoothDevice device, GattOpContext op) {
        UUID charUuid =
                (op.mCharacteristic != null
@@ -407,6 +520,61 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
                        + ", characteristic= "
                        + (charUuid != null ? mcsUuidToString(charUuid) : "UNKNOWN"));

        switch (op.mOperation) {
            /* Allow not yet authorized devices to subscribe for notifications */
            case READ_DESCRIPTOR:
                if (op.mOffset > 1) {
                    mBluetoothGattServer.sendResponse(
                            device,
                            op.mRequestId,
                            BluetoothGatt.GATT_INVALID_OFFSET,
                            op.mOffset,
                            null);
                    return;
                }

                byte[] value = getCccBytes(device, op.mDescriptor.getCharacteristic().getUuid());
                if (value == null) {
                    mBluetoothGattServer.sendResponse(
                            device, op.mRequestId, BluetoothGatt.GATT_FAILURE, op.mOffset, null);
                    return;
                }

                value = Arrays.copyOfRange(value, op.mOffset, value.length);
                mBluetoothGattServer.sendResponse(
                        device, op.mRequestId, BluetoothGatt.GATT_SUCCESS, op.mOffset, value);
                return;
            case WRITE_DESCRIPTOR:
                int status = BluetoothGatt.GATT_SUCCESS;
                if (op.mPreparedWrite) {
                    status = BluetoothGatt.GATT_FAILURE;
                } else if (op.mOffset > 0) {
                    status = BluetoothGatt.GATT_INVALID_OFFSET;
                } else {
                    status = BluetoothGatt.GATT_SUCCESS;
                    setCcc(
                            device,
                            op.mDescriptor.getCharacteristic().getUuid(),
                            op.mOffset,
                            op.mValue,
                            true);
                }

                if (op.mResponseNeeded) {
                    mBluetoothGattServer.sendResponse(
                            device, op.mRequestId, status, op.mOffset, op.mValue);
                }
                return;
            case READ_CHARACTERISTIC:
                onUnauthorizedCharRead(device, op);
                return;
            case WRITE_CHARACTERISTIC:
                // store as pending operation
                break;
            default:
                break;
        }

        synchronized (mPendingGattOperations) {
            List<GattOpContext> operations = mPendingGattOperations.get(device);
            if (operations == null) {
@@ -914,6 +1082,11 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
        public List<BluetoothDevice> getConnectedDevices() {
            return mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        }

        public boolean isDeviceConnected(BluetoothDevice device) {
            return mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT_SERVER)
                    == BluetoothProfile.STATE_CONNECTED;
        }
    }

    protected MediaControlGattService(McpService mcpService,
@@ -1499,6 +1672,9 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
                                        ? "REJECTED"
                                        : "UNKNOWN")));
        ProcessPendingGattOperations(device);
        for (BluetoothGattCharacteristic characteristic : mCharacteristics.values()) {
            notifyCharacteristic(device, characteristic);
        }
    }

    @Override
@@ -1543,16 +1719,13 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
        }
    }

    private void notifyCharacteristic(@NonNull BluetoothGattCharacteristic characteristic,
            @Nullable BluetoothDevice originDevice) {
        for (BluetoothDevice device : mBluetoothGattServer.getConnectedDevices()) {
            // Skip the origin device who changed the characteristic
            if (device.equals(originDevice)) {
                continue;
            }
    private void notifyCharacteristic(
            @NonNull BluetoothDevice device, @NonNull BluetoothGattCharacteristic characteristic) {
        if (!mBluetoothGattServer.isDeviceConnected(device)) return;
        if (getDeviceAuthorization(device) != BluetoothDevice.ACCESS_ALLOWED) return;

        Map<UUID, Short> charCccMap = mCccDescriptorValues.get(device.getAddress());
            if (charCccMap == null) continue;
        if (charCccMap == null) return;

        byte[] ccc = getCccBytes(device, characteristic.getUuid());
        if (VDBG) {
@@ -1563,13 +1736,22 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
                                + " cccVal= "
                                + ByteBuffer.wrap(ccc).order(ByteOrder.LITTLE_ENDIAN).getShort());
        }

            if (!Arrays.equals(ccc, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) continue;
        if (!Arrays.equals(ccc, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) return;

        if (VDBG) Log.d(TAG, "notifyCharacteristic: sending notification");

        mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
    }

    private void notifyCharacteristic(
            @NonNull BluetoothGattCharacteristic characteristic,
            @Nullable BluetoothDevice originDevice) {
        for (BluetoothDevice device : mBluetoothGattServer.getConnectedDevices()) {
            // Skip the origin device who changed the characteristic
            if (device.equals(originDevice)) {
                continue;
            }
            notifyCharacteristic(device, characteristic);
        }
    }

    private static int SpeedFloatToCharacteristicIntValue(float speed) {
+109 −7
Original line number Diff line number Diff line
@@ -112,15 +112,16 @@ public class MediaControlGattServiceTest {
    private void prepareConnectedDevice() {
        if (mCurrentDevice == null) {
            mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
            List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
            devices.add(mCurrentDevice);
            doReturn(devices).when(mMockGattServer).getConnectedDevices();
            doReturn(true).when(mMockGattServer).isDeviceConnected(eq(mCurrentDevice));
        }
    }

    private void prepareConnectedDevicesCccVal(
            BluetoothGattCharacteristic characteristic, byte[] value) {
        prepareConnectedDevice();
        List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
        devices.add(mCurrentDevice);
        doReturn(devices).when(mMockGattServer).getConnectedDevices();
        mMcpService.setCcc(mCurrentDevice, characteristic.getUuid(), 0, value, true);
    }

@@ -1054,7 +1055,7 @@ public class MediaControlGattServiceTest {
    }

    @Test
    public void testCharacteristicReadUnauthorized() {
    public void testCharacteristicReadRejectedUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattCharacteristic characteristic =
@@ -1074,7 +1075,31 @@ public class MediaControlGattServiceTest {
    }

    @Test
    public void testCharacteristicWriteUnauthorized() {
    public void testCharacteristicReadUnknownUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattCharacteristic characteristic =
                service.getCharacteristic(MediaControlGattService.UUID_TRACK_POSITION);

        prepareConnectedDevice();
        doReturn(BluetoothDevice.ACCESS_UNKNOWN)
                .when(mMockMcpService)
                .getDeviceAuthorization(any(BluetoothDevice.class));

        mMcpService.mServerCallback.onCharacteristicReadRequest(
                mCurrentDevice, 1, 0, characteristic);
        verify(mMockMcpService, times(0)).onDeviceUnauthorized(eq(mCurrentDevice));
        verify(mMockGattServer, times(0))
                .sendResponse(
                        eq(mCurrentDevice),
                        eq(1),
                        eq(BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION),
                        eq(0),
                        any());
    }

    @Test
    public void testCharacteristicWriteRejectedUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();
        int track_position = 100;

@@ -1099,7 +1124,29 @@ public class MediaControlGattServiceTest {
    }

    @Test
    public void testDescriptorReadUnauthorized() {
    public void testCharacteristicWriteUnknownUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();
        int track_position = 100;

        BluetoothGattCharacteristic characteristic =
                service.getCharacteristic(MediaControlGattService.UUID_TRACK_POSITION);

        ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES + 1).order(ByteOrder.LITTLE_ENDIAN);
        bb.putInt((int) track_position);
        bb.put((byte) 0);

        prepareConnectedDevice();
        doReturn(BluetoothDevice.ACCESS_UNKNOWN)
                .when(mMockMcpService)
                .getDeviceAuthorization(any(BluetoothDevice.class));

        mMcpService.mServerCallback.onCharacteristicWriteRequest(
                mCurrentDevice, 1, characteristic, false, true, 0, bb.array());
        verify(mMockMcpService).onDeviceUnauthorized(eq(mCurrentDevice));
    }

    @Test
    public void testDescriptorReadRejectedUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattDescriptor descriptor =
@@ -1120,7 +1167,32 @@ public class MediaControlGattServiceTest {
    }

    @Test
    public void testDescriptorWriteUnauthorized() {
    public void testDescriptorReadUnknownUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattDescriptor descriptor =
                service.getCharacteristic(MediaControlGattService.UUID_TRACK_POSITION)
                        .getDescriptor(UUID_CCCD);
        Assert.assertNotNull(descriptor);

        prepareConnectedDevice();
        doReturn(BluetoothDevice.ACCESS_UNKNOWN)
                .when(mMockMcpService)
                .getDeviceAuthorization(any(BluetoothDevice.class));

        mMcpService.mServerCallback.onDescriptorReadRequest(mCurrentDevice, 1, 0, descriptor);
        verify(mMockMcpService, times(0)).onDeviceUnauthorized(eq(mCurrentDevice));
        verify(mMockGattServer, times(0))
                .sendResponse(
                        eq(mCurrentDevice),
                        eq(1),
                        eq(BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION),
                        eq(0),
                        any());
    }

    @Test
    public void testDescriptorWriteRejectedUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattDescriptor descriptor =
@@ -1145,6 +1217,36 @@ public class MediaControlGattServiceTest {
                        eq(BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION), eq(0), any());
    }

    @Test
    public void testDescriptorWriteUnknownUnauthorized() {
        BluetoothGattService service = initAllFeaturesGattService();

        BluetoothGattDescriptor descriptor =
                service.getCharacteristic(MediaControlGattService.UUID_TRACK_POSITION)
                        .getDescriptor(UUID_CCCD);
        Assert.assertNotNull(descriptor);

        prepareConnectedDevice();
        doReturn(BluetoothDevice.ACCESS_UNKNOWN)
                .when(mMockMcpService)
                .getDeviceAuthorization(any(BluetoothDevice.class));

        ByteBuffer bb = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
        bb.put((byte) 0);
        bb.put((byte) 1);

        mMcpService.mServerCallback.onDescriptorWriteRequest(
                mCurrentDevice, 1, descriptor, false, true, 0, bb.array());
        verify(mMockMcpService, times(0)).onDeviceUnauthorized(eq(mCurrentDevice));
        verify(mMockGattServer, times(0))
                .sendResponse(
                        eq(mCurrentDevice),
                        eq(1),
                        eq(BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION),
                        eq(0),
                        any());
    }

    @Test
    public void testUpdatePlayerNameFromNull() {
        BluetoothGattService service = initAllFeaturesGattService();