1 /* 2 * Copyright 2021 HIMSA II K/S - www.himsa.com. 3 * Represented by EHIMA - www.ehima.com 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.bluetooth.mcp; 19 20 import static android.bluetooth.BluetoothDevice.METADATA_GMCS_CCCD; 21 import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED; 22 import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED; 23 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY; 24 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ; 25 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE; 26 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; 27 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.bluetooth.BluetoothAdapter; 31 import android.bluetooth.BluetoothDevice; 32 import android.bluetooth.BluetoothGatt; 33 import android.bluetooth.BluetoothGattCharacteristic; 34 import android.bluetooth.BluetoothGattDescriptor; 35 import android.bluetooth.BluetoothGattServer; 36 import android.bluetooth.BluetoothGattServerCallback; 37 import android.bluetooth.BluetoothGattService; 38 import android.bluetooth.BluetoothManager; 39 import android.bluetooth.BluetoothProfile; 40 import android.bluetooth.IBluetoothManager; 41 import android.bluetooth.IBluetoothStateChangeCallback; 42 import android.content.Context; 43 import android.os.Handler; 44 import android.os.Looper; 45 import android.os.ParcelUuid; 46 import android.os.RemoteException; 47 import android.util.Log; 48 import android.util.Pair; 49 50 import com.android.bluetooth.Utils; 51 import com.android.bluetooth.a2dp.A2dpService; 52 import com.android.bluetooth.btservice.AdapterService; 53 import com.android.bluetooth.hearingaid.HearingAidService; 54 import com.android.bluetooth.le_audio.LeAudioService; 55 import com.android.internal.annotations.VisibleForTesting; 56 57 import java.nio.ByteBuffer; 58 import java.nio.ByteOrder; 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.HashMap; 62 import java.util.List; 63 import java.util.Map; 64 import java.util.Objects; 65 import java.util.UUID; 66 67 /** 68 * This implements Media Control Service object which is given back to the app who registers a new 69 * MCS instance through the MCS Service Manager. It has no higher level logic to control the media 70 * player itself, thus can be used either as an MCS or a single-instance GMCS. It implements only 71 * the GATT Service logic, allowing the higher level layer to control the service state and react to 72 * bluetooth peer device requests through the method calls and callback mechanism. 73 * 74 * Implemented according to Media Control Service v1.0 specification. 75 */ 76 public class MediaControlGattService implements MediaControlGattServiceInterface { 77 private static final String TAG = "MediaControlGattService"; 78 private static final boolean DBG = Log.isLoggable(TAG, Log.INFO); 79 private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); 80 81 /* MCS assigned UUIDs */ 82 public static final UUID UUID_PLAYER_NAME = 83 UUID.fromString("00002b93-0000-1000-8000-00805f9b34fb"); 84 public static final UUID UUID_PLAYER_ICON_OBJ_ID = 85 UUID.fromString("00002b94-0000-1000-8000-00805f9b34fb"); 86 public static final UUID UUID_PLAYER_ICON_URL = 87 UUID.fromString("00002b95-0000-1000-8000-00805f9b34fb"); 88 public static final UUID UUID_TRACK_CHANGED = 89 UUID.fromString("00002b96-0000-1000-8000-00805f9b34fb"); 90 public static final UUID UUID_TRACK_TITLE = 91 UUID.fromString("00002b97-0000-1000-8000-00805f9b34fb"); 92 public static final UUID UUID_TRACK_DURATION = 93 UUID.fromString("00002b98-0000-1000-8000-00805f9b34fb"); 94 public static final UUID UUID_TRACK_POSITION = 95 UUID.fromString("00002b99-0000-1000-8000-00805f9b34fb"); 96 public static final UUID UUID_PLAYBACK_SPEED = 97 UUID.fromString("00002b9a-0000-1000-8000-00805f9b34fb"); 98 public static final UUID UUID_SEEKING_SPEED = 99 UUID.fromString("00002b9b-0000-1000-8000-00805f9b34fb"); 100 public static final UUID UUID_CURRENT_TRACK_SEGMENT_OBJ_ID = 101 UUID.fromString("00002b9c-0000-1000-8000-00805f9b34fb"); 102 public static final UUID UUID_CURRENT_TRACK_OBJ_ID = 103 UUID.fromString("00002b9d-0000-1000-8000-00805f9b34fb"); 104 public static final UUID UUID_NEXT_TRACK_OBJ_ID = 105 UUID.fromString("00002b9e-0000-1000-8000-00805f9b34fb"); 106 public static final UUID UUID_CURRENT_GROUP_OBJ_ID = 107 UUID.fromString("00002b9f-0000-1000-8000-00805f9b34fb"); 108 public static final UUID UUID_PARENT_GROUP_OBJ_ID = 109 UUID.fromString("00002ba0-0000-1000-8000-00805f9b34fb"); 110 public static final UUID UUID_PLAYING_ORDER = 111 UUID.fromString("00002ba1-0000-1000-8000-00805f9b34fb"); 112 public static final UUID UUID_PLAYING_ORDER_SUPPORTED = 113 UUID.fromString("00002ba2-0000-1000-8000-00805f9b34fb"); 114 public static final UUID UUID_MEDIA_STATE = 115 UUID.fromString("00002ba3-0000-1000-8000-00805f9b34fb"); 116 public static final UUID UUID_MEDIA_CONTROL_POINT = 117 UUID.fromString("00002ba4-0000-1000-8000-00805f9b34fb"); 118 public static final UUID UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED = 119 UUID.fromString("00002ba5-0000-1000-8000-00805f9b34fb"); 120 public static final UUID UUID_SEARCH_RESULT_OBJ_ID = 121 UUID.fromString("00002ba6-0000-1000-8000-00805f9b34fb"); 122 public static final UUID UUID_SEARCH_CONTROL_POINT = 123 UUID.fromString("00002ba7-0000-1000-8000-00805f9b34fb"); 124 public static final UUID UUID_CONTENT_CONTROL_ID = 125 UUID.fromString("00002bba-0000-1000-8000-00805f9b34fb"); 126 127 private static final byte SEARCH_CONTROL_POINT_RESULT_SUCCESS = 0x01; 128 private static final byte SEARCH_CONTROL_POINT_RESULT_FAILURE = 0x02; 129 130 private static final float PLAY_SPEED_MIN = 0.25f; 131 private static final float PLAY_SPEED_MAX = 3.957f; 132 133 private static final int INTERVAL_UNAVAILABLE = 0xFFFFFFFF; 134 135 private final int mCcid; 136 private HashMap<String, HashMap<UUID, Short>> mCccDescriptorValues; 137 private long mFeatures; 138 private Context mContext; 139 private MediaControlServiceCallbacks mCallbacks; 140 private BluetoothGattServerProxy mBluetoothGattServer; 141 private BluetoothGattService mGattService = null; 142 private Handler mHandler = new Handler(Looper.getMainLooper()); 143 private Map<Integer, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>(); 144 private MediaState mCurrentMediaState = MediaState.INACTIVE; 145 private Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations = new HashMap<>(); 146 private McpService mMcpService; 147 private LeAudioService mLeAudioService; 148 private AdapterService mAdapterService; 149 mcsUuidToString(UUID uuid)150 private static String mcsUuidToString(UUID uuid) { 151 if (uuid.equals(UUID_PLAYER_NAME)) { 152 return "PLAYER_NAME"; 153 } else if (uuid.equals(UUID_PLAYER_ICON_OBJ_ID)) { 154 return "PLAYER_ICON_OBJ_ID"; 155 } else if (uuid.equals(UUID_PLAYER_ICON_URL)) { 156 return "PLAYER_ICON_URL"; 157 } else if (uuid.equals(UUID_TRACK_CHANGED)) { 158 return "TRACK_CHANGED"; 159 } else if (uuid.equals(UUID_TRACK_TITLE)) { 160 return "TRACK_TITLE"; 161 } else if (uuid.equals(UUID_TRACK_DURATION)) { 162 return "TRACK_DURATION"; 163 } else if (uuid.equals(UUID_TRACK_POSITION)) { 164 return "TRACK_POSITION"; 165 } else if (uuid.equals(UUID_PLAYBACK_SPEED)) { 166 return "PLAYBACK_SPEED"; 167 } else if (uuid.equals(UUID_SEEKING_SPEED)) { 168 return "SEEKING_SPEED"; 169 } else if (uuid.equals(UUID_CURRENT_TRACK_SEGMENT_OBJ_ID)) { 170 return "CURRENT_TRACK_SEGMENT_OBJ_ID"; 171 } else if (uuid.equals(UUID_CURRENT_TRACK_OBJ_ID)) { 172 return "CURRENT_TRACK_OBJ_ID"; 173 } else if (uuid.equals(UUID_NEXT_TRACK_OBJ_ID)) { 174 return "NEXT_TRACK_OBJ_ID"; 175 } else if (uuid.equals(UUID_CURRENT_GROUP_OBJ_ID)) { 176 return "CURRENT_GROUP_OBJ_ID"; 177 } else if (uuid.equals(UUID_PARENT_GROUP_OBJ_ID)) { 178 return "PARENT_GROUP_OBJ_ID"; 179 } else if (uuid.equals(UUID_PLAYING_ORDER)) { 180 return "PLAYING_ORDER"; 181 } else if (uuid.equals(UUID_PLAYING_ORDER_SUPPORTED)) { 182 return "PLAYING_ORDER_SUPPORTED"; 183 } else if (uuid.equals(UUID_MEDIA_STATE)) { 184 return "MEDIA_STATE"; 185 } else if (uuid.equals(UUID_MEDIA_CONTROL_POINT)) { 186 return "MEDIA_CONTROL_POINT"; 187 } else if (uuid.equals(UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { 188 return "MEDIA_CONTROL_POINT_OPCODES_SUPPORTED"; 189 } else if (uuid.equals(UUID_SEARCH_RESULT_OBJ_ID)) { 190 return "SEARCH_RESULT_OBJ_ID"; 191 } else if (uuid.equals(UUID_SEARCH_CONTROL_POINT)) { 192 return "SEARCH_CONTROL_POINT"; 193 } else if (uuid.equals(UUID_CONTENT_CONTROL_ID)) { 194 return "CONTENT_CONTROL_ID"; 195 } else { 196 return "UNKNOWN(" + uuid + ")"; 197 } 198 } 199 200 private static class GattOpContext { 201 public enum Operation { 202 READ_CHARACTERISTIC, 203 WRITE_CHARACTERISTIC, 204 READ_DESCRIPTOR, 205 WRITE_DESCRIPTOR, 206 } 207 GattOpContext(Operation operation, int requestId, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)208 GattOpContext(Operation operation, int requestId, 209 BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, 210 boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { 211 mOperation = operation; 212 mRequestId = requestId; 213 mCharacteristic = characteristic; 214 mDescriptor = descriptor; 215 mPreparedWrite = preparedWrite; 216 mResponseNeeded = responseNeeded; 217 mOffset = offset; 218 mValue = value; 219 } 220 GattOpContext(Operation operation, int requestId, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor)221 GattOpContext(Operation operation, int requestId, 222 BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor) { 223 mOperation = operation; 224 mRequestId = requestId; 225 mCharacteristic = characteristic; 226 mDescriptor = descriptor; 227 mPreparedWrite = false; 228 mResponseNeeded = false; 229 mOffset = 0; 230 mValue = null; 231 } 232 233 public Operation mOperation; 234 public int mRequestId; 235 public BluetoothGattCharacteristic mCharacteristic; 236 public BluetoothGattDescriptor mDescriptor; 237 public boolean mPreparedWrite; 238 public boolean mResponseNeeded; 239 public int mOffset; 240 public byte[] mValue; 241 } 242 243 private final Map<UUID, CharacteristicWriteHandler> mCharWriteCallback = Map.of( 244 UUID_TRACK_POSITION, 245 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 246 if (VDBG) { 247 Log.d(TAG, "TRACK_POSITION write request"); 248 } 249 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 250 if (value.length == 4) { 251 status = BluetoothGatt.GATT_SUCCESS; 252 ByteBuffer bb = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN); 253 handleTrackPositionRequest(bb.getInt()); 254 } 255 if (responseNeeded) { 256 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 257 } 258 }, 259 UUID_PLAYBACK_SPEED, 260 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 261 if (VDBG) { 262 Log.d(TAG, "PLAYBACK_SPEED write request"); 263 } 264 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 265 if (value.length == 1) { 266 status = BluetoothGatt.GATT_SUCCESS; 267 268 Integer intVal = characteristic.getIntValue( 269 BluetoothGattCharacteristic.FORMAT_SINT8, 0); 270 // Don't bother player with the same value 271 if (intVal == value[0]) { 272 notifyCharacteristic(characteristic, null); 273 } else { 274 handlePlaybackSpeedRequest(value[0]); 275 } 276 } 277 if (responseNeeded) { 278 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 279 } 280 }, 281 UUID_CURRENT_TRACK_OBJ_ID, 282 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 283 if (VDBG) { 284 Log.d(TAG, "CURRENT_TRACK_OBJ_ID write request"); 285 } 286 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 287 if (value.length == 6) { 288 status = BluetoothGatt.GATT_SUCCESS; 289 handleObjectIdRequest( 290 ObjectIds.CURRENT_TRACK_OBJ_ID, byteArray2ObjId(value)); 291 } 292 if (responseNeeded) { 293 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 294 } 295 }, 296 UUID_NEXT_TRACK_OBJ_ID, 297 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 298 if (VDBG) { 299 Log.d(TAG, "NEXT_TRACK_OBJ_ID write request"); 300 } 301 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 302 if (value.length == 6) { 303 status = BluetoothGatt.GATT_SUCCESS; 304 handleObjectIdRequest(ObjectIds.NEXT_TRACK_OBJ_ID, byteArray2ObjId(value)); 305 } 306 if (responseNeeded) { 307 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 308 } 309 }, 310 UUID_CURRENT_GROUP_OBJ_ID, 311 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 312 if (VDBG) { 313 Log.d(TAG, "CURRENT_GROUP_OBJ_ID write request"); 314 } 315 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 316 if (value.length == 6) { 317 status = BluetoothGatt.GATT_SUCCESS; 318 handleObjectIdRequest( 319 ObjectIds.CURRENT_GROUP_OBJ_ID, byteArray2ObjId(value)); 320 } 321 if (responseNeeded) { 322 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 323 } 324 }, 325 UUID_PLAYING_ORDER, 326 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 327 if (VDBG) { 328 Log.d(TAG, "PLAYING_ORDER write request"); 329 } 330 int status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 331 Integer currentPlayingOrder = null; 332 333 if (characteristic.getValue() != null) { 334 currentPlayingOrder = characteristic.getIntValue( 335 BluetoothGattCharacteristic.FORMAT_UINT8, 0); 336 } 337 338 if (value.length == 1 339 && (currentPlayingOrder == null || currentPlayingOrder != value[0])) { 340 status = BluetoothGatt.GATT_SUCCESS; 341 BluetoothGattCharacteristic supportedPlayingOrderChar = 342 mCharacteristics.get(CharId.PLAYING_ORDER_SUPPORTED); 343 Integer supportedPlayingOrder = 344 supportedPlayingOrderChar.getIntValue( 345 BluetoothGattCharacteristic.FORMAT_UINT16, 0); 346 347 if ((supportedPlayingOrder & (1 << (value[0] - 1))) != 0) { 348 handlePlayingOrderRequest(value[0]); 349 } 350 } 351 if (responseNeeded) { 352 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 353 } 354 }, 355 UUID_MEDIA_CONTROL_POINT, 356 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 357 if (VDBG) { 358 Log.d(TAG, "MEDIA_CONTROL_POINT write request"); 359 } 360 int status = handleMediaControlPointRequest(device, value); 361 if (responseNeeded) { 362 mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 363 } 364 }, 365 UUID_SEARCH_CONTROL_POINT, 366 (device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) -> { 367 if (VDBG) { 368 Log.d(TAG, "SEARCH_CONTROL_POINT write request"); 369 } 370 // TODO: There is no Object Trasfer Service implementation. 371 if (responseNeeded) { 372 mBluetoothGattServer.sendResponse(device, requestId, 0, offset, value); 373 } 374 }); 375 millisecondsToMcsInterval(long interval)376 private long millisecondsToMcsInterval(long interval) { 377 /* MCS presents time in 0.01s intervals */ 378 return interval / 10; 379 } 380 mcsIntervalToMilliseconds(long interval)381 private long mcsIntervalToMilliseconds(long interval) { 382 /* MCS presents time in 0.01s intervals */ 383 return interval * 10L; 384 } 385 getDeviceAuthorization(BluetoothDevice device)386 private int getDeviceAuthorization(BluetoothDevice device) { 387 return mMcpService.getDeviceAuthorization(device); 388 } 389 onUnauthorizedGattOperation(BluetoothDevice device, GattOpContext op)390 private void onUnauthorizedGattOperation(BluetoothDevice device, GattOpContext op) { 391 if (VDBG) { 392 Log.d(TAG, "onUnauthorizedGattOperation device: " + device); 393 } 394 395 synchronized (mPendingGattOperations) { 396 List<GattOpContext> operations = mPendingGattOperations.get(device); 397 if (operations == null) { 398 operations = new ArrayList<>(); 399 mPendingGattOperations.put(device, operations); 400 } 401 402 operations.add(op); 403 // Send authorization request for each device only for it's first GATT request 404 if (operations.size() == 1) { 405 mMcpService.onDeviceUnauthorized(device); 406 } 407 } 408 } 409 onAuthorizedGattOperation(BluetoothDevice device, GattOpContext op)410 private void onAuthorizedGattOperation(BluetoothDevice device, GattOpContext op) { 411 int status = BluetoothGatt.GATT_SUCCESS; 412 413 if (VDBG) { 414 Log.d(TAG, "onAuthorizedGattOperation device: " + device); 415 } 416 417 switch (op.mOperation) { 418 case READ_CHARACTERISTIC: 419 // Always ask for the latest position 420 if (op.mCharacteristic.getUuid().equals( 421 mCharacteristics.get(CharId.TRACK_POSITION).getUuid())) { 422 long positionMs = TRACK_POSITION_UNAVAILABLE; 423 positionMs = mCallbacks.onGetCurrentTrackPosition(); 424 final int position = (positionMs != TRACK_POSITION_UNAVAILABLE) 425 ? new Long(millisecondsToMcsInterval(positionMs)).intValue() 426 : INTERVAL_UNAVAILABLE; 427 428 ByteBuffer bb = 429 ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); 430 bb.putInt(position); 431 432 mBluetoothGattServer.sendResponse(device, op.mRequestId, 433 BluetoothGatt.GATT_SUCCESS, op.mOffset, 434 Arrays.copyOfRange(bb.array(), op.mOffset, Integer.BYTES)); 435 return; 436 } 437 438 if (op.mCharacteristic.getValue() != null) { 439 mBluetoothGattServer.sendResponse(device, op.mRequestId, 440 BluetoothGatt.GATT_SUCCESS, op.mOffset, 441 Arrays.copyOfRange(op.mCharacteristic.getValue(), op.mOffset, 442 op.mCharacteristic.getValue().length)); 443 } else { 444 Log.e(TAG, 445 "Missing characteristic value for char: " 446 + op.mCharacteristic.getUuid()); 447 mBluetoothGattServer.sendResponse(device, op.mRequestId, 448 BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH, op.mOffset, new byte[]{}); 449 } 450 break; 451 452 case WRITE_CHARACTERISTIC: 453 if (op.mPreparedWrite) { 454 status = BluetoothGatt.GATT_FAILURE; 455 } else if (op.mOffset > 0) { 456 status = BluetoothGatt.GATT_INVALID_OFFSET; 457 } else { 458 CharacteristicWriteHandler handler = 459 mCharWriteCallback.get(op.mCharacteristic.getUuid()); 460 handler.onCharacteristicWriteRequest( 461 device, op.mRequestId, op.mCharacteristic, op.mPreparedWrite, 462 op.mResponseNeeded, op.mOffset, op.mValue); 463 break; 464 } 465 466 if (op.mResponseNeeded) { 467 mBluetoothGattServer.sendResponse( 468 device, op.mRequestId, status, op.mOffset, op.mValue); 469 } 470 break; 471 472 case READ_DESCRIPTOR: 473 if (op.mOffset > 1) { 474 mBluetoothGattServer.sendResponse(device, op.mRequestId, 475 BluetoothGatt.GATT_INVALID_OFFSET, op.mOffset, null); 476 break; 477 } 478 479 byte[] value = getCccBytes(device, op.mDescriptor.getCharacteristic().getUuid()); 480 if (value == null) { 481 mBluetoothGattServer.sendResponse( 482 device, op.mRequestId, BluetoothGatt.GATT_FAILURE, op.mOffset, null); 483 break; 484 } 485 486 value = Arrays.copyOfRange(value, op.mOffset, value.length); 487 mBluetoothGattServer.sendResponse( 488 device, op.mRequestId, BluetoothGatt.GATT_SUCCESS, op.mOffset, value); 489 break; 490 491 case WRITE_DESCRIPTOR: 492 if (op.mPreparedWrite) { 493 status = BluetoothGatt.GATT_FAILURE; 494 } else if (op.mOffset > 0) { 495 status = BluetoothGatt.GATT_INVALID_OFFSET; 496 } else { 497 status = BluetoothGatt.GATT_SUCCESS; 498 setCcc(device, op.mDescriptor.getCharacteristic().getUuid(), op.mOffset, 499 op.mValue, true); 500 } 501 502 if (op.mResponseNeeded) { 503 mBluetoothGattServer.sendResponse( 504 device, op.mRequestId, status, op.mOffset, op.mValue); 505 } 506 break; 507 508 default: 509 break; 510 } 511 } 512 onRejectedAuthorizationGattOperation(BluetoothDevice device, GattOpContext op)513 private void onRejectedAuthorizationGattOperation(BluetoothDevice device, GattOpContext op) { 514 Log.w(TAG, "onRejectedAuthorizationGattOperation device: " + device); 515 516 switch (op.mOperation) { 517 case READ_CHARACTERISTIC: 518 case READ_DESCRIPTOR: 519 mBluetoothGattServer.sendResponse(device, op.mRequestId, 520 BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); 521 break; 522 case WRITE_CHARACTERISTIC: 523 if (op.mResponseNeeded) { 524 mBluetoothGattServer.sendResponse(device, op.mRequestId, 525 BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); 526 } else { 527 // In case of control point operations we can send an application error code 528 if (op.mCharacteristic.getUuid().equals(UUID_MEDIA_CONTROL_POINT)) { 529 setMediaControlRequestResult( 530 new Request(op.mValue[0], 0), 531 Request.Results.COMMAND_CANNOT_BE_COMPLETED); 532 } else if (op.mCharacteristic.getUuid().equals(UUID_SEARCH_CONTROL_POINT)) { 533 setSearchRequestResult(null, SearchRequest.Results.FAILURE, 0); 534 } 535 } 536 break; 537 case WRITE_DESCRIPTOR: 538 if (op.mResponseNeeded) { 539 mBluetoothGattServer.sendResponse(device, op.mRequestId, 540 BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION, op.mOffset, null); 541 } 542 break; 543 544 default: 545 break; 546 } 547 } 548 ClearUnauthorizedGattOperations(BluetoothDevice device)549 private void ClearUnauthorizedGattOperations(BluetoothDevice device) { 550 if (VDBG) { 551 Log.d(TAG, "ClearUnauthorizedGattOperations device: " + device); 552 } 553 554 synchronized (mPendingGattOperations) { 555 mPendingGattOperations.remove(device); 556 } 557 } 558 ProcessPendingGattOperations(BluetoothDevice device)559 private void ProcessPendingGattOperations(BluetoothDevice device) { 560 if (VDBG) { 561 Log.d(TAG, "ProcessPendingGattOperations device: " + device); 562 } 563 564 synchronized (mPendingGattOperations) { 565 if (mPendingGattOperations.containsKey(device)) { 566 if (getDeviceAuthorization(device) == BluetoothDevice.ACCESS_ALLOWED) { 567 for (GattOpContext op : mPendingGattOperations.get(device)) { 568 onAuthorizedGattOperation(device, op); 569 } 570 } else { 571 for (GattOpContext op : mPendingGattOperations.get(device)) { 572 onRejectedAuthorizationGattOperation(device, op); 573 } 574 } 575 ClearUnauthorizedGattOperations(device); 576 } 577 } 578 } 579 restoreCccValuesForStoredDevices()580 private void restoreCccValuesForStoredDevices() { 581 for (BluetoothDevice device : mAdapterService.getBondedDevices()) { 582 byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); 583 584 if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { 585 return; 586 } 587 588 List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)); 589 590 /* Restore CCCD values for device */ 591 for (ParcelUuid uuid : uuidList) { 592 setCcc(device, uuid.getUuid(), 0, 593 BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, false); 594 } 595 } 596 } 597 598 private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = 599 new IBluetoothStateChangeCallback.Stub() { 600 public void onBluetoothStateChange(boolean up) { 601 if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); 602 if (up) { 603 restoreCccValuesForStoredDevices(); 604 } 605 } 606 }; 607 608 @VisibleForTesting 609 final BluetoothGattServerCallback mServerCallback = new BluetoothGattServerCallback() { 610 @Override 611 public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { 612 super.onConnectionStateChange(device, status, newState); 613 if (VDBG) { 614 Log.d(TAG, "BluetoothGattServerCallback: onConnectionStateChange"); 615 } 616 if (newState == BluetoothProfile.STATE_DISCONNECTED) { 617 ClearUnauthorizedGattOperations(device); 618 } 619 } 620 621 @Override 622 public void onServiceAdded(int status, BluetoothGattService service) { 623 super.onServiceAdded(status, service); 624 if (VDBG) { 625 Log.d(TAG, "BluetoothGattServerCallback: onServiceAdded"); 626 } 627 628 if (mCallbacks != null) { 629 mCallbacks.onServiceInstanceRegistered((status != BluetoothGatt.GATT_SUCCESS) 630 ? ServiceStatus.UNKNOWN_ERROR 631 : ServiceStatus.OK, 632 MediaControlGattService.this); 633 } 634 635 mCharacteristics.get(CharId.CONTENT_CONTROL_ID) 636 .setValue(mCcid, BluetoothGattCharacteristic.FORMAT_UINT8, 0); 637 restoreCccValuesForStoredDevices(); 638 setInitialCharacteristicValuesAndNotify(); 639 initialStateRequest(); 640 } 641 642 @Override 643 public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, 644 BluetoothGattCharacteristic characteristic) { 645 super.onCharacteristicReadRequest(device, requestId, offset, characteristic); 646 if (VDBG) { 647 Log.d(TAG, "BluetoothGattServerCallback: onCharacteristicReadRequest offset= " 648 + offset + " entire value= " + Arrays.toString(characteristic.getValue())); 649 } 650 651 if ((characteristic.getProperties() & PROPERTY_READ) == 0) { 652 mBluetoothGattServer.sendResponse(device, requestId, 653 BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); 654 return; 655 } 656 657 GattOpContext op = new GattOpContext( 658 GattOpContext.Operation.READ_CHARACTERISTIC, requestId, characteristic, null); 659 switch (getDeviceAuthorization(device)) { 660 case BluetoothDevice.ACCESS_REJECTED: 661 onRejectedAuthorizationGattOperation(device, op); 662 break; 663 case BluetoothDevice.ACCESS_UNKNOWN: 664 onUnauthorizedGattOperation(device, op); 665 break; 666 default: 667 onAuthorizedGattOperation(device, op); 668 break; 669 } 670 } 671 672 @Override 673 public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, 674 BluetoothGattCharacteristic characteristic, boolean preparedWrite, 675 boolean responseNeeded, int offset, byte[] value) { 676 super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, 677 responseNeeded, offset, value); 678 if (VDBG) { 679 Log.d(TAG, 680 "BluetoothGattServerCallback: " 681 + "onCharacteristicWriteRequest"); 682 } 683 684 if ((characteristic.getProperties() & PROPERTY_WRITE) 685 == 0) { 686 mBluetoothGattServer.sendResponse( 687 device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value); 688 return; 689 } 690 691 GattOpContext op = new GattOpContext(GattOpContext.Operation.WRITE_CHARACTERISTIC, 692 requestId, characteristic, null, preparedWrite, responseNeeded, offset, value); 693 switch (getDeviceAuthorization(device)) { 694 case BluetoothDevice.ACCESS_REJECTED: 695 onRejectedAuthorizationGattOperation(device, op); 696 break; 697 case BluetoothDevice.ACCESS_UNKNOWN: 698 onUnauthorizedGattOperation(device, op); 699 break; 700 default: 701 onAuthorizedGattOperation(device, op); 702 break; 703 } 704 } 705 706 @Override 707 public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, 708 BluetoothGattDescriptor descriptor) { 709 super.onDescriptorReadRequest(device, requestId, offset, descriptor); 710 if (VDBG) { 711 Log.d(TAG, 712 "BluetoothGattServerCallback: " 713 + "onDescriptorReadRequest"); 714 } 715 716 if ((descriptor.getPermissions() & BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED) 717 == 0) { 718 mBluetoothGattServer.sendResponse( 719 device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset, null); 720 return; 721 } 722 723 GattOpContext op = new GattOpContext( 724 GattOpContext.Operation.READ_DESCRIPTOR, requestId, null, descriptor); 725 switch (getDeviceAuthorization(device)) { 726 case BluetoothDevice.ACCESS_REJECTED: 727 onRejectedAuthorizationGattOperation(device, op); 728 break; 729 case BluetoothDevice.ACCESS_UNKNOWN: 730 onUnauthorizedGattOperation(device, op); 731 break; 732 default: 733 onAuthorizedGattOperation(device, op); 734 break; 735 } 736 } 737 738 @Override 739 public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, 740 BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, 741 int offset, byte[] value) { 742 super.onDescriptorWriteRequest( 743 device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); 744 if (VDBG) { 745 Log.d(TAG, 746 "BluetoothGattServerCallback: " 747 + "onDescriptorWriteRequest"); 748 } 749 750 if ((descriptor.getPermissions() & BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED) 751 == 0) { 752 mBluetoothGattServer.sendResponse( 753 device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset, value); 754 return; 755 } 756 757 GattOpContext op = new GattOpContext(GattOpContext.Operation.WRITE_DESCRIPTOR, 758 requestId, null, descriptor, preparedWrite, responseNeeded, offset, value); 759 switch (getDeviceAuthorization(device)) { 760 case BluetoothDevice.ACCESS_REJECTED: 761 onRejectedAuthorizationGattOperation(device, op); 762 break; 763 case BluetoothDevice.ACCESS_UNKNOWN: 764 onUnauthorizedGattOperation(device, op); 765 break; 766 default: 767 onAuthorizedGattOperation(device, op); 768 break; 769 } 770 } 771 }; 772 initialStateRequest()773 private void initialStateRequest() { 774 List<PlayerStateField> field_list = new ArrayList<>(); 775 776 if (isFeatureSupported(ServiceFeature.MEDIA_STATE)) { 777 field_list.add(PlayerStateField.PLAYBACK_STATE); 778 } 779 780 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { 781 field_list.add(PlayerStateField.ICON_URL); 782 } 783 784 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { 785 field_list.add(PlayerStateField.ICON_OBJ_ID); 786 } 787 788 if (isFeatureSupported(ServiceFeature.PLAYER_NAME)) { 789 field_list.add(PlayerStateField.PLAYER_NAME); 790 } 791 792 if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { 793 field_list.add(PlayerStateField.PLAYING_ORDER_SUPPORTED); 794 } 795 796 mCallbacks.onPlayerStateRequest(field_list.stream().toArray(PlayerStateField[]::new)); 797 } 798 setInitialCharacteristicValues(boolean notify)799 private void setInitialCharacteristicValues(boolean notify) { 800 updateMediaStateChar(mCurrentMediaState.getValue()); 801 updatePlayerNameChar("", notify); 802 updatePlayerIconUrlChar(""); 803 804 // Object IDs will have a length of 0; 805 updateObjectID(ObjectIds.PLAYER_ICON_OBJ_ID, -1, notify); 806 updateObjectID(ObjectIds.CURRENT_TRACK_SEGMENT_OBJ_ID, -1, notify); 807 updateObjectID(ObjectIds.CURRENT_TRACK_OBJ_ID, -1, notify); 808 updateObjectID(ObjectIds.NEXT_TRACK_OBJ_ID, -1, notify); 809 updateObjectID(ObjectIds.CURRENT_GROUP_OBJ_ID, -1, notify); 810 updateObjectID(ObjectIds.PARENT_GROUP_OBJ_ID, -1, notify); 811 updateObjectID(ObjectIds.SEARCH_RESULT_OBJ_ID, -1, notify); 812 updateTrackTitleChar("", notify); 813 updateTrackDurationChar(TRACK_DURATION_UNAVAILABLE, notify); 814 updateTrackPositionChar(TRACK_POSITION_UNAVAILABLE, notify); 815 updatePlaybackSpeedChar(1, notify); 816 updateSeekingSpeedChar(1, notify); 817 updatePlayingOrderSupportedChar(SupportedPlayingOrder.SINGLE_ONCE); 818 updatePlayingOrderChar(PlayingOrder.SINGLE_ONCE, notify); 819 updateSupportedOpcodesChar(Request.SupportedOpcodes.NONE, notify); 820 } 821 setInitialCharacteristicValues()822 private void setInitialCharacteristicValues() { 823 setInitialCharacteristicValues(false); 824 } 825 setInitialCharacteristicValuesAndNotify()826 private void setInitialCharacteristicValuesAndNotify() { 827 setInitialCharacteristicValues(true); 828 } 829 830 /** 831 * A proxy class that facilitates testing of the McpService class. 832 * 833 * This is necessary due to the "final" attribute of the BluetoothGattServer class. In order to 834 * test the correct functioning of the McpService class, the final class must be put into a 835 * container that can be mocked correctly. 836 */ 837 public class BluetoothGattServerProxy { 838 private BluetoothGattServer mBluetoothGattServer; 839 private BluetoothManager mBluetoothManager; 840 BluetoothGattServerProxy(BluetoothGattServer gatt, BluetoothManager manager)841 public BluetoothGattServerProxy(BluetoothGattServer gatt, BluetoothManager manager) { 842 mBluetoothManager = manager; 843 mBluetoothGattServer = gatt; 844 } 845 addService(BluetoothGattService service)846 public boolean addService(BluetoothGattService service) { 847 return mBluetoothGattServer.addService(service); 848 } 849 removeService(BluetoothGattService service)850 public boolean removeService(BluetoothGattService service) { 851 return mBluetoothGattServer.removeService(service); 852 } 853 close()854 public void close() { 855 mBluetoothGattServer.close(); 856 } 857 sendResponse( BluetoothDevice device, int requestId, int status, int offset, byte[] value)858 public boolean sendResponse( 859 BluetoothDevice device, int requestId, int status, int offset, byte[] value) { 860 return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value); 861 } 862 notifyCharacteristicChanged(BluetoothDevice device, BluetoothGattCharacteristic characteristic, boolean confirm)863 public boolean notifyCharacteristicChanged(BluetoothDevice device, 864 BluetoothGattCharacteristic characteristic, boolean confirm) { 865 return mBluetoothGattServer.notifyCharacteristicChanged( 866 device, characteristic, confirm); 867 } 868 getConnectedDevices()869 public List<BluetoothDevice> getConnectedDevices() { 870 return mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER); 871 } 872 } 873 MediaControlGattService(McpService mcpService, @NonNull MediaControlServiceCallbacks callbacks, int ccid)874 protected MediaControlGattService(McpService mcpService, 875 @NonNull MediaControlServiceCallbacks callbacks, int ccid) { 876 mContext = mcpService; 877 mCallbacks = callbacks; 878 mCcid = ccid; 879 880 mMcpService = mcpService; 881 mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), 882 "AdapterService shouldn't be null when creating MediaControlCattService"); 883 884 IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager(); 885 if (mgr != null) { 886 try { 887 mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); 888 } catch (RemoteException e) { 889 throw e.rethrowFromSystemServer(); 890 } 891 } 892 } 893 init(UUID scvUuid)894 protected boolean init(UUID scvUuid) { 895 mCccDescriptorValues = new HashMap<>(); 896 897 mFeatures = mCallbacks.onGetFeatureFlags(); 898 899 // Verify the minimum required set of supported player features 900 if ((mFeatures & ServiceFeature.ALL_MANDATORY_SERVICE_FEATURES) 901 != ServiceFeature.ALL_MANDATORY_SERVICE_FEATURES) { 902 mCallbacks.onServiceInstanceRegistered(ServiceStatus.INVALID_FEATURE_FLAGS, null); 903 return false; 904 } 905 906 // Init attribute database 907 return initGattService(scvUuid); 908 } 909 handleObjectIdRequest(int objField, long objId)910 private void handleObjectIdRequest(int objField, long objId) { 911 mCallbacks.onSetObjectIdRequest(objField, objId); 912 } 913 handlePlayingOrderRequest(int order)914 private void handlePlayingOrderRequest(int order) { 915 mCallbacks.onPlayingOrderSetRequest(order); 916 } 917 handlePlaybackSpeedRequest(int speed)918 private void handlePlaybackSpeedRequest(int speed) { 919 float floatingSpeed = (float) Math.pow(2, speed / 64); 920 mCallbacks.onPlaybackSpeedSetRequest(floatingSpeed); 921 } 922 handleTrackPositionRequest(long position)923 private void handleTrackPositionRequest(long position) { 924 final long positionMs = (position != INTERVAL_UNAVAILABLE) 925 ? mcsIntervalToMilliseconds(position) 926 : TRACK_POSITION_UNAVAILABLE; 927 928 mCallbacks.onTrackPositionSetRequest(positionMs); 929 } 930 getMediaControlPointRequestPayloadLength(int opcode)931 private static int getMediaControlPointRequestPayloadLength(int opcode) { 932 switch (opcode) { 933 case Request.Opcodes.MOVE_RELATIVE: 934 case Request.Opcodes.GOTO_SEGMENT: 935 case Request.Opcodes.GOTO_TRACK: 936 case Request.Opcodes.GOTO_GROUP: 937 return 4; 938 default: 939 return 0; 940 } 941 } 942 943 @VisibleForTesting handleMediaControlPointRequest(BluetoothDevice device, byte[] value)944 int handleMediaControlPointRequest(BluetoothDevice device, byte[] value) { 945 final int payloadOffset = 1; 946 final int opcode = value[0]; 947 948 // Test for RFU bits and currently supported opcodes 949 if (!isOpcodeSupported(opcode)) { 950 Log.i(TAG, "handleMediaControlPointRequest: " + Request.Opcodes.toString(opcode) 951 + " not supported"); 952 mHandler.post(() -> { 953 setMediaControlRequestResult(new Request(opcode, 0), 954 Request.Results.OPCODE_NOT_SUPPORTED); 955 }); 956 return BluetoothGatt.GATT_SUCCESS; 957 } 958 959 if (getMediaControlPointRequestPayloadLength(opcode) != (value.length - payloadOffset)) { 960 Log.w(TAG, "handleMediaControlPointRequest: " + Request.Opcodes.toString(opcode) 961 + " bad payload length"); 962 return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 963 } 964 965 // Only some requests have payload 966 int intVal = 0; 967 if (opcode == Request.Opcodes.MOVE_RELATIVE 968 || opcode == Request.Opcodes.GOTO_SEGMENT 969 || opcode == Request.Opcodes.GOTO_TRACK 970 || opcode == Request.Opcodes.GOTO_GROUP) { 971 intVal = ByteBuffer.wrap(value, payloadOffset, value.length - payloadOffset) 972 .order(ByteOrder.LITTLE_ENDIAN) 973 .getInt(); 974 975 // If the argument is time interval, convert to milliseconds time domain 976 if (opcode == Request.Opcodes.MOVE_RELATIVE) { 977 intVal = new Long(mcsIntervalToMilliseconds(intVal)).intValue(); 978 } 979 } 980 981 Request req = new Request(opcode, intVal); 982 983 if (DBG) { 984 Log.d(TAG, "handleMediaControlPointRequest: sending " + Request.Opcodes.toString(opcode) 985 + " request up"); 986 } 987 988 // TODO: Activate/deactivate devices with ActiveDeviceManager 989 if (req.getOpcode() == Request.Opcodes.PLAY) { 990 if (mAdapterService.getActiveDevices(BluetoothProfile.A2DP).size() > 0) { 991 A2dpService.getA2dpService().removeActiveDevice(false); 992 } 993 if (mAdapterService.getActiveDevices(BluetoothProfile.HEARING_AID).size() > 0) { 994 HearingAidService.getHearingAidService().removeActiveDevice(false); 995 } 996 if (mLeAudioService == null) { 997 mLeAudioService = LeAudioService.getLeAudioService(); 998 } 999 mLeAudioService.setActiveDevice(device); 1000 } 1001 mCallbacks.onMediaControlRequest(req); 1002 1003 return BluetoothGatt.GATT_SUCCESS; 1004 } 1005 setCallbacks(MediaControlServiceCallbacks callbacks)1006 public void setCallbacks(MediaControlServiceCallbacks callbacks) { 1007 mCallbacks = callbacks; 1008 } 1009 1010 @VisibleForTesting setServiceManagerForTesting(McpService manager)1011 protected void setServiceManagerForTesting(McpService manager) { 1012 mMcpService = manager; 1013 } 1014 1015 @VisibleForTesting setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy)1016 void setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy) { 1017 mBluetoothGattServer = proxy; 1018 } 1019 1020 @VisibleForTesting setLeAudioServiceForTesting(LeAudioService leAudioService)1021 void setLeAudioServiceForTesting(LeAudioService leAudioService) { 1022 mLeAudioService = leAudioService; 1023 } 1024 initGattService(UUID serviceUuid)1025 private boolean initGattService(UUID serviceUuid) { 1026 if (DBG) { 1027 Log.d(TAG, "initGattService uuid: " + serviceUuid); 1028 } 1029 1030 if (mBluetoothGattServer == null) { 1031 BluetoothManager manager = mContext.getSystemService(BluetoothManager.class); 1032 BluetoothGattServer server = manager.openGattServer(mContext, mServerCallback); 1033 if (server == null) { 1034 Log.e(TAG, "Failed to start BluetoothGattServer for MCP"); 1035 //TODO: This now effectively makes MCP unusable, but fixes tests 1036 // Handle this error more gracefully, verify BluetoothInstrumentationTests 1037 // are passing after fix is applied 1038 return false; 1039 } 1040 mBluetoothGattServer = new BluetoothGattServerProxy(server, manager); 1041 } 1042 1043 mGattService = 1044 new BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY); 1045 1046 for (Pair<UUID, CharacteristicData> entry : getUuidCharacteristicList()) { 1047 CharacteristicData desc = entry.second; 1048 UUID uuid = entry.first; 1049 if (VDBG) { 1050 Log.d(TAG, "Checking uuid: " + uuid); 1051 } 1052 if ((mFeatures & desc.featureFlag) != 0) { 1053 int notifyProp = (((mFeatures & desc.ntfFeatureFlag) != 0) 1054 ? PROPERTY_NOTIFY 1055 : 0); 1056 1057 BluetoothGattCharacteristic myChar = new BluetoothGattCharacteristic( 1058 uuid, desc.properties | notifyProp, desc.permissions); 1059 1060 // Add CCC descriptor if notification is supported 1061 if ((myChar.getProperties() & PROPERTY_NOTIFY) != 0) { 1062 BluetoothGattDescriptor cccDesc = new BluetoothGattDescriptor(UUID_CCCD, 1063 BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED 1064 | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED); 1065 if (VDBG) { 1066 Log.d(TAG, "Adding descriptor: " + cccDesc); 1067 } 1068 myChar.addDescriptor(cccDesc); 1069 } 1070 1071 if (VDBG) { 1072 Log.d(TAG, "Adding char: " + myChar); 1073 } 1074 mCharacteristics.put(desc.id, myChar); 1075 mGattService.addCharacteristic(myChar); 1076 } 1077 } 1078 if (VDBG) { 1079 Log.d(TAG, "Adding service: " + mGattService); 1080 } 1081 return mBluetoothGattServer.addService(mGattService); 1082 } 1083 removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device)1084 private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) { 1085 List<ParcelUuid> uuidList; 1086 byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); 1087 1088 if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { 1089 uuidList = new ArrayList<ParcelUuid>(); 1090 } else { 1091 uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd))); 1092 1093 if (!uuidList.contains(charUuid)) { 1094 Log.d(TAG, "Characteristic CCCD can't be removed (not cached): " 1095 + charUuid.toString()); 1096 return; 1097 } 1098 } 1099 1100 uuidList.remove(charUuid); 1101 1102 if (!device.setMetadata(METADATA_GMCS_CCCD, 1103 Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) { 1104 Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString() 1105 + ", (remove)"); 1106 } 1107 } 1108 addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device)1109 private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) { 1110 List<ParcelUuid> uuidList; 1111 byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD); 1112 1113 if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) { 1114 uuidList = new ArrayList<ParcelUuid>(); 1115 } else { 1116 uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd))); 1117 1118 if (uuidList.contains(charUuid)) { 1119 Log.d(TAG, "Characteristic CCCD already added: " + charUuid.toString()); 1120 return; 1121 } 1122 } 1123 1124 uuidList.add(charUuid); 1125 1126 if (!device.setMetadata(METADATA_GMCS_CCCD, 1127 Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) { 1128 Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString() 1129 + ", (add)"); 1130 } 1131 } 1132 1133 @VisibleForTesting setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value, boolean store)1134 void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value, boolean store) { 1135 HashMap<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress()); 1136 if (characteristicCcc == null) { 1137 characteristicCcc = new HashMap<>(); 1138 mCccDescriptorValues.put(device.getAddress(), characteristicCcc); 1139 } 1140 1141 characteristicCcc.put(charUuid, 1142 ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort()); 1143 1144 if (!store) { 1145 return; 1146 } 1147 1148 if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { 1149 addUuidToMetadata(new ParcelUuid(charUuid), device); 1150 } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) { 1151 removeUuidFromMetadata(new ParcelUuid(charUuid), device); 1152 } else { 1153 Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value)); 1154 } 1155 } 1156 getCccBytes(BluetoothDevice device, UUID charUuid)1157 private byte[] getCccBytes(BluetoothDevice device, UUID charUuid) { 1158 Map<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress()); 1159 if (characteristicCcc != null) { 1160 ByteBuffer bb = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN); 1161 Short ccc = characteristicCcc.get(charUuid); 1162 if (ccc != null) { 1163 bb.putShort(characteristicCcc.get(charUuid)); 1164 return bb.array(); 1165 } 1166 } 1167 return BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; 1168 } 1169 1170 @Override updatePlaybackState(MediaState state)1171 public void updatePlaybackState(MediaState state) { 1172 if (DBG) { 1173 Log.d(TAG, "updatePlaybackState"); 1174 } 1175 1176 if ((state.getValue() <= MediaState.STATE_MAX.getValue()) 1177 && (state.getValue() >= MediaState.STATE_MIN.getValue())) { 1178 updateMediaStateChar(state.getValue()); 1179 } 1180 } 1181 1182 @VisibleForTesting getMediaStateChar()1183 int getMediaStateChar() { 1184 if (!isFeatureSupported(ServiceFeature.MEDIA_STATE)) return MediaState.INACTIVE.getValue(); 1185 1186 BluetoothGattCharacteristic stateChar = 1187 mCharacteristics.get(CharId.MEDIA_STATE); 1188 1189 if (stateChar.getValue() != null) { 1190 return stateChar.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); 1191 } 1192 1193 return MediaState.INACTIVE.getValue(); 1194 } 1195 1196 @VisibleForTesting updateMediaStateChar(int state)1197 void updateMediaStateChar(int state) { 1198 if (DBG) { 1199 Log.d(TAG, "updateMediaStateChar"); 1200 } 1201 1202 if (!isFeatureSupported(ServiceFeature.MEDIA_STATE)) return; 1203 1204 if (DBG) { 1205 Log.d(TAG, "updateMediaStateChar setting to state= " + state); 1206 } 1207 1208 BluetoothGattCharacteristic stateChar = 1209 mCharacteristics.get(CharId.MEDIA_STATE); 1210 stateChar.setValue(state, BluetoothGattCharacteristic.FORMAT_UINT8, 0); 1211 notifyCharacteristic(stateChar, null); 1212 } 1213 updateObjectID(int objectIdField, long objectIdValue, boolean notify)1214 private void updateObjectID(int objectIdField, long objectIdValue, boolean notify) { 1215 if (DBG) { 1216 Log.d(TAG, "updateObjectID"); 1217 } 1218 int feature = ObjectIds.GetMatchingServiceFeature(objectIdField); 1219 1220 if (!isFeatureSupported(feature)) return; 1221 1222 updateObjectIdChar(mCharacteristics.get(CharId.FromFeature(feature)), 1223 objectIdValue, null, notify); 1224 } 1225 1226 @Override updateObjectID(int objectIdField, long objectIdValue)1227 public void updateObjectID(int objectIdField, long objectIdValue) { 1228 updateObjectID(objectIdField, objectIdValue, true); 1229 } 1230 1231 @Override setMediaControlRequestResult(Request request, Request.Results resultStatus)1232 public void setMediaControlRequestResult(Request request, 1233 Request.Results resultStatus) { 1234 if (DBG) { 1235 Log.d(TAG, "setMediaControlRequestResult"); 1236 } 1237 1238 if (getMediaStateChar() == MediaState.INACTIVE.getValue()) { 1239 resultStatus = Request.Results.MEDIA_PLAYER_INACTIVE; 1240 } 1241 1242 ByteBuffer bb = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN); 1243 bb.put((byte) request.getOpcode()); 1244 bb.put((byte) resultStatus.getValue()); 1245 1246 BluetoothGattCharacteristic characteristic = 1247 mCharacteristics.get(CharId.MEDIA_CONTROL_POINT); 1248 characteristic.setValue(bb.array()); 1249 notifyCharacteristic(characteristic, null); 1250 } 1251 1252 @Override setSearchRequestResult(SearchRequest request, SearchRequest.Results resultStatus, long resultObjectId)1253 public void setSearchRequestResult(SearchRequest request, 1254 SearchRequest.Results resultStatus, long resultObjectId) { 1255 if (DBG) { 1256 Log.d(TAG, "setSearchRequestResult"); 1257 } 1258 1259 // TODO: There is no Object Trasfer Service implementation. 1260 BluetoothGattCharacteristic characteristic = 1261 mCharacteristics.get(CharId.SEARCH_CONTROL_POINT); 1262 characteristic.setValue(new byte[]{SEARCH_CONTROL_POINT_RESULT_FAILURE}); 1263 notifyCharacteristic(characteristic, null); 1264 } 1265 1266 @Override updatePlayerState(Map stateFields)1267 public void updatePlayerState(Map stateFields) { 1268 if (stateFields.isEmpty()) { 1269 return; 1270 } 1271 1272 if (stateFields.containsKey(PlayerStateField.PLAYBACK_STATE)) { 1273 MediaState playbackState = 1274 (MediaState) stateFields.get(PlayerStateField.PLAYBACK_STATE); 1275 if (DBG) { 1276 Log.d(TAG, 1277 "updatePlayerState: playbackState= " 1278 + stateFields.get(PlayerStateField.PLAYBACK_STATE)); 1279 } 1280 1281 if (playbackState == MediaState.INACTIVE) { 1282 setInitialCharacteristicValues(); 1283 } 1284 } 1285 final boolean doNotifyValueChange = true; 1286 1287 // Additional fields that may be requested by the service to complete the new state info 1288 List<PlayerStateField> reqFieldList = null; 1289 1290 if (stateFields.containsKey(PlayerStateField.PLAYBACK_SPEED)) { 1291 updatePlaybackSpeedChar( 1292 (float) stateFields.get(PlayerStateField.PLAYBACK_SPEED), doNotifyValueChange); 1293 } 1294 1295 if (stateFields.containsKey(PlayerStateField.PLAYING_ORDER_SUPPORTED)) { 1296 updatePlayingOrderSupportedChar( 1297 (Integer) stateFields.get(PlayerStateField.PLAYING_ORDER_SUPPORTED)); 1298 } 1299 1300 if (stateFields.containsKey(PlayerStateField.PLAYING_ORDER)) { 1301 updatePlayingOrderChar((PlayingOrder) stateFields.get(PlayerStateField.PLAYING_ORDER), 1302 doNotifyValueChange); 1303 } 1304 1305 if (stateFields.containsKey(PlayerStateField.TRACK_POSITION)) { 1306 updateTrackPositionChar( 1307 (long) stateFields.get(PlayerStateField.TRACK_POSITION), doNotifyValueChange); 1308 } 1309 1310 if (stateFields.containsKey(PlayerStateField.PLAYER_NAME)) { 1311 String name = (String) stateFields.get(PlayerStateField.PLAYER_NAME); 1312 if ((getPlayerNameChar() != null) && (name.compareTo(getPlayerNameChar()) != 0)) { 1313 updatePlayerNameChar(name, doNotifyValueChange); 1314 1315 // Most likely the player has changed - request critical info fields 1316 reqFieldList = new ArrayList<>(); 1317 reqFieldList.add(PlayerStateField.PLAYBACK_STATE); 1318 reqFieldList.add(PlayerStateField.TRACK_DURATION); 1319 1320 if (isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { 1321 reqFieldList.add(PlayerStateField.OPCODES_SUPPORTED); 1322 } 1323 if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { 1324 reqFieldList.add(PlayerStateField.PLAYING_ORDER_SUPPORTED); 1325 } 1326 if (isFeatureSupported(ServiceFeature.PLAYING_ORDER)) { 1327 reqFieldList.add(PlayerStateField.PLAYING_ORDER); 1328 } 1329 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { 1330 reqFieldList.add(PlayerStateField.ICON_OBJ_ID); 1331 } 1332 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { 1333 reqFieldList.add(PlayerStateField.ICON_URL); 1334 } 1335 } 1336 } 1337 1338 if (stateFields.containsKey(PlayerStateField.ICON_URL)) { 1339 updatePlayerIconUrlChar((String) stateFields.get(PlayerStateField.ICON_URL)); 1340 } 1341 1342 if (stateFields.containsKey(PlayerStateField.ICON_OBJ_ID)) { 1343 updateIconObjIdChar((Long) stateFields.get(PlayerStateField.ICON_OBJ_ID)); 1344 } 1345 1346 if (stateFields.containsKey(PlayerStateField.OPCODES_SUPPORTED)) { 1347 updateSupportedOpcodesChar( 1348 (Integer) stateFields.get(PlayerStateField.OPCODES_SUPPORTED), 1349 doNotifyValueChange); 1350 } 1351 1352 // Notify track change if any of these have changed 1353 boolean notifyTrackChange = false; 1354 if (stateFields.containsKey(PlayerStateField.TRACK_TITLE)) { 1355 String newTitle = (String) stateFields.get(PlayerStateField.TRACK_TITLE); 1356 1357 if (getTrackTitleChar().compareTo(newTitle) != 0) { 1358 updateTrackTitleChar((String) stateFields.get(PlayerStateField.TRACK_TITLE), 1359 doNotifyValueChange); 1360 notifyTrackChange = true; 1361 } 1362 } 1363 1364 if (stateFields.containsKey(PlayerStateField.TRACK_DURATION)) { 1365 long newTrackDuration = (long) (stateFields.get(PlayerStateField.TRACK_DURATION)); 1366 if (getTrackDurationChar() != newTrackDuration) { 1367 updateTrackDurationChar(newTrackDuration, doNotifyValueChange); 1368 notifyTrackChange = true; 1369 } 1370 } 1371 1372 if (stateFields.containsKey(PlayerStateField.PLAYBACK_STATE)) { 1373 mCurrentMediaState = 1374 (MediaState) stateFields.get(PlayerStateField.PLAYBACK_STATE); 1375 } 1376 1377 int mediaState = getMediaStateChar(); 1378 if (mediaState != mCurrentMediaState.getValue()) { 1379 updateMediaStateChar(mCurrentMediaState.getValue()); 1380 } 1381 1382 if (stateFields.containsKey(PlayerStateField.SEEKING_SPEED)) { 1383 int playbackState = getMediaStateChar(); 1384 // Seeking speed should be 1.0f (char. value of 0) when not in seeking state. 1385 // [Ref. Media Control Service v1.0, sec. 3.9] 1386 if (playbackState == MediaState.SEEKING.getValue()) { 1387 updateSeekingSpeedChar((float) stateFields.get(PlayerStateField.SEEKING_SPEED), 1388 doNotifyValueChange); 1389 } else { 1390 updateSeekingSpeedChar(1.0f, doNotifyValueChange); 1391 } 1392 } 1393 1394 // Notify track change as the last step of all track change related characteristic changes. 1395 // [Ref. Media Control Service v1.0, sec. 3.4.1] 1396 if (notifyTrackChange) { 1397 if (isFeatureSupported(ServiceFeature.TRACK_CHANGED)) { 1398 BluetoothGattCharacteristic myChar = 1399 mCharacteristics.get(CharId.TRACK_CHANGED); 1400 myChar.setValue(new byte[]{}); 1401 notifyCharacteristic(myChar, null); 1402 } 1403 } 1404 1405 if (reqFieldList != null) { 1406 // Don't ask for those that we just got. 1407 reqFieldList.removeAll(stateFields.keySet()); 1408 1409 if (!reqFieldList.isEmpty()) { 1410 mCallbacks.onPlayerStateRequest( 1411 reqFieldList.stream().toArray(PlayerStateField[]::new)); 1412 } 1413 } 1414 } 1415 1416 @Override getContentControlId()1417 public int getContentControlId() { 1418 return mCcid; 1419 } 1420 1421 @Override onDeviceAuthorizationSet(BluetoothDevice device)1422 public void onDeviceAuthorizationSet(BluetoothDevice device) { 1423 ProcessPendingGattOperations(device); 1424 } 1425 1426 @Override destroy()1427 public void destroy() { 1428 if (DBG) { 1429 Log.d(TAG, "Destroy"); 1430 } 1431 1432 if (mBluetoothGattServer == null) { 1433 return; 1434 } 1435 1436 if (mBluetoothGattServer.removeService(mGattService)) { 1437 if (mCallbacks != null) { 1438 mCallbacks.onServiceInstanceUnregistered(ServiceStatus.OK); 1439 } 1440 } 1441 1442 mBluetoothGattServer.close(); 1443 } 1444 1445 @VisibleForTesting updatePlayingOrderChar(PlayingOrder order, boolean notify)1446 void updatePlayingOrderChar(PlayingOrder order, boolean notify) { 1447 if (VDBG) { 1448 Log.d(TAG, "updatePlayingOrderChar: " + order); 1449 } 1450 if (!isFeatureSupported(ServiceFeature.PLAYING_ORDER)) return; 1451 1452 BluetoothGattCharacteristic orderChar = mCharacteristics.get(CharId.PLAYING_ORDER); 1453 Integer playingOrder = null; 1454 1455 if (orderChar.getValue() != null) { 1456 playingOrder = orderChar.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); 1457 } 1458 1459 if ((playingOrder == null) || (playingOrder != order.getValue())) { 1460 orderChar.setValue(order.getValue(), BluetoothGattCharacteristic.FORMAT_UINT8, 0); 1461 if (notify && isFeatureSupported(ServiceFeature.PLAYING_ORDER_NOTIFY)) { 1462 notifyCharacteristic(orderChar, null); 1463 } 1464 } 1465 } 1466 notifyCharacteristic(@onNull BluetoothGattCharacteristic characteristic, @Nullable BluetoothDevice originDevice)1467 private void notifyCharacteristic(@NonNull BluetoothGattCharacteristic characteristic, 1468 @Nullable BluetoothDevice originDevice) { 1469 for (BluetoothDevice device : mBluetoothGattServer.getConnectedDevices()) { 1470 // Skip the origin device who changed the characteristic 1471 if (device.equals(originDevice)) { 1472 continue; 1473 } 1474 1475 HashMap<UUID, Short> charCccMap = mCccDescriptorValues.get(device.getAddress()); 1476 if (charCccMap == null) continue; 1477 1478 byte[] ccc = getCccBytes(device, characteristic.getUuid()); 1479 if (VDBG) { 1480 Log.d(TAG, "notifyCharacteristic char= " + characteristic.getUuid().toString() 1481 + " cccVal= " 1482 + ByteBuffer.wrap(ccc).order(ByteOrder.LITTLE_ENDIAN).getShort()); 1483 } 1484 1485 if (!Arrays.equals(ccc, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) continue; 1486 1487 if (VDBG) Log.d(TAG, "notifyCharacteristic sending notification"); 1488 1489 mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false); 1490 } 1491 } 1492 SpeedFloatToCharacteristicIntValue(float speed)1493 private static int SpeedFloatToCharacteristicIntValue(float speed) { 1494 /* The spec. defined valid speed range is <0.25, 3.957> as float input, resulting in 1495 * <-128, 127> output integer range. */ 1496 if (speed < 0) { 1497 speed = -speed; 1498 } 1499 if (speed < PLAY_SPEED_MIN) { 1500 speed = PLAY_SPEED_MIN; 1501 } else if (speed > PLAY_SPEED_MAX) { 1502 speed = PLAY_SPEED_MAX; 1503 } 1504 1505 return new Float(64 * Math.log(speed) / Math.log(2)).intValue(); 1506 } 1507 CharacteristicSpeedIntValueToSpeedFloat(Integer speed)1508 private static float CharacteristicSpeedIntValueToSpeedFloat(Integer speed) { 1509 return new Float(Math.pow(2, (speed.floatValue() / 64.0f))); 1510 } 1511 1512 @VisibleForTesting getSeekingSpeedChar()1513 Float getSeekingSpeedChar() { 1514 Float speed = null; 1515 1516 if (isFeatureSupported(ServiceFeature.SEEKING_SPEED)) { 1517 BluetoothGattCharacteristic characteristic = 1518 mCharacteristics.get(CharId.SEEKING_SPEED); 1519 if (characteristic.getValue() != null) { 1520 Integer intVal = 1521 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT8, 0); 1522 speed = CharacteristicSpeedIntValueToSpeedFloat(intVal); 1523 } 1524 } 1525 1526 return speed; 1527 } 1528 1529 @VisibleForTesting updateSeekingSpeedChar(float speed, boolean notify)1530 void updateSeekingSpeedChar(float speed, boolean notify) { 1531 if (VDBG) { 1532 Log.d(TAG, "updateSeekingSpeedChar: " + speed); 1533 } 1534 if (isFeatureSupported(ServiceFeature.SEEKING_SPEED)) { 1535 if ((getSeekingSpeedChar() == null) || (getSeekingSpeedChar() != speed)) { 1536 BluetoothGattCharacteristic characteristic = 1537 mCharacteristics.get(CharId.SEEKING_SPEED); 1538 int intSpeed = SpeedFloatToCharacteristicIntValue(speed); 1539 characteristic.setValue(intSpeed, BluetoothGattCharacteristic.FORMAT_SINT8, 0); 1540 if (notify && isFeatureSupported(ServiceFeature.SEEKING_SPEED_NOTIFY)) { 1541 notifyCharacteristic(characteristic, null); 1542 } 1543 } 1544 } 1545 } 1546 1547 @VisibleForTesting getPlaybackSpeedChar()1548 Float getPlaybackSpeedChar() { 1549 Float speed = null; 1550 1551 if (!isFeatureSupported(ServiceFeature.PLAYBACK_SPEED)) return null; 1552 1553 BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.PLAYBACK_SPEED); 1554 if (characteristic.getValue() != null) { 1555 Integer intVal = 1556 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT8, 0); 1557 speed = CharacteristicSpeedIntValueToSpeedFloat(intVal); 1558 } 1559 1560 return speed; 1561 } 1562 1563 @VisibleForTesting updatePlaybackSpeedChar(float speed, boolean notify)1564 void updatePlaybackSpeedChar(float speed, boolean notify) { 1565 if (VDBG) { 1566 Log.d(TAG, "updatePlaybackSpeedChar: " + speed); 1567 } 1568 1569 if (!isFeatureSupported(ServiceFeature.PLAYBACK_SPEED)) return; 1570 1571 // Reject if no changes were made 1572 if ((getPlaybackSpeedChar() == null) || (getPlaybackSpeedChar() != speed)) { 1573 BluetoothGattCharacteristic characteristic = 1574 mCharacteristics.get(CharId.PLAYBACK_SPEED); 1575 int intSpeed = SpeedFloatToCharacteristicIntValue(speed); 1576 characteristic.setValue(intSpeed, BluetoothGattCharacteristic.FORMAT_SINT8, 0); 1577 if (notify && isFeatureSupported(ServiceFeature.PLAYBACK_SPEED_NOTIFY)) { 1578 notifyCharacteristic(characteristic, null); 1579 } 1580 } 1581 } 1582 1583 @VisibleForTesting updateTrackPositionChar(long positionMs, boolean forceNotify)1584 void updateTrackPositionChar(long positionMs, boolean forceNotify) { 1585 if (VDBG) { 1586 Log.d(TAG, "updateTrackPositionChar: " + positionMs); 1587 } 1588 if (!isFeatureSupported(ServiceFeature.TRACK_POSITION)) return; 1589 1590 final int position = (positionMs != TRACK_POSITION_UNAVAILABLE) 1591 ? new Long(millisecondsToMcsInterval(positionMs)).intValue() 1592 : INTERVAL_UNAVAILABLE; 1593 1594 BluetoothGattCharacteristic characteristic = 1595 mCharacteristics.get(CharId.TRACK_POSITION); 1596 characteristic.setValue(position, BluetoothGattCharacteristic.FORMAT_SINT32, 0); 1597 1598 if (isFeatureSupported(ServiceFeature.TRACK_POSITION_NOTIFY)) { 1599 // Position should be notified only while seeking (frequency is implementation 1600 // specific), on pause, or position change, but not during the playback. 1601 if ((getMediaStateChar() == MediaState.PAUSED.getValue()) 1602 || (getMediaStateChar() == MediaState.SEEKING.getValue()) 1603 || forceNotify) { 1604 notifyCharacteristic(characteristic, null); 1605 } 1606 } 1607 } 1608 getTrackDurationChar()1609 private long getTrackDurationChar() { 1610 if (!isFeatureSupported(ServiceFeature.TRACK_DURATION)) return TRACK_DURATION_UNAVAILABLE; 1611 1612 BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.TRACK_DURATION); 1613 if (characteristic.getValue() != null) { 1614 int duration = 1615 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT32, 0); 1616 return (duration != INTERVAL_UNAVAILABLE) ? mcsIntervalToMilliseconds(duration) 1617 : TRACK_DURATION_UNAVAILABLE; 1618 } 1619 return TRACK_DURATION_UNAVAILABLE; 1620 } 1621 1622 @VisibleForTesting updateTrackDurationChar(long durationMs, boolean notify)1623 void updateTrackDurationChar(long durationMs, boolean notify) { 1624 if (VDBG) { 1625 Log.d(TAG, "updateTrackDurationChar: " + durationMs); 1626 } 1627 if (isFeatureSupported(ServiceFeature.TRACK_DURATION)) { 1628 final int duration = (durationMs != TRACK_DURATION_UNAVAILABLE) 1629 ? new Long(millisecondsToMcsInterval(durationMs)).intValue() 1630 : INTERVAL_UNAVAILABLE; 1631 1632 BluetoothGattCharacteristic characteristic = 1633 mCharacteristics.get(CharId.TRACK_DURATION); 1634 characteristic.setValue(duration, BluetoothGattCharacteristic.FORMAT_SINT32, 0); 1635 if (notify && isFeatureSupported(ServiceFeature.TRACK_DURATION_NOTIFY)) { 1636 notifyCharacteristic(characteristic, null); 1637 } 1638 } 1639 } 1640 getTrackTitleChar()1641 private String getTrackTitleChar() { 1642 if (isFeatureSupported(ServiceFeature.TRACK_TITLE)) { 1643 BluetoothGattCharacteristic characteristic = 1644 mCharacteristics.get(CharId.TRACK_TITLE); 1645 if (characteristic.getValue() != null) { 1646 return characteristic.getStringValue(0); 1647 } 1648 } 1649 1650 return ""; 1651 } 1652 1653 @VisibleForTesting updateTrackTitleChar(String title, boolean notify)1654 void updateTrackTitleChar(String title, boolean notify) { 1655 if (VDBG) { 1656 Log.d(TAG, "updateTrackTitleChar: " + title); 1657 } 1658 if (isFeatureSupported(ServiceFeature.TRACK_TITLE)) { 1659 BluetoothGattCharacteristic characteristic = 1660 mCharacteristics.get(CharId.TRACK_TITLE); 1661 characteristic.setValue(title); 1662 if (notify && isFeatureSupported(ServiceFeature.TRACK_TITLE_NOTIFY)) { 1663 notifyCharacteristic(characteristic, null); 1664 } 1665 } 1666 } 1667 1668 @VisibleForTesting updateSupportedOpcodesChar(int opcodes, boolean notify)1669 void updateSupportedOpcodesChar(int opcodes, boolean notify) { 1670 if (VDBG) { 1671 Log.d(TAG, "updateSupportedOpcodesChar: " + opcodes); 1672 } 1673 1674 if (!isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) return; 1675 1676 BluetoothGattCharacteristic characteristic = mCharacteristics.get( 1677 CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED); 1678 // Do nothing if nothing has changed 1679 if (characteristic.getValue() != null 1680 && characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0) == opcodes) { 1681 return; 1682 } 1683 1684 if (VDBG) { 1685 Log.d(TAG, "updateSupportedOpcodesChar setting char"); 1686 } 1687 1688 characteristic.setValue(opcodes, BluetoothGattCharacteristic.FORMAT_UINT32, 0); 1689 if (notify 1690 && isFeatureSupported( 1691 ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_NOTIFY)) { 1692 notifyCharacteristic(characteristic, null); 1693 } 1694 1695 } 1696 1697 @VisibleForTesting updatePlayingOrderSupportedChar(int supportedOrder)1698 void updatePlayingOrderSupportedChar(int supportedOrder) { 1699 if (VDBG) { 1700 Log.d(TAG, "updatePlayingOrderSupportedChar: " + supportedOrder); 1701 } 1702 if (isFeatureSupported(ServiceFeature.PLAYING_ORDER_SUPPORTED)) { 1703 mCharacteristics.get(CharId.PLAYING_ORDER_SUPPORTED) 1704 .setValue(supportedOrder, BluetoothGattCharacteristic.FORMAT_UINT16, 0); 1705 } 1706 } 1707 updateIconObjIdChar(Long objId)1708 private void updateIconObjIdChar(Long objId) { 1709 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_OBJ_ID)) { 1710 updateObjectIdChar(mCharacteristics.get(CharId.PLAYER_ICON_OBJ_ID), objId, 1711 null, true); 1712 } 1713 } 1714 1715 @VisibleForTesting byteArray2ObjId(byte[] buffer)1716 public long byteArray2ObjId(byte[] buffer) { 1717 ByteBuffer bb = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.LITTLE_ENDIAN); 1718 bb.put(buffer, 0, 6); 1719 // Move position to beginnng after putting data to buffer 1720 bb.position(0); 1721 return bb.getLong(); 1722 } 1723 1724 @VisibleForTesting objId2ByteArray(long objId)1725 public byte[] objId2ByteArray(long objId) { 1726 if (objId < 0) { 1727 return new byte[0]; 1728 } 1729 1730 ByteBuffer bb = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN); 1731 bb.putInt((int) objId); 1732 bb.putShort((short) (objId >> Integer.SIZE)); 1733 1734 return bb.array(); 1735 } 1736 updateObjectIdChar(BluetoothGattCharacteristic characteristic, long objId, BluetoothDevice originDevice, boolean notify)1737 private void updateObjectIdChar(BluetoothGattCharacteristic characteristic, long objId, 1738 BluetoothDevice originDevice, boolean notify) { 1739 characteristic.setValue(objId2ByteArray(objId)); 1740 if ((characteristic.getProperties() & PROPERTY_NOTIFY) != 0) { 1741 // Notify all clients but not the originDevice 1742 if (notify) { 1743 notifyCharacteristic(characteristic, originDevice); 1744 } 1745 } 1746 } 1747 updatePlayerIconUrlChar(String url)1748 private void updatePlayerIconUrlChar(String url) { 1749 if (VDBG) { 1750 Log.d(TAG, "updatePlayerIconUrlChar: " + url); 1751 } 1752 if (isFeatureSupported(ServiceFeature.PLAYER_ICON_URL)) { 1753 mCharacteristics.get(CharId.PLAYER_ICON_URL).setValue(url); 1754 } 1755 } 1756 getPlayerNameChar()1757 private String getPlayerNameChar() { 1758 if (!isFeatureSupported(ServiceFeature.PLAYER_NAME)) return null; 1759 1760 BluetoothGattCharacteristic characteristic = mCharacteristics.get(CharId.PLAYER_NAME); 1761 if (characteristic.getValue() != null) { 1762 return characteristic.getStringValue(0); 1763 } 1764 return null; 1765 } 1766 1767 @VisibleForTesting updatePlayerNameChar(String name, boolean notify)1768 void updatePlayerNameChar(String name, boolean notify) { 1769 if (VDBG) { 1770 Log.d(TAG, "updatePlayerNameChar: " + name); 1771 } 1772 1773 if (!isFeatureSupported(ServiceFeature.PLAYER_NAME)) return; 1774 1775 BluetoothGattCharacteristic characteristic = 1776 mCharacteristics.get(CharId.PLAYER_NAME); 1777 characteristic.setValue(name); 1778 if (notify && isFeatureSupported(ServiceFeature.PLAYER_NAME_NOTIFY)) { 1779 notifyCharacteristic(characteristic, null); 1780 } 1781 } 1782 isFeatureSupported(long featureBit)1783 private boolean isFeatureSupported(long featureBit) { 1784 if (DBG) { 1785 Log.w(TAG, "Feature " + ServiceFeature.toString(featureBit) + " support: " 1786 + ((mFeatures & featureBit) != 0)); 1787 } 1788 return (mFeatures & featureBit) != 0; 1789 } 1790 1791 @VisibleForTesting isOpcodeSupported(int opcode)1792 boolean isOpcodeSupported(int opcode) { 1793 if (opcode < Request.Opcodes.PLAY || opcode > Request.Opcodes.GOTO_GROUP) { 1794 return false; 1795 } 1796 1797 if (!isFeatureSupported(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED)) { 1798 return false; 1799 } 1800 1801 Integer opcodeSupportBit = Request.OpcodeToOpcodeSupport.get(opcode); 1802 if (opcodeSupportBit == null) return false; 1803 1804 return (mCharacteristics.get(CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED) 1805 .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0) 1806 & opcodeSupportBit) == opcodeSupportBit; 1807 } 1808 1809 private interface CharacteristicWriteHandler { onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)1810 void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, 1811 BluetoothGattCharacteristic characteristic, boolean preparedWrite, 1812 boolean responseNeeded, int offset, byte[] value); 1813 } 1814 1815 private static final class CharacteristicData { 1816 public final int id; 1817 public final int properties; 1818 public final int permissions; 1819 public final long featureFlag; 1820 public final long ntfFeatureFlag; 1821 CharacteristicData( int id, long featureFlag, long ntfFeatureFlag, int properties, int permissions)1822 private CharacteristicData( 1823 int id, long featureFlag, long ntfFeatureFlag, int properties, int permissions) { 1824 this.id = id; 1825 this.featureFlag = featureFlag; 1826 this.ntfFeatureFlag = ntfFeatureFlag; 1827 this.properties = properties; 1828 this.permissions = permissions; 1829 } 1830 } 1831 1832 private final static class CharId { 1833 public static final int PLAYER_NAME = 1834 Long.numberOfTrailingZeros(ServiceFeature.PLAYER_NAME); 1835 public static final int PLAYER_ICON_OBJ_ID = 1836 Long.numberOfTrailingZeros(ServiceFeature.PLAYER_ICON_OBJ_ID); 1837 public static final int PLAYER_ICON_URL = 1838 Long.numberOfTrailingZeros(ServiceFeature.PLAYER_ICON_URL); 1839 public static final int TRACK_CHANGED = 1840 Long.numberOfTrailingZeros(ServiceFeature.TRACK_CHANGED); 1841 public static final int TRACK_TITLE = 1842 Long.numberOfTrailingZeros(ServiceFeature.TRACK_TITLE); 1843 public static final int TRACK_DURATION = 1844 Long.numberOfTrailingZeros(ServiceFeature.TRACK_DURATION); 1845 public static final int TRACK_POSITION = 1846 Long.numberOfTrailingZeros(ServiceFeature.TRACK_POSITION); 1847 public static final int PLAYBACK_SPEED = 1848 Long.numberOfTrailingZeros(ServiceFeature.PLAYBACK_SPEED); 1849 public static final int SEEKING_SPEED = 1850 Long.numberOfTrailingZeros(ServiceFeature.SEEKING_SPEED); 1851 public static final int CURRENT_TRACK_SEGMENT_OBJ_ID = 1852 Long.numberOfTrailingZeros(ServiceFeature.CURRENT_TRACK_SEGMENT_OBJ_ID); 1853 public static final int CURRENT_TRACK_OBJ_ID = 1854 Long.numberOfTrailingZeros(ServiceFeature.CURRENT_TRACK_OBJ_ID); 1855 public static final int NEXT_TRACK_OBJ_ID = 1856 Long.numberOfTrailingZeros(ServiceFeature.NEXT_TRACK_OBJ_ID); 1857 public static final int CURRENT_GROUP_OBJ_ID = 1858 Long.numberOfTrailingZeros(ServiceFeature.CURRENT_GROUP_OBJ_ID); 1859 public static final int PARENT_GROUP_OBJ_ID = 1860 Long.numberOfTrailingZeros(ServiceFeature.PARENT_GROUP_OBJ_ID); 1861 public static final int PLAYING_ORDER = 1862 Long.numberOfTrailingZeros(ServiceFeature.PLAYING_ORDER); 1863 public static final int PLAYING_ORDER_SUPPORTED = 1864 Long.numberOfTrailingZeros(ServiceFeature.PLAYING_ORDER_SUPPORTED); 1865 public static final int MEDIA_STATE = 1866 Long.numberOfTrailingZeros(ServiceFeature.MEDIA_STATE); 1867 public static final int MEDIA_CONTROL_POINT = 1868 Long.numberOfTrailingZeros(ServiceFeature.MEDIA_CONTROL_POINT); 1869 public static final int MEDIA_CONTROL_POINT_OPCODES_SUPPORTED = 1870 Long.numberOfTrailingZeros(ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED); 1871 public static final int SEARCH_RESULT_OBJ_ID = 1872 Long.numberOfTrailingZeros(ServiceFeature.SEARCH_RESULT_OBJ_ID); 1873 public static final int SEARCH_CONTROL_POINT = 1874 Long.numberOfTrailingZeros(ServiceFeature.SEARCH_CONTROL_POINT); 1875 public static final int CONTENT_CONTROL_ID = 1876 Long.numberOfTrailingZeros(ServiceFeature.CONTENT_CONTROL_ID); 1877 FromFeature(long feature)1878 public static int FromFeature(long feature) { 1879 return Long.numberOfTrailingZeros(feature); 1880 } 1881 } 1882 1883 private static final UUID UUID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); 1884 1885 /* All characteristic attributes (UUIDs, properties, permissions and flags needed to enable 1886 * them) This is set according to the Media Control Service Specification. 1887 */ getUuidCharacteristicList()1888 private static List<Pair<UUID, CharacteristicData>> getUuidCharacteristicList() { 1889 List<Pair<UUID, CharacteristicData>> characteristics = new ArrayList<>(); 1890 characteristics.add(new Pair<>(UUID_PLAYER_NAME, 1891 new CharacteristicData(CharId.PLAYER_NAME, ServiceFeature.PLAYER_NAME, 1892 ServiceFeature.PLAYER_NAME_NOTIFY, PROPERTY_READ, 1893 PERMISSION_READ_ENCRYPTED))); 1894 characteristics.add(new Pair<>(UUID_PLAYER_ICON_OBJ_ID, 1895 new CharacteristicData(CharId.PLAYER_ICON_OBJ_ID, ServiceFeature.PLAYER_ICON_OBJ_ID, 1896 // Notifications unsupported 1897 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); 1898 characteristics.add(new Pair<>(UUID_PLAYER_ICON_URL, 1899 new CharacteristicData(CharId.PLAYER_ICON_URL, ServiceFeature.PLAYER_ICON_URL, 1900 // Notifications unsupported 1901 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); 1902 characteristics.add(new Pair<>(UUID_TRACK_CHANGED, 1903 new CharacteristicData(CharId.TRACK_CHANGED, ServiceFeature.TRACK_CHANGED, 1904 // Mandatory notification if char. exists. 1905 ServiceFeature.TRACK_CHANGED, PROPERTY_NOTIFY, 0))); 1906 characteristics.add(new Pair<>(UUID_TRACK_TITLE, 1907 new CharacteristicData(CharId.TRACK_TITLE, ServiceFeature.TRACK_TITLE, 1908 ServiceFeature.TRACK_TITLE_NOTIFY, PROPERTY_READ, 1909 PERMISSION_READ_ENCRYPTED))); 1910 characteristics.add(new Pair<>(UUID_TRACK_DURATION, 1911 new CharacteristicData(CharId.TRACK_DURATION, ServiceFeature.TRACK_DURATION, 1912 ServiceFeature.TRACK_DURATION_NOTIFY, PROPERTY_READ, 1913 PERMISSION_READ_ENCRYPTED))); 1914 characteristics.add(new Pair<>(UUID_TRACK_POSITION, 1915 new CharacteristicData(CharId.TRACK_POSITION, ServiceFeature.TRACK_POSITION, 1916 ServiceFeature.TRACK_POSITION_NOTIFY, 1917 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1918 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1919 characteristics.add(new Pair<>(UUID_PLAYBACK_SPEED, 1920 new CharacteristicData(CharId.PLAYBACK_SPEED, ServiceFeature.PLAYBACK_SPEED, 1921 ServiceFeature.PLAYBACK_SPEED_NOTIFY, 1922 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1923 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1924 characteristics.add(new Pair<>(UUID_SEEKING_SPEED, 1925 new CharacteristicData(CharId.SEEKING_SPEED, ServiceFeature.SEEKING_SPEED, 1926 ServiceFeature.SEEKING_SPEED_NOTIFY, PROPERTY_READ, 1927 PERMISSION_READ_ENCRYPTED))); 1928 characteristics.add(new Pair<>(UUID_CURRENT_TRACK_SEGMENT_OBJ_ID, 1929 new CharacteristicData(CharId.CURRENT_TRACK_SEGMENT_OBJ_ID, 1930 ServiceFeature.CURRENT_TRACK_SEGMENT_OBJ_ID, 1931 // Notifications unsupported 1932 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); 1933 characteristics.add(new Pair<>(UUID_CURRENT_TRACK_OBJ_ID, 1934 new CharacteristicData(CharId.CURRENT_TRACK_OBJ_ID, 1935 ServiceFeature.CURRENT_TRACK_OBJ_ID, 1936 ServiceFeature.CURRENT_TRACK_OBJ_ID_NOTIFY, 1937 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1938 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1939 characteristics.add(new Pair<>(UUID_NEXT_TRACK_OBJ_ID, 1940 new CharacteristicData(CharId.NEXT_TRACK_OBJ_ID, ServiceFeature.NEXT_TRACK_OBJ_ID, 1941 ServiceFeature.NEXT_TRACK_OBJ_ID_NOTIFY, 1942 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1943 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1944 characteristics.add(new Pair<>(UUID_CURRENT_GROUP_OBJ_ID, 1945 new CharacteristicData(CharId.CURRENT_GROUP_OBJ_ID, 1946 ServiceFeature.CURRENT_GROUP_OBJ_ID, 1947 ServiceFeature.CURRENT_GROUP_OBJ_ID_NOTIFY, 1948 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1949 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1950 characteristics.add(new Pair<>(UUID_PARENT_GROUP_OBJ_ID, 1951 new CharacteristicData(CharId.PARENT_GROUP_OBJ_ID, 1952 ServiceFeature.PARENT_GROUP_OBJ_ID, 1953 ServiceFeature.PARENT_GROUP_OBJ_ID_NOTIFY, PROPERTY_READ, 1954 PERMISSION_READ_ENCRYPTED))); 1955 characteristics.add(new Pair<>(UUID_PLAYING_ORDER, 1956 new CharacteristicData(CharId.PLAYING_ORDER, ServiceFeature.PLAYING_ORDER, 1957 ServiceFeature.PLAYING_ORDER_NOTIFY, 1958 PROPERTY_READ | PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE, 1959 PERMISSION_READ_ENCRYPTED | PERMISSION_WRITE_ENCRYPTED))); 1960 characteristics.add(new Pair<>(UUID_PLAYING_ORDER_SUPPORTED, 1961 new CharacteristicData(CharId.PLAYING_ORDER_SUPPORTED, 1962 ServiceFeature.PLAYING_ORDER_SUPPORTED, 1963 // Notifications unsupported 1964 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); 1965 characteristics.add(new Pair<>(UUID_MEDIA_STATE, 1966 new CharacteristicData(CharId.MEDIA_STATE, ServiceFeature.MEDIA_STATE, 1967 // Mandatory notification if char. exists. 1968 ServiceFeature.MEDIA_STATE, PROPERTY_READ | PROPERTY_NOTIFY, 1969 PERMISSION_READ_ENCRYPTED))); 1970 characteristics.add(new Pair<>(UUID_MEDIA_CONTROL_POINT, 1971 new CharacteristicData(CharId.MEDIA_CONTROL_POINT, 1972 ServiceFeature.MEDIA_CONTROL_POINT, 1973 // Mandatory notification if char. exists. 1974 ServiceFeature.MEDIA_CONTROL_POINT, 1975 PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE | PROPERTY_NOTIFY, 1976 PERMISSION_WRITE_ENCRYPTED))); 1977 characteristics.add(new Pair<>(UUID_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, 1978 new CharacteristicData(CharId.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, 1979 ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED, 1980 ServiceFeature.MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_NOTIFY, PROPERTY_READ, 1981 PERMISSION_READ_ENCRYPTED))); 1982 characteristics.add(new Pair<>(UUID_SEARCH_RESULT_OBJ_ID, 1983 new CharacteristicData(CharId.SEARCH_RESULT_OBJ_ID, 1984 ServiceFeature.SEARCH_RESULT_OBJ_ID, 1985 // Mandatory notification if char. exists. 1986 ServiceFeature.SEARCH_RESULT_OBJ_ID, PROPERTY_READ | PROPERTY_NOTIFY, 1987 PERMISSION_READ_ENCRYPTED))); 1988 characteristics.add(new Pair<>(UUID_SEARCH_CONTROL_POINT, 1989 new CharacteristicData(CharId.SEARCH_CONTROL_POINT, 1990 ServiceFeature.SEARCH_CONTROL_POINT, 1991 // Mandatory notification if char. exists. 1992 ServiceFeature.SEARCH_CONTROL_POINT, 1993 PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE | PROPERTY_NOTIFY, 1994 PERMISSION_WRITE_ENCRYPTED))); 1995 characteristics.add(new Pair<>(UUID_CONTENT_CONTROL_ID, 1996 new CharacteristicData(CharId.CONTENT_CONTROL_ID, ServiceFeature.CONTENT_CONTROL_ID, 1997 // Notifications unsupported 1998 0, PROPERTY_READ, PERMISSION_READ_ENCRYPTED))); 1999 return characteristics; 2000 } 2001 dump(StringBuilder sb)2002 public void dump(StringBuilder sb) { 2003 sb.append("\tMediaControlService instance:"); 2004 sb.append("\n\t\tCcid = " + mCcid); 2005 sb.append("\n\t\tFeatures:" + ServiceFeature.featuresToString(mFeatures, "\n\t\t\t")); 2006 2007 BluetoothGattCharacteristic characteristic = 2008 mCharacteristics.get(CharId.PLAYER_NAME); 2009 if (characteristic == null) { 2010 sb.append("\n\t\tPlayer name: <No Player>"); 2011 } else { 2012 sb.append("\n\t\tPlayer name: " + characteristic.getStringValue(0)); 2013 } 2014 2015 sb.append("\n\t\tCurrentPlaybackState = " + mCurrentMediaState); 2016 for (Map.Entry<String, HashMap<UUID, Short>> deviceEntry 2017 : mCccDescriptorValues.entrySet()) { 2018 sb.append("\n\t\tCCC states for device: " + "xx:xx:xx:xx:" 2019 + deviceEntry.getKey().substring(12)); 2020 for (Map.Entry<UUID, Short> entry : deviceEntry.getValue().entrySet()) { 2021 sb.append("\n\t\t\tCharacteristic: " + mcsUuidToString(entry.getKey()) + ", value: " 2022 + Utils.cccIntToStr(entry.getValue())); 2023 } 2024 } 2025 } 2026 } 2027