| /* |
| * Copyright 2021 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * 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.bluetooth.mcp; |
| |
| import static android.bluetooth.BluetoothDevice.METADATA_GMCS_CCCD; |
| import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED; |
| import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED; |
| import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY; |
| import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ; |
| import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE; |
| import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothGatt; |
| import android.bluetooth.BluetoothGattCharacteristic; |
| import android.bluetooth.BluetoothGattDescriptor; |
| import android.bluetooth.BluetoothGattServer; |
| import android.bluetooth.BluetoothGattServerCallback; |
| import android.bluetooth.BluetoothGattService; |
| import android.bluetooth.BluetoothManager; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.IBluetoothManager; |
| import android.bluetooth.IBluetoothStateChangeCallback; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.ParcelUuid; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import com.android.bluetooth.BluetoothEventLogger; |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.a2dp.A2dpService; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.hearingaid.HearingAidService; |
| import com.android.bluetooth.le_audio.LeAudioService; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.UUID; |
| |
| /** |
| * This implements Media Control Service object which is given back to the app who registers a new |
| * MCS instance through the MCS Service Manager. It has no higher level logic to control the media |
| * player itself, thus can be used either as an MCS or a single-instance GMCS. It implements only |
| * the GATT Service logic, allowing the higher level layer to control the service state and react to |
| * bluetooth peer device requests through the method calls and callback mechanism. |
| * |
| * Implemented according to Media Control Service v1.0 specification. |
| */ |
| public class MediaControlGattService implements MediaControlGattServiceInterface { |
| private static final String TAG = "MediaControlGattService"; |
| private static final boolean DBG = Log.isLoggable(TAG, Log.INFO); |
| private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); |
| |
| /* MCS assigned UUIDs */ |
| public static final UUID UUID_PLAYER_NAME = |
| UUID.fromString("00002b93-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PLAYER_ICON_OBJ_ID = |
| UUID.fromString("00002b94-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PLAYER_ICON_URL = |
| UUID.fromString("00002b95-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_TRACK_CHANGED = |
| UUID.fromString("00002b96-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_TRACK_TITLE = |
| UUID.fromString("00002b97-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_TRACK_DURATION = |
| UUID.fromString("00002b98-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_TRACK_POSITION = |
| UUID.fromString("00002b99-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PLAYBACK_SPEED = |
| UUID.fromString("00002b9a-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_SEEKING_SPEED = |
| UUID.fromString("00002b9b-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_CURRENT_TRACK_SEGMENT_OBJ_ID = |
| UUID.fromString("00002b9c-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_CURRENT_TRACK_OBJ_ID = |
| UUID.fromString("00002b9d-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_NEXT_TRACK_OBJ_ID = |
| UUID.fromString("00002b9e-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_CURRENT_GROUP_OBJ_ID = |
| UUID.fromString("00002b9f-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PARENT_GROUP_OBJ_ID = |
| UUID.fromString("00002ba0-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PLAYING_ORDER = |
| UUID.fromString("00002ba1-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_PLAYING_ORDER_SUPPORTED = |
| UUID.fromString("00002ba2-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_MEDIA_STATE = |
| UUID.fromString("00002ba3-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_MEDIA_CONTROL_POINT = |
| UUID.fromString("00002ba4-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED = |
| UUID.fromString("00002ba5-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_SEARCH_RESULT_OBJ_ID = |
| UUID.fromString("00002ba6-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_SEARCH_CONTROL_POINT = |
| UUID.fromString("00002ba7-0000-1000-8000-00805f9b34fb"); |
| public static final UUID UUID_CONTENT_CONTROL_ID = |
| UUID.fromString("00002bba-0000-1000-8000-00805f9b34fb"); |
| |
| private static final byte SEARCH_CONTROL_POINT_RESULT_SUCCESS = 0x01; |
| private static final byte SEARCH_CONTROL_POINT_RESULT_FAILURE = 0x02; |
| |
| private static final float PLAY_SPEED_MIN = 0.25f; |
| private static final float PLAY_SPEED_MAX = 3.957f; |
| |
| private static final int INTERVAL_UNAVAILABLE = 0xFFFFFFFF; |
| |
| private final int mCcid; |
| private Map<String, Map<UUID, Short>> mCccDescriptorValues = new HashMap<>(); |
| private long mFeatures; |
| private Context mContext; |
| private MediaControlServiceCallbacks mCallbacks; |
| private BluetoothGattServerProxy mBluetoothGattServer; |
| private BluetoothGattService mGattService = null; |
| private Handler mHandler = new Handler(Looper.getMainLooper()); |
| private Map<Integer, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>(); |
| private MediaState mCurrentMediaState = MediaState.INACTIVE; |
| private Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations = new HashMap<>(); |
| private McpService mMcpService; |
| private LeAudioService mLeAudioService; |
| private AdapterService mAdapterService; |
| |
| private static final int LOG_NB_EVENTS = 200; |
| private final BluetoothEventLogger mEventLogger; |
| |
| private static String mcsUuidToString(UUID uuid) { |
| if (uuid.equals(UUID_PLAYER_NAME)) { |
| return "PLAYER_NAME"; |
| } else if (uuid.equals(UUID_PLAYER_ICON_OBJ_ID)) { |
| return "PLAYER_ICON_OBJ_ID"; |
| } else if (uuid.equals(UUID_PLAYER_ICON_URL)) { |
| return "PLAYER_ICON_URL"; |
| } else if (uuid.equals(UUID_TRACK_CHANGED)) { |
| return "TRACK_CHANGED"; |
| } else if (uuid.equals(UUID_TRACK_TITLE)) { |
| return "TRACK_TITLE"; |
| } else if (uuid.equals(UUID_TRACK_DURATION)) { |
| return "TRACK_DURATION"; |
| } else if (uuid.equals(UUID_TRACK_POSITION)) { |
| return "TRACK_POSITION"; |
| } else if (uuid.equals(UUID_PLAYBACK_SPEED)) { |
| return "PLAYBACK_SPEED"; |
| } else if (uuid.equals(UUID_SEEKING_SPEED)) { |
| return "SEEKING_SPEED"; |
| } else if (uuid.equals(UUID_CURRENT_TRACK_SEGMENT_OBJ_ID)) { |
| return "CURRENT_TRACK_SEGMENT_OBJ_ID"; |
| } else if (uuid.equals(UUID_CURRENT_TRACK_OBJ_ID)) { |
| return "CURRENT_TRACK_OBJ_ID"; |
| } else if (uuid.equals(UUID_NEXT_TRACK_OBJ_ID)) { |
| return "NEXT_TRACK_OBJ_ID"; |
| } else if (uuid.equals(UUID_CURRENT_GROUP_OBJ_ID)) { |
| return "CURRENT_GROUP_OBJ_ID"; |
| } else if (uuid.equals(UUID_PARENT_GROUP_OBJ_ID)) { |
| return "PARENT_GROUP_OBJ_ID"; |
| } else if (uuid.equals(UUID_PLAYING_ORDER)) { |
| return "PLAYING_ORDER"; |
| } else if (uuid.equals(UUID_PLAYING_ORDER_SUPPORTED)) { |
| return "PLAYING_ORDER_SUPPORTED"; |
| } else if (uuid.equals(UUID_MEDIA_STATE)) { |
| return "MEDIA_STATE"; |
| } else if (uuid.equals(UUID_MEDIA_CONTROL_POINT)) { |
| return "MEDIA_CONTROL_POINT"; |
| } else if (uuid.equals(UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { |
| return "MEDIA_CONTROL_POINT_OPCODES_SUPPORTED"; |
| } else if (uuid.equals(UUID_SEARCH_RESULT_OBJ_ID)) { |
| return "SEARCH_RESULT_OBJ_ID"; |
| } else if (uuid.equals(UUID_SEARCH_CONTROL_POINT)) { |
| return "SEARCH_CONTROL_POINT"; |
| } else if (uuid.equals(UUID_CONTENT_CONTROL_ID)) { |
| return "CONTENT_CONTROL_ID"; |
| } else { |
| return "UNKNOWN(" + uuid + ")"; |
| } |
| } |
| |
| private static class GattOpContext { |
| public enum Operation { |
| READ_CHARACTERISTIC, |
| WRITE_CHARACTERISTIC, |
| READ_DESCRIPTOR, |
| WRITE_DESCRIPTOR, |
| } |
| |
| GattOpContext(Operation operation, int requestId, |
| BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, |
| boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { |
| mOperation = operation; |
| mRequestId = requestId; |
| mCharacteristic = characteristic; |
| mDescriptor = descriptor; |
| mPreparedWrite = preparedWrite; |
| mResponseNeeded = responseNeeded; |
| mOffset = offset; |
| mValue = value; |
| } |
| |
| GattOpContext(Operation operation, int requestId, |
| BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor) { |
| mOperation = operation; |
| mRequestId = requestId; |
| mCharacteristic = characteristic; |
| mDescriptor = descriptor; |
| mPreparedWrite = false; |
| mResponseNeeded = false; |
| mOffset = 0; |
| mValue = null; |
| } |
| |
| public Operation mOperation; |
| public int mRequestId; |
| public BluetoothGattCharacteristic mCharacteristic; |
| public BluetoothGattDescriptor mDescriptor; |
| public boolean mPreparedWrite; |
| public boolean mResponseNeeded; |
| public int mOffset; |
| public byte[] mValue; |
| } |
| |
| private final Map<UUID, CharacteristicWriteHandler> mCharWriteCallback = Map.of( |
| UUID_TRACK_POSITION, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "TRACK_POSITION write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| if (value.length == 4) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| ByteBuffer bb = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN); |
| handleTrackPositionRequest(bb.getInt()); |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_PLAYBACK_SPEED, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "PLAYBACK_SPEED write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| if (value.length == 1) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| |
| Integer intVal = characteristic.getIntValue( |
| BluetoothGattCharacteristic.FORMAT_SINT8, 0); |
| // Don't bother player with the same value |
| if (intVal == value[0]) { |
| notifyCharacteristic(characteristic, null); |
| } else { |
| handlePlaybackSpeedRequest(value[0]); |
| } |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_CURRENT_TRACK_OBJ_ID, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "CURRENT_TRACK_OBJ_ID write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| if (value.length == 6) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| handleObjectIdRequest( |
| ObjectIds.CURRENT_TRACK_OBJ_ID, byteArray2ObjId(value)); |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_NEXT_TRACK_OBJ_ID, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "NEXT_TRACK_OBJ_ID write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| if (value.length == 6) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| handleObjectIdRequest(ObjectIds.NEXT_TRACK_OBJ_ID, byteArray2ObjId(value)); |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_CURRENT_GROUP_OBJ_ID, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "CURRENT_GROUP_OBJ_ID write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| if (value.length == 6) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| handleObjectIdRequest( |
| ObjectIds.CURRENT_GROUP_OBJ_ID, byteArray2ObjId(value)); |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_PLAYING_ORDER, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "PLAYING_ORDER write request"); |
| } |
| int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| Integer currentPlayingOrder = null; |
| |
| if (characteristic.getValue() != null) { |
| currentPlayingOrder = characteristic.getIntValue( |
| BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| } |
| |
| if (value.length == 1 |
| && (currentPlayingOrder == null || currentPlayingOrder != value[0])) { |
| status = BluetoothGatt.GATT_SUCCESS; |
| BluetoothGattCharacteristic supportedPlayingOrderChar = |
| mCharacteristics.get(CharId.PLAYING_ORDER_SUPPORTED); |
| Integer supportedPlayingOrder = |
| supportedPlayingOrderChar.getIntValue( |
| BluetoothGattCharacteristic.FORMAT_UINT16, 0); |
| |
| if ((supportedPlayingOrder & (1 << (value[0] - 1))) != 0) { |
| handlePlayingOrderRequest(value[0]); |
| } |
| } |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_MEDIA_CONTROL_POINT, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "MEDIA_CONTROL_POINT write request"); |
| } |
| int status = handleMediaControlPointRequest(device, value); |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| }, |
| UUID_SEARCH_CONTROL_POINT, |
| (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { |
| if (VDBG) { |
| Log.d(TAG, "SEARCH_CONTROL_POINT write request"); |
| } |
| // TODO: There is no Object Trasfer Service implementation. |
| if (responseNeeded) { |
| mBluetoothGattServer.sendResponse(device, requestId, 0, offset, value); |
| } |
| }); |
| |
| private long millisecondsToMcsInterval(long interval) { |
| /* MCS presents time in 0.01s intervals */ |
| return interval / 10; |
| } |
| |
| private long mcsIntervalToMilliseconds(long interval) { |
| /* MCS presents time in 0.01s intervals */ |
| return interval * 10L; |
| } |
| |
| private int getDeviceAuthorization(BluetoothDevice device) { |
| 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 |
| ? op.mCharacteristic.getUuid() |
| : (op.mDescriptor != null |
| ? op.mDescriptor.getCharacteristic().getUuid() |
| : null)); |
| mEventLogger.logw( |
| TAG, |
| "onUnauthorizedGattOperation: device= " |
| + device |
| + ", opcode= " |
| + op.mOperation |
| + ", 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) { |
| operations = new ArrayList<>(); |
| mPendingGattOperations.put(device, operations); |
| } |
| |
| operations.add(op); |
| // Send authorization request for each device only for it's first GATT request |
| if (operations.size() == 1) { |
| mMcpService.onDeviceUnauthorized(device); |
| } |
| } |
| } |
| |
| private void onAuthorizedGattOperation(BluetoothDevice device, GattOpContext op) { |
| UUID charUuid = |
| (op.mCharacteristic != null |
| ? op.mCharacteristic.getUuid() |
| : (op.mDescriptor != null |
| ? op.mDescriptor.getCharacteristic().getUuid() |
| : null)); |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "onAuthorizedGattOperation: device= " |
| + device |
| + ", opcode= " |
| + op.mOperation |
| + ", characteristic= " |
| + (charUuid != null ? mcsUuidToString(charUuid) : "UNKNOWN")); |
| |
| int status = BluetoothGatt.GATT_SUCCESS; |
| |
| switch (op.mOperation) { |
| case READ_CHARACTERISTIC: |
| // Always ask for the latest position |
| if (op.mCharacteristic.getUuid().equals( |
| mCharacteristics.get(CharId.TRACK_POSITION).getUuid())) { |
| long positionMs = TRACK_POSITION_UNAVAILABLE; |
| positionMs = mCallbacks.onGetCurrentTrackPosition(); |
| final int position = |
| (positionMs != TRACK_POSITION_UNAVAILABLE) |
| ? (int) millisecondsToMcsInterval(positionMs) |
| : INTERVAL_UNAVAILABLE; |
| |
| ByteBuffer bb = |
| ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); |
| bb.putInt(position); |
| |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_SUCCESS, op.mOffset, |
| Arrays.copyOfRange(bb.array(), op.mOffset, Integer.BYTES)); |
| return; |
| } |
| |
| if (op.mCharacteristic.getValue() != null) { |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_SUCCESS, op.mOffset, |
| Arrays.copyOfRange(op.mCharacteristic.getValue(), op.mOffset, |
| op.mCharacteristic.getValue().length)); |
| } else { |
| Log.e(TAG, |
| "Missing characteristic value for char: " |
| + op.mCharacteristic.getUuid()); |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH, op.mOffset, new byte[]{}); |
| } |
| break; |
| |
| case WRITE_CHARACTERISTIC: |
| if (op.mPreparedWrite) { |
| status = BluetoothGatt.GATT_FAILURE; |
| } else if (op.mOffset > 0) { |
| status = BluetoothGatt.GATT_INVALID_OFFSET; |
| } else { |
| CharacteristicWriteHandler handler = |
| mCharWriteCallback.get(op.mCharacteristic.getUuid()); |
| handler.onCharacteristicWriteRequest( |
| device, op.mRequestId, op.mCharacteristic, op.mPreparedWrite, |
| op.mResponseNeeded, op.mOffset, op.mValue); |
| break; |
| } |
| |
| if (op.mResponseNeeded) { |
| mBluetoothGattServer.sendResponse( |
| device, op.mRequestId, status, op.mOffset, op.mValue); |
| } |
| break; |
| |
| case READ_DESCRIPTOR: |
| if (op.mOffset > 1) { |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_INVALID_OFFSET, op.mOffset, null); |
| break; |
| } |
| |
| byte[] value = getCccBytes(device, op.mDescriptor.getCharacteristic().getUuid()); |
| if (value == null) { |
| mBluetoothGattServer.sendResponse( |
| device, op.mRequestId, BluetoothGatt.GATT_FAILURE, op.mOffset, null); |
| break; |
| } |
| |
| value = Arrays.copyOfRange(value, op.mOffset, value.length); |
| mBluetoothGattServer.sendResponse( |
| device, op.mRequestId, BluetoothGatt.GATT_SUCCESS, op.mOffset, value); |
| break; |
| |
| case WRITE_DESCRIPTOR: |
| 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); |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| private void onRejectedAuthorizationGattOperation(BluetoothDevice device, GattOpContext op) { |
| UUID charUuid = |
| (op.mCharacteristic != null |
| ? op.mCharacteristic.getUuid() |
| : (op.mDescriptor != null |
| ? op.mDescriptor.getCharacteristic().getUuid() |
| : null)); |
| mEventLogger.logw( |
| TAG, |
| "onRejectedAuthorizationGattOperation: device= " |
| + device |
| + ", opcode= " |
| + op.mOperation |
| + ", characteristic= " |
| + (charUuid != null ? mcsUuidToString(charUuid) : "UNKNOWN")); |
| |
| switch (op.mOperation) { |
| case READ_CHARACTERISTIC: |
| case READ_DESCRIPTOR: |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); |
| break; |
| case WRITE_CHARACTERISTIC: |
| if (op.mResponseNeeded) { |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); |
| } else { |
| // In case of control point operations we can send an application error code |
| if (op.mCharacteristic.getUuid().equals(UUID_MEDIA_CONTROL_POINT)) { |
| setMediaControlRequestResult( |
| new Request(op.mValue[0], 0), |
| Request.Results.COMMAND_CANNOT_BE_COMPLETED); |
| } else if (op.mCharacteristic.getUuid().equals(UUID_SEARCH_CONTROL_POINT)) { |
| setSearchRequestResult(null, SearchRequest.Results.FAILURE, 0); |
| } |
| } |
| break; |
| case WRITE_DESCRIPTOR: |
| if (op.mResponseNeeded) { |
| mBluetoothGattServer.sendResponse(device, op.mRequestId, |
| BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| private void ClearUnauthorizedGattOperations(BluetoothDevice device) { |
| if (VDBG) { |
| Log.d(TAG, "ClearUnauthorizedGattOperations: device= " + device); |
| } |
| |
| synchronized (mPendingGattOperations) { |
| mPendingGattOperations.remove(device); |
| } |
| } |
| |
| private void ProcessPendingGattOperations(BluetoothDevice device) { |
| if (VDBG) { |
| Log.d(TAG, "ProcessPendingGattOperations: device= " + device); |
| } |
| |
| synchronized (mPendingGattOperations) { |
| if (mPendingGattOperations.containsKey(device)) { |
| if (getDeviceAuthorization(device) == BluetoothDevice.ACCESS_ALLOWED) { |
| for (GattOpContext op : mPendingGattOperations.get(device)) { |
| onAuthorizedGattOperation(device, op); |
| } |
| } else { |
| for (GattOpContext op : mPendingGattOperations.get(device)) { |
| onRejectedAuthorizationGattOperation(device, op); |
| } |
| } |
| ClearUnauthorizedGattOperations(device); |
| } |
| } |
| } |
| |
| private void restoreCccValuesForStoredDevices() { |
| for (BluetoothDevice device : mAdapterService.getBondedDevices()) { |
| byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); |
| |
| if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { |
| return; |
| } |
| |
| List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)); |
| |
| /* Restore CCCD values for device */ |
| for (ParcelUuid uuid : uuidList) { |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "restoreCccValuesForStoredDevices: device= " + device + ", char= " + uuid); |
| setCcc(device, uuid.getUuid(), 0, |
| BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, false); |
| } |
| } |
| } |
| |
| private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = |
| new IBluetoothStateChangeCallback.Stub() { |
| public void onBluetoothStateChange(boolean up) { |
| if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); |
| if (up) { |
| restoreCccValuesForStoredDevices(); |
| } |
| } |
| }; |
| |
| @VisibleForTesting |
| final BluetoothGattServerCallback mServerCallback = new BluetoothGattServerCallback() { |
| @Override |
| public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { |
| super.onConnectionStateChange(device, status, newState); |
| if (VDBG) { |
| Log.d(TAG, "BluetoothGattServerCallback: onConnectionStateChange"); |
| } |
| if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
| ClearUnauthorizedGattOperations(device); |
| } |
| } |
| |
| @Override |
| public void onServiceAdded(int status, BluetoothGattService service) { |
| super.onServiceAdded(status, service); |
| if (VDBG) { |
| Log.d(TAG, "BluetoothGattServerCallback: onServiceAdded"); |
| } |
| |
| if (mCallbacks != null) { |
| mCallbacks.onServiceInstanceRegistered((status != BluetoothGatt.GATT_SUCCESS) |
| ? ServiceStatus.UNKNOWN_ERROR |
| : ServiceStatus.OK, |
| MediaControlGattService.this); |
| } |
| |
| mCharacteristics.get(CharId.CONTENT_CONTROL_ID) |
| .setValue(mCcid, BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| restoreCccValuesForStoredDevices(); |
| setInitialCharacteristicValuesAndNotify(); |
| initialStateRequest(); |
| } |
| |
| @Override |
| public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, |
| BluetoothGattCharacteristic characteristic) { |
| super.onCharacteristicReadRequest(device, requestId, offset, characteristic); |
| if (VDBG) { |
| Log.d(TAG, "BluetoothGattServerCallback: onCharacteristicReadRequest offset= " |
| + offset + " entire value= " + Arrays.toString(characteristic.getValue())); |
| } |
| |
| if ((characteristic.getProperties() & PROPERTY_READ) == 0) { |
| mBluetoothGattServer.sendResponse(device, requestId, |
| BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); |
| return; |
| } |
| |
| GattOpContext op = new GattOpContext( |
| GattOpContext.Operation.READ_CHARACTERISTIC, requestId, characteristic, null); |
| switch (getDeviceAuthorization(device)) { |
| case BluetoothDevice.ACCESS_REJECTED: |
| onRejectedAuthorizationGattOperation(device, op); |
| break; |
| case BluetoothDevice.ACCESS_UNKNOWN: |
| onUnauthorizedGattOperation(device, op); |
| break; |
| default: |
| onAuthorizedGattOperation(device, op); |
| break; |
| } |
| } |
| |
| @Override |
| public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, |
| BluetoothGattCharacteristic characteristic, boolean preparedWrite, |
| boolean responseNeeded, int offset, byte[] value) { |
| super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, |
| responseNeeded, offset, value); |
| if (VDBG) { |
| Log.d(TAG, |
| "BluetoothGattServerCallback: " |
| + "onCharacteristicWriteRequest"); |
| } |
| |
| if ((characteristic.getProperties() & PROPERTY_WRITE) |
| == 0) { |
| mBluetoothGattServer.sendResponse( |
| device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value); |
| return; |
| } |
| |
| GattOpContext op = new GattOpContext(GattOpContext.Operation.WRITE_CHARACTERISTIC, |
| requestId, characteristic, null, preparedWrite, responseNeeded, offset, value); |
| switch (getDeviceAuthorization(device)) { |
| case BluetoothDevice.ACCESS_REJECTED: |
| onRejectedAuthorizationGattOperation(device, op); |
| break; |
| case BluetoothDevice.ACCESS_UNKNOWN: |
| onUnauthorizedGattOperation(device, op); |
| break; |
| default: |
| onAuthorizedGattOperation(device, op); |
| break; |
| } |
| } |
| |
| @Override |
| public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, |
| BluetoothGattDescriptor descriptor) { |
| super.onDescriptorReadRequest(device, requestId, offset, descriptor); |
| if (VDBG) { |
| Log.d(TAG, |
| "BluetoothGattServerCallback: " |
| + "onDescriptorReadRequest"); |
| } |
| |
| if ((descriptor.getPermissions() & BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED) |
| == 0) { |
| mBluetoothGattServer.sendResponse( |
| device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset, null); |
| return; |
| } |
| |
| GattOpContext op = new GattOpContext( |
| GattOpContext.Operation.READ_DESCRIPTOR, requestId, null, descriptor); |
| switch (getDeviceAuthorization(device)) { |
| case BluetoothDevice.ACCESS_REJECTED: |
| onRejectedAuthorizationGattOperation(device, op); |
| break; |
| case BluetoothDevice.ACCESS_UNKNOWN: |
| onUnauthorizedGattOperation(device, op); |
| break; |
| default: |
| onAuthorizedGattOperation(device, op); |
| break; |
| } |
| } |
| |
| @Override |
| public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, |
| BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, |
| int offset, byte[] value) { |
| super.onDescriptorWriteRequest( |
| device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); |
| if (VDBG) { |
| Log.d(TAG, |
| "BluetoothGattServerCallback: " |
| + "onDescriptorWriteRequest"); |
| } |
| |
| if ((descriptor.getPermissions() & BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED) |
| == 0) { |
| mBluetoothGattServer.sendResponse( |
| device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset, value); |
| return; |
| } |
| |
| GattOpContext op = new GattOpContext(GattOpContext.Operation.WRITE_DESCRIPTOR, |
| requestId, null, descriptor, preparedWrite, responseNeeded, offset, value); |
| switch (getDeviceAuthorization(device)) { |
| case BluetoothDevice.ACCESS_REJECTED: |
| onRejectedAuthorizationGattOperation(device, op); |
| break; |
| case BluetoothDevice.ACCESS_UNKNOWN: |
| onUnauthorizedGattOperation(device, op); |
| break; |
| default: |
| onAuthorizedGattOperation(device, op); |
| break; |
| } |
| } |
| }; |
| |
| private void initialStateRequest() { |
| List<PlayerStateField> field_list = new ArrayList<>(); |
| |
| if (isFeatureSupported(ServiceFeature.MEDIA_STATE)) { |
| field_list.add(PlayerStateField.PLAYBACK_STATE); |
| } |
| |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { |
| field_list.add(PlayerStateField.ICON_URL); |
| } |
| |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { |
| field_list.add(PlayerStateField.ICON_OBJ_ID); |
| } |
| |
| if (isFeatureSupported(ServiceFeature.PLAYER_NAME)) { |
| field_list.add(PlayerStateField.PLAYER_NAME); |
| } |
| |
| if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { |
| field_list.add(PlayerStateField.PLAYING_ORDER_SUPPORTED); |
| } |
| |
| mCallbacks.onPlayerStateRequest(field_list.stream().toArray(PlayerStateField[]::new)); |
| } |
| |
| private void setInitialCharacteristicValues(boolean notify) { |
| mEventLogger.logd(DBG, TAG, "setInitialCharacteristicValues"); |
| updateMediaStateChar(mCurrentMediaState.getValue()); |
| updatePlayerNameChar("", notify); |
| updatePlayerIconUrlChar(""); |
| |
| // Object IDs will have a length of 0; |
| updateObjectID(ObjectIds.PLAYER_ICON_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.CURRENT_TRACK_SEGMENT_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.CURRENT_TRACK_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.NEXT_TRACK_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.CURRENT_GROUP_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.PARENT_GROUP_OBJ_ID, -1, notify); |
| updateObjectID(ObjectIds.SEARCH_RESULT_OBJ_ID, -1, notify); |
| updateTrackTitleChar("", notify); |
| updateTrackDurationChar(TRACK_DURATION_UNAVAILABLE, notify); |
| updateTrackPositionChar(TRACK_POSITION_UNAVAILABLE, notify); |
| updatePlaybackSpeedChar(1, notify); |
| updateSeekingSpeedChar(1, notify); |
| updatePlayingOrderSupportedChar(SupportedPlayingOrder.SINGLE_ONCE); |
| updatePlayingOrderChar(PlayingOrder.SINGLE_ONCE, notify); |
| updateSupportedOpcodesChar(Request.SupportedOpcodes.NONE, notify); |
| } |
| |
| private void setInitialCharacteristicValues() { |
| setInitialCharacteristicValues(false); |
| } |
| |
| private void setInitialCharacteristicValuesAndNotify() { |
| setInitialCharacteristicValues(true); |
| } |
| |
| /** |
| * A proxy class that facilitates testing of the McpService class. |
| * |
| * This is necessary due to the "final" attribute of the BluetoothGattServer class. In order to |
| * test the correct functioning of the McpService class, the final class must be put into a |
| * container that can be mocked correctly. |
| */ |
| public class BluetoothGattServerProxy { |
| private BluetoothGattServer mBluetoothGattServer; |
| private BluetoothManager mBluetoothManager; |
| |
| public BluetoothGattServerProxy(BluetoothGattServer gatt, BluetoothManager manager) { |
| mBluetoothManager = manager; |
| mBluetoothGattServer = gatt; |
| } |
| |
| public boolean addService(BluetoothGattService service) { |
| return mBluetoothGattServer.addService(service); |
| } |
| |
| public boolean removeService(BluetoothGattService service) { |
| return mBluetoothGattServer.removeService(service); |
| } |
| |
| public void close() { |
| mBluetoothGattServer.close(); |
| } |
| |
| public boolean sendResponse( |
| BluetoothDevice device, int requestId, int status, int offset, byte[] value) { |
| return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); |
| } |
| |
| public boolean notifyCharacteristicChanged(BluetoothDevice device, |
| BluetoothGattCharacteristic characteristic, boolean confirm) { |
| return mBluetoothGattServer.notifyCharacteristicChanged( |
| device, characteristic, confirm); |
| } |
| |
| 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, |
| @NonNull MediaControlServiceCallbacks callbacks, int ccid) { |
| mContext = mcpService; |
| mCallbacks = callbacks; |
| mCcid = ccid; |
| |
| mMcpService = mcpService; |
| mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), |
| "AdapterService shouldn't be null when creating MediaControlCattService"); |
| |
| IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager(); |
| if (mgr != null) { |
| try { |
| mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| } |
| |
| mEventLogger = |
| new BluetoothEventLogger( |
| LOG_NB_EVENTS, TAG + " instance (CCID= " + ccid + ") event log"); |
| } |
| |
| protected boolean init(UUID scvUuid) { |
| mFeatures = mCallbacks.onGetFeatureFlags(); |
| |
| // Verify the minimum required set of supported player features |
| if ((mFeatures & ServiceFeature.ALL_MANDATORY_SERVICE_FEATURES) |
| != ServiceFeature.ALL_MANDATORY_SERVICE_FEATURES) { |
| mCallbacks.onServiceInstanceRegistered(ServiceStatus.INVALID_FEATURE_FLAGS, null); |
| return false; |
| } |
| |
| mEventLogger.add("Initializing"); |
| |
| // Init attribute database |
| return initGattService(scvUuid); |
| } |
| |
| private void handleObjectIdRequest(int objField, long objId) { |
| mEventLogger.add("handleObjectIdRequest: obj= " + objField + ", objId= " + objId); |
| mCallbacks.onSetObjectIdRequest(objField, objId); |
| } |
| |
| private void handlePlayingOrderRequest(int order) { |
| mEventLogger.add("handlePlayingOrderRequest: order= " + order); |
| mCallbacks.onPlayingOrderSetRequest(order); |
| } |
| |
| private void handlePlaybackSpeedRequest(int speed) { |
| float floatingSpeed = (float) Math.pow(2, speed / 64); |
| mEventLogger.add("handlePlaybackSpeedRequest: floatingSpeed= " + floatingSpeed); |
| mCallbacks.onPlaybackSpeedSetRequest(floatingSpeed); |
| } |
| |
| private void handleTrackPositionRequest(long position) { |
| final long positionMs = (position != INTERVAL_UNAVAILABLE) |
| ? mcsIntervalToMilliseconds(position) |
| : TRACK_POSITION_UNAVAILABLE; |
| mEventLogger.add("handleTrackPositionRequest: positionMs= " + positionMs); |
| mCallbacks.onTrackPositionSetRequest(positionMs); |
| } |
| |
| private static int getMediaControlPointRequestPayloadLength(int opcode) { |
| switch (opcode) { |
| case Request.Opcodes.MOVE_RELATIVE: |
| case Request.Opcodes.GOTO_SEGMENT: |
| case Request.Opcodes.GOTO_TRACK: |
| case Request.Opcodes.GOTO_GROUP: |
| return 4; |
| default: |
| return 0; |
| } |
| } |
| |
| @VisibleForTesting |
| int handleMediaControlPointRequest(BluetoothDevice device, byte[] value) { |
| final int payloadOffset = 1; |
| final int opcode = value[0]; |
| |
| // Test for RFU bits and currently supported opcodes |
| if (!isOpcodeSupported(opcode)) { |
| Log.i(TAG, "handleMediaControlPointRequest: " + Request.Opcodes.toString(opcode) |
| + " not supported"); |
| mHandler.post(() -> { |
| setMediaControlRequestResult(new Request(opcode, 0), |
| Request.Results.OPCODE_NOT_SUPPORTED); |
| }); |
| return BluetoothGatt.GATT_SUCCESS; |
| } |
| |
| if (getMediaControlPointRequestPayloadLength(opcode) != (value.length - payloadOffset)) { |
| Log.w(TAG, "handleMediaControlPointRequest: " + Request.Opcodes.toString(opcode) |
| + " bad payload length"); |
| return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; |
| } |
| |
| // Only some requests have payload |
| int intVal = 0; |
| if (opcode == Request.Opcodes.MOVE_RELATIVE |
| || opcode == Request.Opcodes.GOTO_SEGMENT |
| || opcode == Request.Opcodes.GOTO_TRACK |
| || opcode == Request.Opcodes.GOTO_GROUP) { |
| intVal = ByteBuffer.wrap(value, payloadOffset, value.length - payloadOffset) |
| .order(ByteOrder.LITTLE_ENDIAN) |
| .getInt(); |
| |
| // If the argument is time interval, convert to milliseconds time domain |
| if (opcode == Request.Opcodes.MOVE_RELATIVE) { |
| intVal = (int) mcsIntervalToMilliseconds(intVal); |
| } |
| } |
| |
| Request req = new Request(opcode, intVal); |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "handleMediaControlPointRequest: sending " |
| + Request.Opcodes.toString(opcode) |
| + " request up"); |
| |
| // TODO: Activate/deactivate devices with ActiveDeviceManager |
| if (req.getOpcode() == Request.Opcodes.PLAY) { |
| if (mAdapterService.getActiveDevices(BluetoothProfile.A2DP).size() > 0) { |
| A2dpService.getA2dpService().removeActiveDevice(false); |
| } |
| if (mAdapterService.getActiveDevices(BluetoothProfile.HEARING_AID).size() > 0) { |
| HearingAidService.getHearingAidService().removeActiveDevice(false); |
| } |
| if (mLeAudioService == null) { |
| mLeAudioService = LeAudioService.getLeAudioService(); |
| } |
| mLeAudioService.setActiveDevice(device); |
| } |
| mCallbacks.onMediaControlRequest(req); |
| |
| return BluetoothGatt.GATT_SUCCESS; |
| } |
| |
| public void setCallbacks(MediaControlServiceCallbacks callbacks) { |
| mCallbacks = callbacks; |
| } |
| |
| @VisibleForTesting |
| protected void setServiceManagerForTesting(McpService manager) { |
| mMcpService = manager; |
| } |
| |
| @VisibleForTesting |
| void setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy) { |
| mBluetoothGattServer = proxy; |
| } |
| |
| @VisibleForTesting |
| void setLeAudioServiceForTesting(LeAudioService leAudioService) { |
| mLeAudioService = leAudioService; |
| } |
| |
| private boolean initGattService(UUID serviceUuid) { |
| mEventLogger.logd(DBG, TAG, "initGattService: uuid= " + serviceUuid); |
| |
| if (mBluetoothGattServer == null) { |
| BluetoothManager manager = mContext.getSystemService(BluetoothManager.class); |
| BluetoothGattServer server = manager.openGattServer(mContext, mServerCallback); |
| if (server == null) { |
| Log.e(TAG, "Failed to start BluetoothGattServer for MCP"); |
| //TODO: This now effectively makes MCP unusable, but fixes tests |
| // Handle this error more gracefully, verify BluetoothInstrumentationTests |
| // are passing after fix is applied |
| return false; |
| } |
| mBluetoothGattServer = new BluetoothGattServerProxy(server, manager); |
| } |
| |
| mGattService = |
| new BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY); |
| |
| for (Pair<UUID, CharacteristicData> entry : getUuidCharacteristicList()) { |
| CharacteristicData desc = entry.second; |
| UUID uuid = entry.first; |
| if (VDBG) { |
| Log.d(TAG, "Checking uuid: " + uuid); |
| } |
| if ((mFeatures & desc.featureFlag) != 0) { |
| int notifyProp = (((mFeatures & desc.ntfFeatureFlag) != 0) |
| ? PROPERTY_NOTIFY |
| : 0); |
| |
| BluetoothGattCharacteristic myChar = new BluetoothGattCharacteristic( |
| uuid, desc.properties | notifyProp, desc.permissions); |
| |
| // Add CCC descriptor if notification is supported |
| if ((myChar.getProperties() & PROPERTY_NOTIFY) != 0) { |
| BluetoothGattDescriptor cccDesc = new BluetoothGattDescriptor(UUID_CCCD, |
| BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED |
| | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED); |
| if (VDBG) { |
| Log.d(TAG, "Adding descriptor: " + cccDesc); |
| } |
| myChar.addDescriptor(cccDesc); |
| } |
| |
| if (VDBG) { |
| Log.d(TAG, "Adding char: " + myChar); |
| } |
| mCharacteristics.put(desc.id, myChar); |
| mGattService.addCharacteristic(myChar); |
| } |
| } |
| if (VDBG) { |
| Log.d(TAG, "Adding service: " + mGattService); |
| } |
| return mBluetoothGattServer.addService(mGattService); |
| } |
| |
| private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) { |
| List<ParcelUuid> uuidList; |
| byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); |
| |
| if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { |
| uuidList = new ArrayList<ParcelUuid>(); |
| } else { |
| uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd))); |
| |
| if (!uuidList.contains(charUuid)) { |
| Log.d(TAG, "Characteristic CCCD can't be removed (not cached): " |
| + charUuid.toString()); |
| return; |
| } |
| } |
| |
| uuidList.remove(charUuid); |
| |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "removeUuidFromMetadata: device= " + device + ", char= " + charUuid.toString()); |
| if (!device.setMetadata(METADATA_GMCS_CCCD, |
| Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) { |
| Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString() |
| + ", (remove)"); |
| } |
| } |
| |
| private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) { |
| List<ParcelUuid> uuidList; |
| byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); |
| |
| if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { |
| uuidList = new ArrayList<ParcelUuid>(); |
| } else { |
| uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd))); |
| |
| if (uuidList.contains(charUuid)) { |
| Log.d(TAG, "Characteristic CCCD already added: " + charUuid.toString()); |
| return; |
| } |
| } |
| |
| uuidList.add(charUuid); |
| |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "addUuidToMetadata: device= " + device + ", char= " + charUuid.toString()); |
| if (!device.setMetadata(METADATA_GMCS_CCCD, |
| Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) { |
| Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString() |
| + ", (add)"); |
| } |
| } |
| |
| @VisibleForTesting |
| void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value, boolean store) { |
| Map<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress()); |
| if (characteristicCcc == null) { |
| characteristicCcc = new HashMap<>(); |
| mCccDescriptorValues.put(device.getAddress(), characteristicCcc); |
| } |
| |
| characteristicCcc.put(charUuid, |
| ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort()); |
| |
| if (!store) { |
| return; |
| } |
| |
| if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { |
| mEventLogger.add("setCcc: device= " + device + ", notify= " + true); |
| addUuidToMetadata(new ParcelUuid(charUuid), device); |
| } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) { |
| mEventLogger.add("setCcc: device= " + device + ", notify= " + false); |
| removeUuidFromMetadata(new ParcelUuid(charUuid), device); |
| } else { |
| Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value)); |
| } |
| } |
| |
| private byte[] getCccBytes(BluetoothDevice device, UUID charUuid) { |
| Map<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress()); |
| if (characteristicCcc != null) { |
| ByteBuffer bb = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN); |
| Short ccc = characteristicCcc.get(charUuid); |
| if (ccc != null) { |
| bb.putShort(characteristicCcc.get(charUuid)); |
| return bb.array(); |
| } |
| } |
| return BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; |
| } |
| |
| @Override |
| public void updatePlaybackState(MediaState state) { |
| if (DBG) { |
| Log.d(TAG, "updatePlaybackState"); |
| } |
| |
| if ((state.getValue() <= MediaState.STATE_MAX.getValue()) |
| && (state.getValue() >= MediaState.STATE_MIN.getValue())) { |
| updateMediaStateChar(state.getValue()); |
| } |
| } |
| |
| @VisibleForTesting |
| int getMediaStateChar() { |
| if (!isFeatureSupported(ServiceFeature.MEDIA_STATE)) return MediaState.INACTIVE.getValue(); |
| |
| BluetoothGattCharacteristic stateChar = |
| mCharacteristics.get(CharId.MEDIA_STATE); |
| |
| if (stateChar.getValue() != null) { |
| return stateChar.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| } |
| |
| return MediaState.INACTIVE.getValue(); |
| } |
| |
| @VisibleForTesting |
| void updateMediaStateChar(int state) { |
| if (DBG) { |
| Log.d(TAG, "updateMediaStateChar: state= " + MediaState.toString(state)); |
| } |
| |
| if (!isFeatureSupported(ServiceFeature.MEDIA_STATE)) return; |
| |
| mEventLogger.logd(DBG, TAG, "updateMediaStateChar: state= " + MediaState.toString(state)); |
| |
| BluetoothGattCharacteristic stateChar = |
| mCharacteristics.get(CharId.MEDIA_STATE); |
| stateChar.setValue(state, BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| notifyCharacteristic(stateChar, null); |
| } |
| |
| private void updateObjectID(int objectIdField, long objectIdValue, boolean notify) { |
| if (DBG) { |
| Log.d(TAG, "updateObjectID"); |
| } |
| int feature = ObjectIds.GetMatchingServiceFeature(objectIdField); |
| |
| if (!isFeatureSupported(feature)) return; |
| |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateObjectIdChar: charId= " |
| + CharId.FromFeature(feature) |
| + ", objId= " |
| + objectIdValue); |
| updateObjectIdChar(mCharacteristics.get(CharId.FromFeature(feature)), |
| objectIdValue, null, notify); |
| } |
| |
| @Override |
| public void updateObjectID(int objectIdField, long objectIdValue) { |
| updateObjectID(objectIdField, objectIdValue, true); |
| } |
| |
| @Override |
| public void setMediaControlRequestResult(Request request, |
| Request.Results resultStatus) { |
| if (DBG) { |
| Log.d(TAG, "setMediaControlRequestResult"); |
| } |
| |
| if (getMediaStateChar() == MediaState.INACTIVE.getValue()) { |
| resultStatus = Request.Results.MEDIA_PLAYER_INACTIVE; |
| } |
| |
| ByteBuffer bb = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN); |
| bb.put((byte) request.getOpcode()); |
| bb.put((byte) resultStatus.getValue()); |
| |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.MEDIA_CONTROL_POINT); |
| characteristic.setValue(bb.array()); |
| notifyCharacteristic(characteristic, null); |
| } |
| |
| @Override |
| public void setSearchRequestResult(SearchRequest request, |
| SearchRequest.Results resultStatus, long resultObjectId) { |
| if (DBG) { |
| Log.d(TAG, "setSearchRequestResult"); |
| } |
| |
| // TODO: There is no Object Trasfer Service implementation. |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.SEARCH_CONTROL_POINT); |
| characteristic.setValue(new byte[]{SEARCH_CONTROL_POINT_RESULT_FAILURE}); |
| notifyCharacteristic(characteristic, null); |
| } |
| |
| @Override |
| public void updatePlayerState(Map stateFields) { |
| if (stateFields.isEmpty()) { |
| return; |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYBACK_STATE)) { |
| MediaState playbackState = |
| (MediaState) stateFields.get(PlayerStateField.PLAYBACK_STATE); |
| if (DBG) { |
| Log.d(TAG, |
| "updatePlayerState: playbackState= " |
| + stateFields.get(PlayerStateField.PLAYBACK_STATE)); |
| } |
| |
| if (playbackState == MediaState.INACTIVE) { |
| setInitialCharacteristicValues(); |
| } |
| } |
| final boolean doNotifyValueChange = true; |
| |
| // Additional fields that may be requested by the service to complete the new state info |
| List<PlayerStateField> reqFieldList = null; |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYBACK_SPEED)) { |
| updatePlaybackSpeedChar( |
| (float) stateFields.get(PlayerStateField.PLAYBACK_SPEED), doNotifyValueChange); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYING_ORDER_SUPPORTED)) { |
| updatePlayingOrderSupportedChar( |
| (Integer) stateFields.get(PlayerStateField.PLAYING_ORDER_SUPPORTED)); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYING_ORDER)) { |
| updatePlayingOrderChar((PlayingOrder) stateFields.get(PlayerStateField.PLAYING_ORDER), |
| doNotifyValueChange); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.TRACK_POSITION)) { |
| updateTrackPositionChar( |
| (long) stateFields.get(PlayerStateField.TRACK_POSITION), doNotifyValueChange); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYER_NAME)) { |
| String name = (String) stateFields.get(PlayerStateField.PLAYER_NAME); |
| if ((getPlayerNameChar() != null) && (name.compareTo(getPlayerNameChar()) != 0)) { |
| updatePlayerNameChar(name, doNotifyValueChange); |
| |
| // Most likely the player has changed - request critical info fields |
| reqFieldList = new ArrayList<>(); |
| reqFieldList.add(PlayerStateField.PLAYBACK_STATE); |
| reqFieldList.add(PlayerStateField.TRACK_DURATION); |
| |
| if (isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { |
| reqFieldList.add(PlayerStateField.OPCODES_SUPPORTED); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { |
| reqFieldList.add(PlayerStateField.PLAYING_ORDER_SUPPORTED); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYING_ORDER)) { |
| reqFieldList.add(PlayerStateField.PLAYING_ORDER); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { |
| reqFieldList.add(PlayerStateField.ICON_OBJ_ID); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { |
| reqFieldList.add(PlayerStateField.ICON_URL); |
| } |
| } |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.ICON_URL)) { |
| updatePlayerIconUrlChar((String) stateFields.get(PlayerStateField.ICON_URL)); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.ICON_OBJ_ID)) { |
| updateIconObjIdChar((Long) stateFields.get(PlayerStateField.ICON_OBJ_ID)); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.OPCODES_SUPPORTED)) { |
| updateSupportedOpcodesChar( |
| (Integer) stateFields.get(PlayerStateField.OPCODES_SUPPORTED), |
| doNotifyValueChange); |
| } |
| |
| // Notify track change if any of these have changed |
| boolean notifyTrackChange = false; |
| if (stateFields.containsKey(PlayerStateField.TRACK_TITLE)) { |
| String newTitle = (String) stateFields.get(PlayerStateField.TRACK_TITLE); |
| |
| if (getTrackTitleChar().compareTo(newTitle) != 0) { |
| updateTrackTitleChar((String) stateFields.get(PlayerStateField.TRACK_TITLE), |
| doNotifyValueChange); |
| notifyTrackChange = true; |
| } |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.TRACK_DURATION)) { |
| long newTrackDuration = (long) (stateFields.get(PlayerStateField.TRACK_DURATION)); |
| if (getTrackDurationChar() != newTrackDuration) { |
| updateTrackDurationChar(newTrackDuration, doNotifyValueChange); |
| notifyTrackChange = true; |
| } |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.PLAYBACK_STATE)) { |
| mCurrentMediaState = |
| (MediaState) stateFields.get(PlayerStateField.PLAYBACK_STATE); |
| } |
| |
| int mediaState = getMediaStateChar(); |
| if (mediaState != mCurrentMediaState.getValue()) { |
| updateMediaStateChar(mCurrentMediaState.getValue()); |
| } |
| |
| if (stateFields.containsKey(PlayerStateField.SEEKING_SPEED)) { |
| int playbackState = getMediaStateChar(); |
| // Seeking speed should be 1.0f (char. value of 0) when not in seeking state. |
| // [Ref. Media Control Service v1.0, sec. 3.9] |
| if (playbackState == MediaState.SEEKING.getValue()) { |
| updateSeekingSpeedChar((float) stateFields.get(PlayerStateField.SEEKING_SPEED), |
| doNotifyValueChange); |
| } else { |
| updateSeekingSpeedChar(1.0f, doNotifyValueChange); |
| } |
| } |
| |
| // Notify track change as the last step of all track change related characteristic changes. |
| // [Ref. Media Control Service v1.0, sec. 3.4.1] |
| if (notifyTrackChange) { |
| if (isFeatureSupported(ServiceFeature.TRACK_CHANGED)) { |
| BluetoothGattCharacteristic myChar = |
| mCharacteristics.get(CharId.TRACK_CHANGED); |
| myChar.setValue(new byte[]{}); |
| notifyCharacteristic(myChar, null); |
| } |
| } |
| |
| if (reqFieldList != null) { |
| // Don't ask for those that we just got. |
| reqFieldList.removeAll(stateFields.keySet()); |
| |
| if (!reqFieldList.isEmpty()) { |
| mCallbacks.onPlayerStateRequest( |
| reqFieldList.stream().toArray(PlayerStateField[]::new)); |
| } |
| } |
| } |
| |
| @Override |
| public int getContentControlId() { |
| return mCcid; |
| } |
| |
| @Override |
| public void onDeviceAuthorizationSet(BluetoothDevice device) { |
| int auth = getDeviceAuthorization(device); |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "onDeviceAuthorizationSet: device= " |
| + device |
| + ", authorization= " |
| + (auth == BluetoothDevice.ACCESS_ALLOWED |
| ? "ALLOWED" |
| : (auth == BluetoothDevice.ACCESS_REJECTED |
| ? "REJECTED" |
| : "UNKNOWN"))); |
| ProcessPendingGattOperations(device); |
| for (BluetoothGattCharacteristic characteristic : mCharacteristics.values()) { |
| // Notify only the updated characteristics |
| if (characteristic.getValue() != null) { |
| notifyCharacteristic(device, characteristic); |
| } |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| if (DBG) { |
| Log.d(TAG, "Destroy"); |
| } |
| |
| if (mBluetoothGattServer == null) { |
| return; |
| } |
| |
| if (mBluetoothGattServer.removeService(mGattService)) { |
| if (mCallbacks != null) { |
| mCallbacks.onServiceInstanceUnregistered(ServiceStatus.OK); |
| } |
| } |
| |
| mBluetoothGattServer.close(); |
| } |
| |
| @VisibleForTesting |
| void updatePlayingOrderChar(PlayingOrder order, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updatePlayingOrderChar: order= " + order); |
| } |
| if (!isFeatureSupported(ServiceFeature.PLAYING_ORDER)) return; |
| |
| BluetoothGattCharacteristic orderChar = mCharacteristics.get(CharId.PLAYING_ORDER); |
| Integer playingOrder = null; |
| |
| if (orderChar.getValue() != null) { |
| playingOrder = orderChar.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| } |
| |
| if ((playingOrder == null) || (playingOrder != order.getValue())) { |
| orderChar.setValue(order.getValue(), BluetoothGattCharacteristic.FORMAT_UINT8, 0); |
| if (notify && isFeatureSupported(ServiceFeature.PLAYING_ORDER_NOTIFY)) { |
| notifyCharacteristic(orderChar, null); |
| } |
| mEventLogger.logd(DBG, TAG, "updatePlayingOrderChar: order= " + order); |
| } |
| } |
| |
| 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) return; |
| |
| byte[] ccc = getCccBytes(device, characteristic.getUuid()); |
| if (VDBG) { |
| Log.d( |
| TAG, |
| "notifyCharacteristic: char= " |
| + characteristic.getUuid().toString() |
| + " cccVal= " |
| + ByteBuffer.wrap(ccc).order(ByteOrder.LITTLE_ENDIAN).getShort()); |
| } |
| 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) { |
| /* The spec. defined valid speed range is <0.25, 3.957> as float input, resulting in |
| * <-128, 127> output integer range. */ |
| if (speed < 0) { |
| speed = -speed; |
| } |
| if (speed < PLAY_SPEED_MIN) { |
| speed = PLAY_SPEED_MIN; |
| } else if (speed > PLAY_SPEED_MAX) { |
| speed = PLAY_SPEED_MAX; |
| } |
| |
| return (int) (64 * Math.log(speed) / Math.log(2)); |
| } |
| |
| private static float CharacteristicSpeedIntValueToSpeedFloat(Integer speed) { |
| return (float) (Math.pow(2, (speed.floatValue() / 64.0f))); |
| } |
| |
| @VisibleForTesting |
| Float getSeekingSpeedChar() { |
| Float speed = null; |
| |
| if (isFeatureSupported(ServiceFeature.SEEKING_SPEED)) { |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.SEEKING_SPEED); |
| if (characteristic.getValue() != null) { |
| Integer intVal = |
| characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT8, 0); |
| speed = CharacteristicSpeedIntValueToSpeedFloat(intVal); |
| } |
| } |
| |
| return speed; |
| } |
| |
| @VisibleForTesting |
| void updateSeekingSpeedChar(float speed, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updateSeekingSpeedChar: speed= " + speed); |
| } |
| if (isFeatureSupported(ServiceFeature.SEEKING_SPEED)) { |
| if ((getSeekingSpeedChar() == null) || (getSeekingSpeedChar() != speed)) { |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.SEEKING_SPEED); |
| int intSpeed = SpeedFloatToCharacteristicIntValue(speed); |
| characteristic.setValue(intSpeed, BluetoothGattCharacteristic.FORMAT_SINT8, 0); |
| if (notify && isFeatureSupported(ServiceFeature.SEEKING_SPEED_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateSeekingSpeedChar: intSpeed=" + intSpeed + ", speed= " + speed); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| Float getPlaybackSpeedChar() { |
| Float speed = null; |
| |
| if (!isFeatureSupported(ServiceFeature.PLAYBACK_SPEED)) return null; |
| |
| BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.PLAYBACK_SPEED); |
| if (characteristic.getValue() != null) { |
| Integer intVal = |
| characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT8, 0); |
| speed = CharacteristicSpeedIntValueToSpeedFloat(intVal); |
| } |
| |
| return speed; |
| } |
| |
| @VisibleForTesting |
| void updatePlaybackSpeedChar(float speed, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updatePlaybackSpeedChar: " + speed); |
| } |
| |
| if (!isFeatureSupported(ServiceFeature.PLAYBACK_SPEED)) return; |
| |
| // Reject if no changes were made |
| if ((getPlaybackSpeedChar() == null) || (getPlaybackSpeedChar() != speed)) { |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.PLAYBACK_SPEED); |
| int intSpeed = SpeedFloatToCharacteristicIntValue(speed); |
| characteristic.setValue(intSpeed, BluetoothGattCharacteristic.FORMAT_SINT8, 0); |
| if (notify && isFeatureSupported(ServiceFeature.PLAYBACK_SPEED_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updatePlaybackSpeedChar: intSpeed=" + intSpeed + ", speed= " + speed); |
| } |
| } |
| |
| @VisibleForTesting |
| void updateTrackPositionChar(long positionMs, boolean forceNotify) { |
| if (VDBG) { |
| Log.d(TAG, "updateTrackPositionChar: " + positionMs); |
| } |
| if (!isFeatureSupported(ServiceFeature.TRACK_POSITION)) return; |
| |
| final int position = |
| (positionMs != TRACK_POSITION_UNAVAILABLE) |
| ? (int) millisecondsToMcsInterval(positionMs) |
| : INTERVAL_UNAVAILABLE; |
| |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.TRACK_POSITION); |
| characteristic.setValue(position, BluetoothGattCharacteristic.FORMAT_SINT32, 0); |
| |
| if (isFeatureSupported(ServiceFeature.TRACK_POSITION_NOTIFY)) { |
| // Position should be notified only while seeking (frequency is implementation |
| // specific), on pause, or position change, but not during the playback. |
| if ((getMediaStateChar() == MediaState.PAUSED.getValue()) |
| || (getMediaStateChar() == MediaState.SEEKING.getValue()) |
| || forceNotify) { |
| notifyCharacteristic(characteristic, null); |
| } |
| } |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateTrackPositionChar: positionMs= " + positionMs + ", position= " + position); |
| } |
| |
| private long getTrackDurationChar() { |
| if (!isFeatureSupported(ServiceFeature.TRACK_DURATION)) return TRACK_DURATION_UNAVAILABLE; |
| |
| BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.TRACK_DURATION); |
| if (characteristic.getValue() != null) { |
| int duration = |
| characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT32, 0); |
| return (duration != INTERVAL_UNAVAILABLE) ? mcsIntervalToMilliseconds(duration) |
| : TRACK_DURATION_UNAVAILABLE; |
| } |
| return TRACK_DURATION_UNAVAILABLE; |
| } |
| |
| @VisibleForTesting |
| void updateTrackDurationChar(long durationMs, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updateTrackDurationChar: " + durationMs); |
| } |
| if (isFeatureSupported(ServiceFeature.TRACK_DURATION)) { |
| final int duration = |
| (durationMs != TRACK_DURATION_UNAVAILABLE) |
| ? (int) millisecondsToMcsInterval(durationMs) |
| : INTERVAL_UNAVAILABLE; |
| |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.TRACK_DURATION); |
| characteristic.setValue(duration, BluetoothGattCharacteristic.FORMAT_SINT32, 0); |
| if (notify && isFeatureSupported(ServiceFeature.TRACK_DURATION_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateTrackDurationChar: durationMs= " |
| + durationMs |
| + ", duration= " |
| + duration); |
| } |
| } |
| |
| private String getTrackTitleChar() { |
| if (isFeatureSupported(ServiceFeature.TRACK_TITLE)) { |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.TRACK_TITLE); |
| if (characteristic.getValue() != null) { |
| return characteristic.getStringValue(0); |
| } |
| } |
| |
| return ""; |
| } |
| |
| @VisibleForTesting |
| void updateTrackTitleChar(String title, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updateTrackTitleChar: " + title); |
| } |
| if (isFeatureSupported(ServiceFeature.TRACK_TITLE)) { |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.TRACK_TITLE); |
| characteristic.setValue(title); |
| if (notify && isFeatureSupported(ServiceFeature.TRACK_TITLE_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| mEventLogger.logd(DBG, TAG, "updateTrackTitleChar: title= '" + title + "'"); |
| } |
| } |
| |
| @VisibleForTesting |
| void updateSupportedOpcodesChar(int opcodes, boolean notify) { |
| if (VDBG) { |
| Log.d( |
| TAG, |
| "updateSupportedOpcodesChar: opcodes= " |
| + Request.SupportedOpcodes.toString(opcodes)); |
| } |
| if (!isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) return; |
| |
| BluetoothGattCharacteristic characteristic = mCharacteristics.get( |
| CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED); |
| // Do nothing if nothing has changed |
| if (characteristic.getValue() != null |
| && characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0) == opcodes) { |
| return; |
| } |
| |
| characteristic.setValue(opcodes, BluetoothGattCharacteristic.FORMAT_UINT32, 0); |
| if (notify |
| && isFeatureSupported( |
| ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateSupportedOpcodesChar: opcodes= " |
| + Request.SupportedOpcodes.toString(opcodes)); |
| } |
| |
| @VisibleForTesting |
| void updatePlayingOrderSupportedChar(int supportedOrder) { |
| if (VDBG) { |
| Log.d(TAG, "updatePlayingOrderSupportedChar: " + supportedOrder); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { |
| mCharacteristics.get(CharId.PLAYING_ORDER_SUPPORTED) |
| .setValue(supportedOrder, BluetoothGattCharacteristic.FORMAT_UINT16, 0); |
| mEventLogger.logd( |
| DBG, TAG, "updatePlayingOrderSupportedChar: order= " + supportedOrder); |
| } |
| } |
| |
| private void updateIconObjIdChar(Long objId) { |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { |
| mEventLogger.logd( |
| DBG, |
| TAG, |
| "updateObjectIdChar charId= " |
| + CharId.PLAYER_ICON_OBJ_ID |
| + ", objId= " |
| + objId); |
| updateObjectIdChar(mCharacteristics.get(CharId.PLAYER_ICON_OBJ_ID), objId, |
| null, true); |
| } |
| } |
| |
| @VisibleForTesting |
| public long byteArray2ObjId(byte[] buffer) { |
| ByteBuffer bb = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.LITTLE_ENDIAN); |
| bb.put(buffer, 0, 6); |
| // Move position to beginnng after putting data to buffer |
| bb.position(0); |
| return bb.getLong(); |
| } |
| |
| @VisibleForTesting |
| public byte[] objId2ByteArray(long objId) { |
| if (objId < 0) { |
| return new byte[0]; |
| } |
| |
| ByteBuffer bb = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN); |
| bb.putInt((int) objId); |
| bb.putShort((short) (objId >> Integer.SIZE)); |
| |
| return bb.array(); |
| } |
| |
| private void updateObjectIdChar(BluetoothGattCharacteristic characteristic, long objId, |
| BluetoothDevice originDevice, boolean notify) { |
| characteristic.setValue(objId2ByteArray(objId)); |
| if ((characteristic.getProperties() & PROPERTY_NOTIFY) != 0) { |
| // Notify all clients but not the originDevice |
| if (notify) { |
| notifyCharacteristic(characteristic, originDevice); |
| } |
| } |
| } |
| |
| private void updatePlayerIconUrlChar(String url) { |
| if (VDBG) { |
| Log.d(TAG, "updatePlayerIconUrlChar: " + url); |
| } |
| if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { |
| mCharacteristics.get(CharId.PLAYER_ICON_URL).setValue(url); |
| mEventLogger.logd(DBG, TAG, "updatePlayerIconUrlChar: " + url); |
| } |
| } |
| |
| private String getPlayerNameChar() { |
| if (!isFeatureSupported(ServiceFeature.PLAYER_NAME)) return null; |
| |
| BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.PLAYER_NAME); |
| if (characteristic.getValue() != null) { |
| return characteristic.getStringValue(0); |
| } |
| return null; |
| } |
| |
| @VisibleForTesting |
| void updatePlayerNameChar(String name, boolean notify) { |
| if (VDBG) { |
| Log.d(TAG, "updatePlayerNameChar: " + name); |
| } |
| |
| if (!isFeatureSupported(ServiceFeature.PLAYER_NAME)) return; |
| |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.PLAYER_NAME); |
| characteristic.setValue(name); |
| mEventLogger.logd(DBG, TAG, "updatePlayerNameChar: name= '" + name + "'"); |
| if (notify && isFeatureSupported(ServiceFeature.PLAYER_NAME_NOTIFY)) { |
| notifyCharacteristic(characteristic, null); |
| } |
| } |
| |
| private boolean isFeatureSupported(long featureBit) { |
| if (DBG) { |
| Log.w(TAG, "Feature " + ServiceFeature.toString(featureBit) + " support: " |
| + ((mFeatures & featureBit) != 0)); |
| } |
| return (mFeatures & featureBit) != 0; |
| } |
| |
| @VisibleForTesting |
| boolean isOpcodeSupported(int opcode) { |
| if (opcode < Request.Opcodes.PLAY || opcode > Request.Opcodes.GOTO_GROUP) { |
| return false; |
| } |
| |
| if (!isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { |
| return false; |
| } |
| |
| Integer opcodeSupportBit = Request.OpcodeToOpcodeSupport.get(opcode); |
| if (opcodeSupportBit == null) return false; |
| |
| return (mCharacteristics.get(CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED) |
| .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0) |
| & opcodeSupportBit) == opcodeSupportBit; |
| } |
| |
| private interface CharacteristicWriteHandler { |
| void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, |
| BluetoothGattCharacteristic characteristic, boolean preparedWrite, |
| boolean responseNeeded, int offset, byte[] value); |
| } |
| |
| private static final class CharacteristicData { |
| public final int id; |
| public final int properties; |
| public final int permissions; |
| public final long featureFlag; |
| public final long ntfFeatureFlag; |
| |
| private CharacteristicData( |
| int id, long featureFlag, long ntfFeatureFlag, int properties, int permissions) { |
| this.id = id; |
| this.featureFlag = featureFlag; |
| this.ntfFeatureFlag = ntfFeatureFlag; |
| this.properties = properties; |
| this.permissions = permissions; |
| } |
| } |
| |
| private final static class CharId { |
| public static final int PLAYER_NAME = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYER_NAME); |
| public static final int PLAYER_ICON_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYER_ICON_OBJ_ID); |
| public static final int PLAYER_ICON_URL = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYER_ICON_URL); |
| public static final int TRACK_CHANGED = |
| Long.numberOfTrailingZeros(ServiceFeature.TRACK_CHANGED); |
| public static final int TRACK_TITLE = |
| Long.numberOfTrailingZeros(ServiceFeature.TRACK_TITLE); |
| public static final int TRACK_DURATION = |
| Long.numberOfTrailingZeros(ServiceFeature.TRACK_DURATION); |
| public static final int TRACK_POSITION = |
| Long.numberOfTrailingZeros(ServiceFeature.TRACK_POSITION); |
| public static final int PLAYBACK_SPEED = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYBACK_SPEED); |
| public static final int SEEKING_SPEED = |
| Long.numberOfTrailingZeros(ServiceFeature.SEEKING_SPEED); |
| public static final int CURRENT_TRACK_SEGMENT_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.CURRENT_TRACK_SEGMENT_OBJ_ID); |
| public static final int CURRENT_TRACK_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.CURRENT_TRACK_OBJ_ID); |
| public static final int NEXT_TRACK_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.NEXT_TRACK_OBJ_ID); |
| public static final int CURRENT_GROUP_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.CURRENT_GROUP_OBJ_ID); |
| public static final int PARENT_GROUP_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.PARENT_GROUP_OBJ_ID); |
| public static final int PLAYING_ORDER = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYING_ORDER); |
| public static final int PLAYING_ORDER_SUPPORTED = |
| Long.numberOfTrailingZeros(ServiceFeature.PLAYING_ORDER_SUPPORTED); |
| public static final int MEDIA_STATE = |
| Long.numberOfTrailingZeros(ServiceFeature.MEDIA_STATE); |
| public static final int MEDIA_CONTROL_POINT = |
| Long.numberOfTrailingZeros(ServiceFeature.MEDIA_CONTROL_POINT); |
| public static final int MEDIA_CONTROL_POINT_OPCODES_SUPPORTED = |
| Long.numberOfTrailingZeros(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED); |
| public static final int SEARCH_RESULT_OBJ_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.SEARCH_RESULT_OBJ_ID); |
| public static final int SEARCH_CONTROL_POINT = |
| Long.numberOfTrailingZeros(ServiceFeature.SEARCH_CONTROL_POINT); |
| public static final int CONTENT_CONTROL_ID = |
| Long.numberOfTrailingZeros(ServiceFeature.CONTENT_CONTROL_ID); |
| |
| public static int FromFeature(long feature) { |
| return Long.numberOfTrailingZeros(feature); |
| } |
| } |
| |
| private static final UUID UUID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); |
| |
| /* All characteristic attributes (UUIDs, properties, permissions and flags needed to enable |
| * them) This is set according to the Media Control Service Specification. |
| */ |
| private static List<Pair<UUID, CharacteristicData>> getUuidCharacteristicList() { |
| List<Pair<UUID, CharacteristicData>> characteristics = new ArrayList<>(); |
| characteristics.add(new Pair<>(UUID_PLAYER_NAME, |
| new CharacteristicData(CharId.PLAYER_NAME, ServiceFeature.PLAYER_NAME, |
| ServiceFeature.PLAYER_NAME_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PLAYER_ICON_OBJ_ID, |
| new CharacteristicData(CharId.PLAYER_ICON_OBJ_ID, ServiceFeature.PLAYER_ICON_OBJ_ID, |
| // Notifications unsupported |
| 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PLAYER_ICON_URL, |
| new CharacteristicData(CharId.PLAYER_ICON_URL, ServiceFeature.PLAYER_ICON_URL, |
| // Notifications unsupported |
| 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_TRACK_CHANGED, |
| new CharacteristicData(CharId.TRACK_CHANGED, ServiceFeature.TRACK_CHANGED, |
| // Mandatory notification if char. exists. |
| ServiceFeature.TRACK_CHANGED, PROPERTY_NOTIFY, 0))); |
| characteristics.add(new Pair<>(UUID_TRACK_TITLE, |
| new CharacteristicData(CharId.TRACK_TITLE, ServiceFeature.TRACK_TITLE, |
| ServiceFeature.TRACK_TITLE_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_TRACK_DURATION, |
| new CharacteristicData(CharId.TRACK_DURATION, ServiceFeature.TRACK_DURATION, |
| ServiceFeature.TRACK_DURATION_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_TRACK_POSITION, |
| new CharacteristicData(CharId.TRACK_POSITION, ServiceFeature.TRACK_POSITION, |
| ServiceFeature.TRACK_POSITION_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PLAYBACK_SPEED, |
| new CharacteristicData(CharId.PLAYBACK_SPEED, ServiceFeature.PLAYBACK_SPEED, |
| ServiceFeature.PLAYBACK_SPEED_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_SEEKING_SPEED, |
| new CharacteristicData(CharId.SEEKING_SPEED, ServiceFeature.SEEKING_SPEED, |
| ServiceFeature.SEEKING_SPEED_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_CURRENT_TRACK_SEGMENT_OBJ_ID, |
| new CharacteristicData(CharId.CURRENT_TRACK_SEGMENT_OBJ_ID, |
| ServiceFeature.CURRENT_TRACK_SEGMENT_OBJ_ID, |
| // Notifications unsupported |
| 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_CURRENT_TRACK_OBJ_ID, |
| new CharacteristicData(CharId.CURRENT_TRACK_OBJ_ID, |
| ServiceFeature.CURRENT_TRACK_OBJ_ID, |
| ServiceFeature.CURRENT_TRACK_OBJ_ID_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_NEXT_TRACK_OBJ_ID, |
| new CharacteristicData(CharId.NEXT_TRACK_OBJ_ID, ServiceFeature.NEXT_TRACK_OBJ_ID, |
| ServiceFeature.NEXT_TRACK_OBJ_ID_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_CURRENT_GROUP_OBJ_ID, |
| new CharacteristicData(CharId.CURRENT_GROUP_OBJ_ID, |
| ServiceFeature.CURRENT_GROUP_OBJ_ID, |
| ServiceFeature.CURRENT_GROUP_OBJ_ID_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PARENT_GROUP_OBJ_ID, |
| new CharacteristicData(CharId.PARENT_GROUP_OBJ_ID, |
| ServiceFeature.PARENT_GROUP_OBJ_ID, |
| ServiceFeature.PARENT_GROUP_OBJ_ID_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PLAYING_ORDER, |
| new CharacteristicData(CharId.PLAYING_ORDER, ServiceFeature.PLAYING_ORDER, |
| ServiceFeature.PLAYING_ORDER_NOTIFY, |
| PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, |
| PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_PLAYING_ORDER_SUPPORTED, |
| new CharacteristicData(CharId.PLAYING_ORDER_SUPPORTED, |
| ServiceFeature.PLAYING_ORDER_SUPPORTED, |
| // Notifications unsupported |
| 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_MEDIA_STATE, |
| new CharacteristicData(CharId.MEDIA_STATE, ServiceFeature.MEDIA_STATE, |
| // Mandatory notification if char. exists. |
| ServiceFeature.MEDIA_STATE, PROPERTY_READ | PROPERTY_NOTIFY, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_MEDIA_CONTROL_POINT, |
| new CharacteristicData(CharId.MEDIA_CONTROL_POINT, |
| ServiceFeature.MEDIA_CONTROL_POINT, |
| // Mandatory notification if char. exists. |
| ServiceFeature.MEDIA_CONTROL_POINT, |
| PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE | PROPERTY_NOTIFY, |
| PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, |
| new CharacteristicData(CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, |
| ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, |
| ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_NOTIFY, PROPERTY_READ, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_SEARCH_RESULT_OBJ_ID, |
| new CharacteristicData(CharId.SEARCH_RESULT_OBJ_ID, |
| ServiceFeature.SEARCH_RESULT_OBJ_ID, |
| // Mandatory notification if char. exists. |
| ServiceFeature.SEARCH_RESULT_OBJ_ID, PROPERTY_READ | PROPERTY_NOTIFY, |
| PERMISSION_READ_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_SEARCH_CONTROL_POINT, |
| new CharacteristicData(CharId.SEARCH_CONTROL_POINT, |
| ServiceFeature.SEARCH_CONTROL_POINT, |
| // Mandatory notification if char. exists. |
| ServiceFeature.SEARCH_CONTROL_POINT, |
| PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE | PROPERTY_NOTIFY, |
| PERMISSION_WRITE_ENCRYPTED))); |
| characteristics.add(new Pair<>(UUID_CONTENT_CONTROL_ID, |
| new CharacteristicData(CharId.CONTENT_CONTROL_ID, ServiceFeature.CONTENT_CONTROL_ID, |
| // Notifications unsupported |
| 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); |
| return characteristics; |
| } |
| |
| public void dump(StringBuilder sb) { |
| sb.append("\tMediaControlService instance current state:"); |
| sb.append("\n\t\tCcid = " + mCcid); |
| sb.append("\n\t\tFeatures:" + ServiceFeature.featuresToString(mFeatures, "\n\t\t\t")); |
| |
| BluetoothGattCharacteristic characteristic = |
| mCharacteristics.get(CharId.PLAYER_NAME); |
| if (characteristic == null) { |
| sb.append("\n\t\tPlayer name: <No Player>"); |
| } else { |
| sb.append("\n\t\tPlayer name: " + characteristic.getStringValue(0)); |
| } |
| |
| sb.append("\n\t\tCurrentPlaybackState = " + mCurrentMediaState); |
| for (Map.Entry<String, Map<UUID, Short>> deviceEntry : mCccDescriptorValues.entrySet()) { |
| sb.append("\n\t\tCCC states for device: " + "xx:xx:xx:xx:" |
| + deviceEntry.getKey().substring(12)); |
| for (Map.Entry<UUID, Short> entry : deviceEntry.getValue().entrySet()) { |
| sb.append("\n\t\t\tCharacteristic: " + mcsUuidToString(entry.getKey()) + ", value: " |
| + Utils.cccIntToStr(entry.getValue())); |
| } |
| } |
| |
| sb.append("\n\n"); |
| mEventLogger.dump(sb); |
| } |
| } |