1 /* 2 * Copyright (C) 2015 Samsung System LSI 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.android.bluetooth.sdp; 16 17 import android.bluetooth.BluetoothDevice; 18 import android.bluetooth.SdpMasRecord; 19 import android.bluetooth.SdpMnsRecord; 20 import android.bluetooth.SdpOppOpsRecord; 21 import android.bluetooth.SdpPseRecord; 22 import android.bluetooth.SdpSapsRecord; 23 import android.bluetooth.SdpRecord; 24 import android.content.Intent; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.os.ParcelUuid; 28 import android.os.Parcelable; 29 import android.util.Log; 30 31 import com.android.bluetooth.Utils; 32 import com.android.bluetooth.btservice.AbstractionLayer; 33 import com.android.bluetooth.btservice.AdapterService; 34 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 38 public class SdpManager { 39 40 private static final boolean D = true; 41 private static final boolean V = false; 42 private static final String TAG="SdpManager"; 43 44 // TODO: When changing PBAP to use this new API. 45 // Move the defines to the profile (PBAP already have the feature bits) 46 /* PBAP repositories */ 47 public static final byte PBAP_REPO_LOCAL = 0x01<<0; 48 public static final byte PBAP_REPO_SIM = 0x01<<1; 49 public static final byte PBAP_REPO_SPEED_DAIL = 0x01<<2; 50 public static final byte PBAP_REPO_FAVORITES = 0x01<<3; 51 52 /* Variables to keep track of ongoing and queued search requests. 53 * mTrackerLock must be held, when using/changing sSdpSearchTracker 54 * and mSearchInProgress. */ 55 static SdpSearchTracker sSdpSearchTracker; 56 static boolean mSearchInProgress = false; 57 static Object mTrackerLock = new Object(); 58 59 /* The timeout to wait for reply from native. Should never fire. */ 60 private static final int SDP_INTENT_DELAY = 11000; 61 private static final int MESSAGE_SDP_INTENT = 2; 62 63 // We need a reference to the adapter service, to be able to send intents 64 private static AdapterService sAdapterService; 65 private static boolean sNativeAvailable; 66 67 // This object is a singleton 68 private static SdpManager sSdpManager = null; 69 70 static { 71 classInitNative(); 72 } 73 74 private native static void classInitNative(); 75 private native void initializeNative(); 76 private native void cleanupNative(); 77 private native boolean sdpSearchNative(byte[] address, byte[] uuid); 78 79 private native int sdpCreateMapMasRecordNative(String serviceName, int masId, 80 int rfcommChannel, int l2capPsm, int version, int msgTypes, int features); 81 82 private native int sdpCreateMapMnsRecordNative(String serviceName, 83 int rfcommChannel, int l2capPsm, int version, int features); 84 85 private native int sdpCreatePbapPseRecordNative(String serviceName, int rfcommChannel, 86 int l2capPsm, int version, int repositories, int features); 87 88 private native int sdpCreateOppOpsRecordNative(String serviceName, 89 int rfcommChannel, int l2capPsm, int version, byte[] formats_list); 90 91 private native int sdpCreateSapsRecordNative(String serviceName, int rfcommChannel, 92 int version); 93 94 private native boolean sdpRemoveSdpRecordNative(int record_id); 95 96 97 /* Inner class used for wrapping sdp search instance data */ 98 private class SdpSearchInstance { 99 private final BluetoothDevice mDevice; 100 private final ParcelUuid mUuid; 101 private int mStatus = 0; 102 private boolean mSearching; 103 /* TODO: If we change the API to use another mechanism than intents for 104 * delivering the results, this would be the place to keep a list 105 * of the objects to deliver the results to. */ 106 public SdpSearchInstance(int status, BluetoothDevice device, ParcelUuid uuid){ 107 this.mDevice = device; 108 this.mUuid = uuid; 109 this.mStatus = status; 110 mSearching = true; 111 } 112 public BluetoothDevice getDevice() { 113 return mDevice; 114 } 115 public ParcelUuid getUuid() { 116 return mUuid; 117 } 118 public int getStatus(){ 119 return mStatus; 120 } 121 122 public void setStatus(int status) { 123 this.mStatus = status; 124 } 125 126 public void startSearch() { 127 mSearching = true; 128 Message message = mHandler.obtainMessage(MESSAGE_SDP_INTENT, this); 129 mHandler.sendMessageDelayed(message, SDP_INTENT_DELAY); 130 } 131 132 public void stopSearch() { 133 if(mSearching) { 134 mHandler.removeMessages(MESSAGE_SDP_INTENT, this); 135 } 136 mSearching = false; 137 } 138 public boolean isSearching() { 139 return mSearching; 140 } 141 } 142 143 144 /* We wrap the ArrayList class to decorate with functionality to 145 * find an instance based on UUID AND device address. 146 * As we use a mix of byte[] and object instances, this is more 147 * efficient than implementing comparable. */ 148 class SdpSearchTracker { 149 private final ArrayList<SdpSearchInstance> list = new ArrayList<SdpSearchInstance>(); 150 151 void clear() { 152 list.clear(); 153 } 154 155 boolean add(SdpSearchInstance inst){ 156 return list.add(inst); 157 } 158 159 boolean remove(SdpSearchInstance inst) { 160 return list.remove(inst); 161 } 162 163 SdpSearchInstance getNext() { 164 if(list.size() > 0) { 165 return list.get(0); 166 } 167 return null; 168 } 169 170 SdpSearchInstance getSearchInstance(byte[] address, byte[] uuidBytes) { 171 String addressString = Utils.getAddressStringFromByte(address); 172 ParcelUuid uuid = Utils.byteArrayToUuid(uuidBytes)[0]; 173 for (SdpSearchInstance inst : list) { 174 if (inst.getDevice().getAddress().equals(addressString) 175 && inst.getUuid().equals(uuid)) { 176 return inst; 177 } 178 } 179 return null; 180 } 181 182 boolean isSearching(BluetoothDevice device, ParcelUuid uuid) { 183 String addressString = device.getAddress(); 184 for (SdpSearchInstance inst : list) { 185 if (inst.getDevice().getAddress().equals(addressString) 186 && inst.getUuid().equals(uuid)) { 187 return inst.isSearching(); 188 } 189 } 190 return false; 191 } 192 } 193 194 195 private SdpManager(AdapterService adapterService) { 196 sSdpSearchTracker = new SdpSearchTracker(); 197 198 /* This is only needed until intents are no longer used */ 199 sAdapterService = adapterService; 200 initializeNative(); 201 sNativeAvailable=true; 202 } 203 204 205 public static SdpManager init(AdapterService adapterService) { 206 sSdpManager = new SdpManager(adapterService); 207 return sSdpManager; 208 } 209 210 public static SdpManager getDefaultManager() { 211 return sSdpManager; 212 } 213 214 public void cleanup() { 215 if (sSdpSearchTracker !=null) { 216 synchronized(mTrackerLock) { 217 sSdpSearchTracker.clear(); 218 } 219 } 220 221 if (sNativeAvailable) { 222 cleanupNative(); 223 sNativeAvailable=false; 224 } 225 sSdpManager = null; 226 } 227 228 229 void sdpMasRecordFoundCallback(int status, byte[] address, byte[] uuid, 230 int masInstanceId, 231 int l2capPsm, 232 int rfcommCannelNumber, 233 int profileVersion, 234 int supportedFeatures, 235 int supportedMessageTypes, 236 String serviceName, 237 boolean moreResults) { 238 239 synchronized(mTrackerLock) { 240 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 241 SdpMasRecord sdpRecord = null; 242 if (inst == null) { 243 Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); 244 return; 245 } 246 inst.setStatus(status); 247 if(status == AbstractionLayer.BT_STATUS_SUCCESS) { 248 sdpRecord = new SdpMasRecord(masInstanceId, 249 l2capPsm, 250 rfcommCannelNumber, 251 profileVersion, 252 supportedFeatures, 253 supportedMessageTypes, 254 serviceName); 255 } 256 if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 257 if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 258 sendSdpIntent(inst, sdpRecord, moreResults); 259 } 260 } 261 262 void sdpMnsRecordFoundCallback(int status, byte[] address, byte[] uuid, 263 int l2capPsm, 264 int rfcommCannelNumber, 265 int profileVersion, 266 int supportedFeatures, 267 String serviceName, 268 boolean moreResults) { 269 synchronized(mTrackerLock) { 270 271 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 272 SdpMnsRecord sdpRecord = null; 273 if (inst == null) { 274 Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); 275 return; 276 } 277 inst.setStatus(status); 278 if(status == AbstractionLayer.BT_STATUS_SUCCESS) { 279 sdpRecord = new SdpMnsRecord(l2capPsm, 280 rfcommCannelNumber, 281 profileVersion, 282 supportedFeatures, 283 serviceName); 284 } 285 if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 286 if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 287 sendSdpIntent(inst, sdpRecord, moreResults); 288 } 289 } 290 291 void sdpPseRecordFoundCallback(int status, byte[] address, byte[] uuid, 292 int l2capPsm, 293 int rfcommCannelNumber, 294 int profileVersion, 295 int supportedFeatures, 296 int supportedRepositories, 297 String serviceName, 298 boolean moreResults) { 299 synchronized(mTrackerLock) { 300 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 301 SdpPseRecord sdpRecord = null; 302 if (inst == null) { 303 Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); 304 return; 305 } 306 inst.setStatus(status); 307 if(status == AbstractionLayer.BT_STATUS_SUCCESS) { 308 sdpRecord = new SdpPseRecord(l2capPsm, 309 rfcommCannelNumber, 310 profileVersion, 311 supportedFeatures, 312 supportedRepositories, 313 serviceName); 314 } 315 if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 316 if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 317 sendSdpIntent(inst, sdpRecord, moreResults); 318 } 319 } 320 321 void sdpOppOpsRecordFoundCallback(int status, byte[] address, byte[] uuid, 322 int l2capPsm, 323 int rfcommCannelNumber, 324 int profileVersion, 325 String serviceName, 326 byte[] formatsList, 327 boolean moreResults) { 328 329 synchronized(mTrackerLock) { 330 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 331 SdpOppOpsRecord sdpRecord = null; 332 333 if (inst == null) { 334 Log.e(TAG, "sdpOppOpsRecordFoundCallback: Search instance is NULL"); 335 return; 336 } 337 inst.setStatus(status); 338 if(status == AbstractionLayer.BT_STATUS_SUCCESS) { 339 sdpRecord = new SdpOppOpsRecord(serviceName, 340 rfcommCannelNumber, 341 l2capPsm, 342 profileVersion, 343 formatsList); 344 } 345 if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 346 if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 347 sendSdpIntent(inst, sdpRecord, moreResults); 348 } 349 } 350 351 void sdpSapsRecordFoundCallback(int status, byte[] address, byte[] uuid, 352 int rfcommCannelNumber, 353 int profileVersion, 354 String serviceName, 355 boolean moreResults) { 356 357 synchronized(mTrackerLock) { 358 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 359 SdpSapsRecord sdpRecord = null; 360 if (inst == null) { 361 Log.e(TAG, "sdpSapsRecordFoundCallback: Search instance is NULL"); 362 return; 363 } 364 inst.setStatus(status); 365 if (status == AbstractionLayer.BT_STATUS_SUCCESS) { 366 sdpRecord = new SdpSapsRecord(rfcommCannelNumber, 367 profileVersion, 368 serviceName); 369 } 370 if (D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 371 if (D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 372 sendSdpIntent(inst, sdpRecord, moreResults); 373 } 374 } 375 376 /* TODO: Test or remove! */ 377 void sdpRecordFoundCallback(int status, byte[] address, byte[] uuid, 378 int size_record, byte[] record) { 379 synchronized(mTrackerLock) { 380 381 SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); 382 SdpRecord sdpRecord = null; 383 if (inst == null) { 384 Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); 385 return; 386 } 387 inst.setStatus(status); 388 if(status == AbstractionLayer.BT_STATUS_SUCCESS) { 389 if(D) Log.d(TAG, "sdpRecordFoundCallback: found a sdp record of size " 390 + size_record ); 391 if(D) Log.d(TAG, "Record:"+ Arrays.toString(record)); 392 sdpRecord = new SdpRecord(size_record, record); 393 } 394 if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid)); 395 if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); 396 sendSdpIntent(inst, sdpRecord, false); 397 } 398 } 399 400 public void sdpSearch(BluetoothDevice device, ParcelUuid uuid) { 401 if (sNativeAvailable == false) { 402 Log.e(TAG, "Native not initialized!"); 403 return; 404 } 405 synchronized (mTrackerLock) { 406 if (sSdpSearchTracker.isSearching(device, uuid)) { 407 /* Search already in progress */ 408 return; 409 } 410 411 SdpSearchInstance inst = new SdpSearchInstance(0, device, uuid); 412 sSdpSearchTracker.add(inst); // Queue the request 413 414 startSearch(); // Start search if not busy 415 } 416 417 } 418 419 /* Caller must hold the mTrackerLock */ 420 private void startSearch() { 421 422 SdpSearchInstance inst = sSdpSearchTracker.getNext(); 423 424 if((inst != null) && (mSearchInProgress == false)) { 425 if(D) Log.d(TAG, "Starting search for UUID: "+ inst.getUuid()); 426 mSearchInProgress = true; 427 428 inst.startSearch(); // Trigger timeout message 429 430 sdpSearchNative(Utils.getBytesFromAddress(inst.getDevice().getAddress()), 431 Utils.uuidToByteArray(inst.getUuid())); 432 } // Else queue is empty. 433 else { 434 if(D) Log.d(TAG, "startSearch(): nextInst = " + inst + 435 " mSearchInProgress = " + mSearchInProgress 436 + " - search busy or queue empty."); 437 } 438 } 439 440 /* Caller must hold the mTrackerLock */ 441 private void sendSdpIntent(SdpSearchInstance inst, 442 Parcelable record, boolean moreResults) { 443 444 inst.stopSearch(); 445 446 Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD); 447 448 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, inst.getDevice()); 449 intent.putExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, inst.getStatus()); 450 if (record != null) intent.putExtra(BluetoothDevice.EXTRA_SDP_RECORD, record); 451 intent.putExtra(BluetoothDevice.EXTRA_UUID, inst.getUuid()); 452 /* TODO: BLUETOOTH_ADMIN_PERM was private... change to callback interface. 453 * Keep in mind that the MAP client needs to use this as well, 454 * hence to make it call-backs, the MAP client profile needs to be 455 * part of the Bluetooth APK. */ 456 sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM); 457 458 if(moreResults == false) { 459 //Remove the outstanding UUID request 460 sSdpSearchTracker.remove(inst); 461 mSearchInProgress = false; 462 startSearch(); 463 } 464 } 465 466 private final Handler mHandler = new Handler() { 467 @Override 468 public void handleMessage(Message msg) { 469 switch (msg.what) { 470 case MESSAGE_SDP_INTENT: 471 SdpSearchInstance msgObj = (SdpSearchInstance)msg.obj; 472 Log.w(TAG, "Search timedout for UUID " + msgObj.getUuid()); 473 synchronized (mTrackerLock) { 474 sendSdpIntent(msgObj, null, false); 475 } 476 break; 477 } 478 } 479 }; 480 481 /** 482 * Create a server side Message Access Profile Service Record. 483 * Create the record once, and reuse it for all connections. 484 * If changes to a record is needed remove the old record using {@link removeSdpRecord} 485 * and then create a new one. 486 * @param serviceName The textual name of the service 487 * @param masId The MAS ID to associate with this SDP record 488 * @param rfcommChannel The RFCOMM channel that clients can connect to 489 * (obtain from BluetoothServerSocket) 490 * @param l2capPsm The L2CAP PSM channel that clients can connect to 491 * (obtain from BluetoothServerSocket) 492 * Supply -1 to omit the L2CAP PSM from the record. 493 * @param version The Profile version number (As specified in the Bluetooth 494 * MAP specification) 495 * @param msgTypes The supported message types bit mask (As specified in 496 * the Bluetooth MAP specification) 497 * @param features The feature bit mask (As specified in the Bluetooth 498 * MAP specification) 499 * @return a handle to the record created. The record can be removed again 500 * using {@link removeSdpRecord}(). The record is not linked to the 501 * creation/destruction of BluetoothSockets, hence SDP record cleanup 502 * is a separate process. 503 */ 504 public int createMapMasRecord(String serviceName, int masId, 505 int rfcommChannel, int l2capPsm, int version, 506 int msgTypes, int features) { 507 if(sNativeAvailable == false) { 508 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 509 } 510 return sdpCreateMapMasRecordNative(serviceName, masId, rfcommChannel, 511 l2capPsm, version, msgTypes, features); 512 } 513 514 /** 515 * Create a client side Message Access Profile Service Record. 516 * Create the record once, and reuse it for all connections. 517 * If changes to a record is needed remove the old record using {@link removeSdpRecord} 518 * and then create a new one. 519 * @param serviceName The textual name of the service 520 * @param rfcommChannel The RFCOMM channel that clients can connect to 521 * (obtain from BluetoothServerSocket) 522 * @param l2capPsm The L2CAP PSM channel that clients can connect to 523 * (obtain from BluetoothServerSocket) 524 * Supply -1 to omit the L2CAP PSM from the record. 525 * @param version The Profile version number (As specified in the Bluetooth 526 * MAP specification) 527 * @param features The feature bit mask (As specified in the Bluetooth 528 * MAP specification) 529 * @return a handle to the record created. The record can be removed again 530 * using {@link removeSdpRecord}(). The record is not linked to the 531 * creation/destruction of BluetoothSockets, hence SDP record cleanup 532 * is a separate process. 533 */ 534 public int createMapMnsRecord(String serviceName, int rfcommChannel, 535 int l2capPsm, int version, int features) { 536 if(sNativeAvailable == false) { 537 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 538 } 539 return sdpCreateMapMnsRecordNative(serviceName, rfcommChannel, 540 l2capPsm, version, features); 541 } 542 543 /** 544 * Create a Server side Phone Book Access Profile Service Record. 545 * Create the record once, and reuse it for all connections. 546 * If changes to a record is needed remove the old record using {@link removeSdpRecord} 547 * and then create a new one. 548 * @param serviceName The textual name of the service 549 * @param rfcommChannel The RFCOMM channel that clients can connect to 550 * (obtain from BluetoothServerSocket) 551 * @param l2capPsm The L2CAP PSM channel that clients can connect to 552 * (obtain from BluetoothServerSocket) 553 * Supply -1 to omit the L2CAP PSM from the record. 554 * @param version The Profile version number (As specified in the Bluetooth 555 * PBAP specification) 556 * @param repositories The supported repositories bit mask (As specified in 557 * the Bluetooth PBAP specification) 558 * @param features The feature bit mask (As specified in the Bluetooth 559 * PBAP specification) 560 * @return a handle to the record created. The record can be removed again 561 * using {@link removeSdpRecord}(). The record is not linked to the 562 * creation/destruction of BluetoothSockets, hence SDP record cleanup 563 * is a separate process. 564 */ 565 public int createPbapPseRecord(String serviceName, int rfcommChannel, int l2capPsm, 566 int version, int repositories, int features) { 567 if(sNativeAvailable == false) { 568 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 569 } 570 return sdpCreatePbapPseRecordNative(serviceName, rfcommChannel, 571 l2capPsm, version, repositories, features); 572 } 573 574 /** 575 * Create a Server side Object Push Profile Service Record. 576 * Create the record once, and reuse it for all connections. 577 * If changes to a record is needed remove the old record using {@link removeSdpRecord} 578 * and then create a new one. 579 * @param serviceName The textual name of the service 580 * @param rfcommChannel The RFCOMM channel that clients can connect to 581 * (obtain from BluetoothServerSocket) 582 * @param l2capPsm The L2CAP PSM channel that clients can connect to 583 * (obtain from BluetoothServerSocket) 584 * Supply -1 to omit the L2CAP PSM from the record. 585 * @param version The Profile version number (As specified in the Bluetooth 586 * OPP specification) 587 * @param formatsList A list of the supported formats (As specified in 588 * the Bluetooth OPP specification) 589 * @return a handle to the record created. The record can be removed again 590 * using {@link removeSdpRecord}(). The record is not linked to the 591 * creation/destruction of BluetoothSockets, hence SDP record cleanup 592 * is a separate process. 593 */ 594 public int createOppOpsRecord(String serviceName, int rfcommChannel, int l2capPsm, 595 int version, byte[] formatsList) { 596 if(sNativeAvailable == false) { 597 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 598 } 599 return sdpCreateOppOpsRecordNative(serviceName, rfcommChannel, 600 l2capPsm, version, formatsList); 601 } 602 603 /** 604 * Create a server side Sim Access Profile Service Record. 605 * Create the record once, and reuse it for all connections. 606 * If changes to a record is needed remove the old record using {@link removeSdpRecord} 607 * and then create a new one. 608 * @param serviceName The textual name of the service 609 * @param rfcommChannel The RFCOMM channel that clients can connect to 610 * (obtain from BluetoothServerSocket) 611 * @param version The Profile version number (As specified in the Bluetooth 612 * SAP specification) 613 * @return a handle to the record created. The record can be removed again 614 * using {@link removeSdpRecord}(). The record is not linked to the 615 * creation/destruction of BluetoothSockets, hence SDP record cleanup 616 * is a separate process. 617 */ 618 public int createSapsRecord(String serviceName, int rfcommChannel, int version) { 619 if (sNativeAvailable == false) { 620 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 621 } 622 return sdpCreateSapsRecordNative(serviceName, rfcommChannel, version); 623 } 624 625 /** 626 * Remove a SDP record. 627 * When Bluetooth is disabled all records will be deleted, hence there 628 * is no need to call this function when bluetooth is disabled. 629 * @param recordId The Id returned by on of the createXxxXxxRecord() functions. 630 * @return TRUE if the record removal was initiated successfully. FALSE if the record 631 * handle is not known/have already been removed. 632 */ 633 public boolean removeSdpRecord(int recordId){ 634 if(sNativeAvailable == false) { 635 throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); 636 } 637 return sdpRemoveSdpRecordNative(recordId); 638 } 639 } 640