1/* 2* Copyright (C) 2014 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 16package com.android.bluetooth.map; 17 18import android.app.AlarmManager; 19import android.app.PendingIntent; 20import android.bluetooth.BluetoothAdapter; 21import android.bluetooth.BluetoothDevice; 22import android.bluetooth.BluetoothMap; 23import android.bluetooth.BluetoothProfile; 24import android.bluetooth.BluetoothUuid; 25import android.bluetooth.IBluetoothMap; 26import android.bluetooth.SdpMnsRecord; 27import android.content.BroadcastReceiver; 28import android.content.Context; 29import android.content.Intent; 30import android.content.IntentFilter; 31import android.content.IntentFilter.MalformedMimeTypeException; 32import android.os.Handler; 33import android.os.Message; 34import android.os.ParcelUuid; 35import android.os.PowerManager; 36import android.os.RemoteException; 37import android.provider.Settings; 38import android.text.TextUtils; 39import android.util.Log; 40import android.util.SparseArray; 41 42import com.android.bluetooth.Utils; 43import com.android.bluetooth.btservice.AdapterService; 44import com.android.bluetooth.btservice.ProfileService; 45import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; 46import com.android.bluetooth.R; 47 48import java.io.IOException; 49import java.util.ArrayList; 50import java.util.HashMap; 51import java.util.List; 52import java.util.Set; 53 54public class BluetoothMapService extends ProfileService { 55 private static final String TAG = "BluetoothMapService"; 56 57 /** 58 * To enable MAP DEBUG/VERBOSE logging - run below cmd in adb shell, and 59 * restart com.android.bluetooth process. only enable DEBUG log: 60 * "setprop log.tag.BluetoothMapService DEBUG"; enable both VERBOSE and 61 * DEBUG log: "setprop log.tag.BluetoothMapService VERBOSE" 62 */ 63 64 public static final boolean DEBUG = true; //FIXME set to false; 65 66 public static final boolean VERBOSE = false; 67 68 /** 69 * Intent indicating timeout for user confirmation, which is sent to 70 * BluetoothMapActivity 71 */ 72 public static final String USER_CONFIRM_TIMEOUT_ACTION = 73 "com.android.bluetooth.map.USER_CONFIRM_TIMEOUT"; 74 private static final int USER_CONFIRM_TIMEOUT_VALUE = 25000; 75 76 /** Intent indicating that the email settings activity should be opened*/ 77 public static final String ACTION_SHOW_MAPS_SETTINGS = 78 "android.btmap.intent.action.SHOW_MAPS_SETTINGS"; 79 80 public static final int MSG_SERVERSESSION_CLOSE = 5000; 81 82 public static final int MSG_SESSION_ESTABLISHED = 5001; 83 84 public static final int MSG_SESSION_DISCONNECTED = 5002; 85 86 public static final int MSG_MAS_CONNECT = 5003; // Send at MAS connect, including the MAS_ID 87 public static final int MSG_MAS_CONNECT_CANCEL = 5004; // Send at auth. declined 88 89 public static final int MSG_ACQUIRE_WAKE_LOCK = 5005; 90 91 public static final int MSG_RELEASE_WAKE_LOCK = 5006; 92 93 private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; 94 95 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; 96 97 private static final int START_LISTENER = 1; 98 99 private static final int USER_TIMEOUT = 2; 100 101 private static final int DISCONNECT_MAP = 3; 102 103 private static final int SHUTDOWN = 4; 104 105 private static final int RELEASE_WAKE_LOCK_DELAY = 10000; 106 107 private PowerManager.WakeLock mWakeLock = null; 108 109 private static final int UPDATE_MAS_INSTANCES = 5; 110 111 public static final int UPDATE_MAS_INSTANCES_ACCOUNT_ADDED = 0; 112 public static final int UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED = 1; 113 public static final int UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED = 2; 114 public static final int UPDATE_MAS_INSTANCES_ACCOUNT_DISCONNECT = 3; 115 116 private static final int MAS_ID_SMS_MMS = 0; 117 118 private BluetoothAdapter mAdapter; 119 120 private BluetoothMnsObexClient mBluetoothMnsObexClient = null; 121 122 /* mMasInstances: A list of the active MasInstances with the key being the MasId */ 123 private SparseArray<BluetoothMapMasInstance> mMasInstances = 124 new SparseArray<BluetoothMapMasInstance>(1); 125 /* mMasInstanceMap: A list of the active MasInstances with the key being the account */ 126 private HashMap<BluetoothMapAccountItem, BluetoothMapMasInstance> mMasInstanceMap = 127 new HashMap<BluetoothMapAccountItem, BluetoothMapMasInstance>(1); 128 129 private BluetoothDevice mRemoteDevice = null; // The remote connected device - protect access 130 131 private ArrayList<BluetoothMapAccountItem> mEnabledAccounts = null; 132 private static String sRemoteDeviceName = null; 133 134 private int mState; 135 private BluetoothMapAppObserver mAppObserver = null; 136 private AlarmManager mAlarmManager = null; 137 138 private boolean mIsWaitingAuthorization = false; 139 private boolean mRemoveTimeoutMsg = false; 140 private int mPermission = BluetoothDevice.ACCESS_UNKNOWN; 141 private boolean mAccountChanged = false; 142 private boolean mSdpSearchInitiated = false; 143 SdpMnsRecord mMnsRecord = null; 144 145 // package and class name to which we send intent to check phone book access permission 146 private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; 147 private static final String ACCESS_AUTHORITY_CLASS = 148 "com.android.settings.bluetooth.BluetoothPermissionRequest"; 149 150 private static final ParcelUuid[] MAP_UUIDS = { 151 BluetoothUuid.MAP, 152 BluetoothUuid.MNS, 153 }; 154 155 public BluetoothMapService() { 156 mState = BluetoothMap.STATE_DISCONNECTED; 157 158 } 159 160 161 private final void closeService() { 162 if (DEBUG) Log.d(TAG, "MAP Service closeService in"); 163 164 if (mBluetoothMnsObexClient != null) { 165 mBluetoothMnsObexClient.shutdown(); 166 mBluetoothMnsObexClient = null; 167 } 168 169 for(int i=0, c=mMasInstances.size(); i < c; i++) { 170 mMasInstances.valueAt(i).shutdown(); 171 } 172 mMasInstances.clear(); 173 174 if (mSessionStatusHandler != null) { 175 mSessionStatusHandler.removeCallbacksAndMessages(null); 176 } 177 178 mIsWaitingAuthorization = false; 179 mPermission = BluetoothDevice.ACCESS_UNKNOWN; 180 setState(BluetoothMap.STATE_DISCONNECTED); 181 182 if (mWakeLock != null) { 183 mWakeLock.release(); 184 if (VERBOSE) Log.v(TAG, "CloseService(): Release Wake Lock"); 185 mWakeLock = null; 186 } 187 mRemoteDevice = null; 188 189 if (VERBOSE) Log.v(TAG, "MAP Service closeService out"); 190 } 191 192 /** 193 * Starts the RFComm listener threads for each MAS 194 * @throws IOException 195 */ 196 private final void startRfcommSocketListeners(int masId) { 197 if(masId == -1) { 198 for(int i=0, c=mMasInstances.size(); i < c; i++) { 199 mMasInstances.valueAt(i).startRfcommSocketListener(); 200 } 201 } else { 202 BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1 203 if(masInst != null) { 204 masInst.startRfcommSocketListener(); 205 } else { 206 Log.w(TAG, "startRfcommSocketListeners(): Invalid MasId: " + masId); 207 } 208 } 209 } 210 211 /** 212 * Start a MAS instance for SMS/MMS and each e-mail account. 213 */ 214 private final void startObexServerSessions() { 215 if (DEBUG) Log.d(TAG, "Map Service START ObexServerSessions()"); 216 217 // acquire the wakeLock before start Obex transaction thread 218 if (mWakeLock == null) { 219 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 220 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 221 "StartingObexMapTransaction"); 222 mWakeLock.setReferenceCounted(false); 223 mWakeLock.acquire(); 224 if (VERBOSE) Log.v(TAG, "startObexSessions(): Acquire Wake Lock"); 225 } 226 227 if(mBluetoothMnsObexClient == null) { 228 mBluetoothMnsObexClient = 229 new BluetoothMnsObexClient(mRemoteDevice, mMnsRecord, mSessionStatusHandler); 230 } 231 232 boolean connected = false; 233 for(int i=0, c=mMasInstances.size(); i < c; i++) { 234 try { 235 if(mMasInstances.valueAt(i) 236 .startObexServerSession(mBluetoothMnsObexClient) == true) { 237 connected = true; 238 } 239 } catch (IOException e) { 240 Log.w(TAG,"IOException occured while starting an obexServerSession restarting" + 241 " the listener",e); 242 mMasInstances.valueAt(i).restartObexServerSession(); 243 } catch (RemoteException e) { 244 Log.w(TAG,"RemoteException occured while starting an obexServerSession restarting" + 245 " the listener",e); 246 mMasInstances.valueAt(i).restartObexServerSession(); 247 } 248 } 249 if(connected) { 250 setState(BluetoothMap.STATE_CONNECTED); 251 } 252 253 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 254 mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler 255 .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY); 256 257 if (VERBOSE) Log.v(TAG, "startObexServerSessions() success!"); 258 } 259 260 public Handler getHandler() { 261 return mSessionStatusHandler; 262 } 263 264 /** 265 * Restart a MAS instances. 266 * @param masId use -1 to stop all instances 267 */ 268 private void stopObexServerSessions(int masId) { 269 if (DEBUG) Log.d(TAG, "MAP Service STOP ObexServerSessions()"); 270 271 boolean lastMasInst = true; 272 273 if(masId != -1) { 274 for(int i=0, c=mMasInstances.size(); i < c; i++) { 275 BluetoothMapMasInstance masInst = mMasInstances.valueAt(i); 276 if(masInst.getMasId() != masId && masInst.isStarted()) { 277 lastMasInst = false; 278 } 279 } 280 } // Else just close down it all 281 282 /* Shutdown the MNS client - currently must happen before MAS close */ 283 if(mBluetoothMnsObexClient != null && lastMasInst) { 284 mBluetoothMnsObexClient.shutdown(); 285 mBluetoothMnsObexClient = null; 286 } 287 288 BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1 289 if(masInst != null) { 290 masInst.restartObexServerSession(); 291 } else { 292 for(int i=0, c=mMasInstances.size(); i < c; i++) { 293 mMasInstances.valueAt(i).restartObexServerSession(); 294 } 295 } 296 297 if(lastMasInst) { 298 setState(BluetoothMap.STATE_DISCONNECTED); 299 mPermission = BluetoothDevice.ACCESS_UNKNOWN; 300 mRemoteDevice = null; 301 if(mAccountChanged) { 302 updateMasInstances(UPDATE_MAS_INSTANCES_ACCOUNT_DISCONNECT); 303 } 304 } 305 306 // Release the wake lock at disconnect 307 if (mWakeLock != null && lastMasInst) { 308 mSessionStatusHandler.removeMessages(MSG_ACQUIRE_WAKE_LOCK); 309 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 310 mWakeLock.release(); 311 if (VERBOSE) Log.v(TAG, "stopObexServerSessions(): Release Wake Lock"); 312 } 313 } 314 315 private final Handler mSessionStatusHandler = new Handler() { 316 @Override 317 public void handleMessage(Message msg) { 318 if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what); 319 320 switch (msg.what) { 321 case UPDATE_MAS_INSTANCES: 322 updateMasInstancesHandler(); 323 break; 324 case START_LISTENER: 325 if (mAdapter.isEnabled()) { 326 startRfcommSocketListeners(msg.arg1); 327 } 328 break; 329 case MSG_MAS_CONNECT: 330 onConnectHandler(msg.arg1); 331 break; 332 case MSG_MAS_CONNECT_CANCEL: 333 /* TODO: We need to handle this by accepting the connection and reject at 334 * OBEX level, by using ObexRejectServer - add timeout to handle clients not 335 * closing the transport channel. 336 */ 337 stopObexServerSessions(-1); 338 break; 339 case USER_TIMEOUT: 340 if (mIsWaitingAuthorization){ 341 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); 342 intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); 343 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 344 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 345 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 346 sendBroadcast(intent); 347 cancelUserTimeoutAlarm(); 348 mIsWaitingAuthorization = false; 349 stopObexServerSessions(-1); 350 } 351 break; 352 case MSG_SERVERSESSION_CLOSE: 353 stopObexServerSessions(msg.arg1); 354 break; 355 case MSG_SESSION_ESTABLISHED: 356 break; 357 case MSG_SESSION_DISCONNECTED: 358 // handled elsewhere 359 break; 360 case DISCONNECT_MAP: 361 disconnectMap((BluetoothDevice)msg.obj); 362 break; 363 case SHUTDOWN: 364 /* Ensure to call close from this handler to avoid starting new stuff 365 because of pending messages */ 366 closeService(); 367 break; 368 case MSG_ACQUIRE_WAKE_LOCK: 369 if (VERBOSE) Log.v(TAG, "Acquire Wake Lock request message"); 370 if (mWakeLock == null) { 371 PowerManager pm = (PowerManager)getSystemService( 372 Context.POWER_SERVICE); 373 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 374 "StartingObexMapTransaction"); 375 mWakeLock.setReferenceCounted(false); 376 } 377 if(!mWakeLock.isHeld()) { 378 mWakeLock.acquire(); 379 if (DEBUG) Log.d(TAG, " Acquired Wake Lock by message"); 380 } 381 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 382 mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler 383 .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY); 384 break; 385 case MSG_RELEASE_WAKE_LOCK: 386 if (VERBOSE) Log.v(TAG, "Release Wake Lock request message"); 387 if (mWakeLock != null) { 388 mWakeLock.release(); 389 if (DEBUG) Log.d(TAG, " Released Wake Lock by message"); 390 } 391 break; 392 default: 393 break; 394 } 395 } 396 }; 397 398 private void onConnectHandler(int masId) { 399 if (mIsWaitingAuthorization == true || mRemoteDevice == null 400 || mSdpSearchInitiated == true) { 401 return; 402 } 403 BluetoothMapMasInstance masInst = mMasInstances.get(masId); 404 // Need to ensure we are still allowed. 405 if (DEBUG) Log.d(TAG, "mPermission = " + mPermission); 406 if (mPermission == BluetoothDevice.ACCESS_ALLOWED) { 407 try { 408 if (VERBOSE) Log.v(TAG, "incoming connection accepted from: " 409 + sRemoteDeviceName + " automatically as trusted device"); 410 if (mBluetoothMnsObexClient != null && masInst != null) { 411 masInst.startObexServerSession(mBluetoothMnsObexClient); 412 } else { 413 startObexServerSessions(); 414 } 415 } catch (IOException ex) { 416 Log.e(TAG, "catch IOException starting obex server session", ex); 417 } catch (RemoteException ex) { 418 Log.e(TAG, "catch RemoteException starting obex server session", ex); 419 } 420 } 421 } 422 423 public int getState() { 424 return mState; 425 } 426 427 public BluetoothDevice getRemoteDevice() { 428 return mRemoteDevice; 429 } 430 private void setState(int state) { 431 setState(state, BluetoothMap.RESULT_SUCCESS); 432 } 433 434 private synchronized void setState(int state, int result) { 435 if (state != mState) { 436 if (DEBUG) Log.d(TAG, "Map state " + mState + " -> " + state + ", result = " 437 + result); 438 int prevState = mState; 439 mState = state; 440 Intent intent = new Intent(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); 441 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 442 intent.putExtra(BluetoothProfile.EXTRA_STATE, mState); 443 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 444 sendBroadcast(intent, BLUETOOTH_PERM); 445 AdapterService s = AdapterService.getAdapterService(); 446 if (s != null) { 447 s.onProfileConnectionStateChanged(mRemoteDevice, BluetoothProfile.MAP, 448 mState, prevState); 449 } 450 } 451 } 452 453 public static String getRemoteDeviceName() { 454 return sRemoteDeviceName; 455 } 456 457 public boolean disconnect(BluetoothDevice device) { 458 mSessionStatusHandler.sendMessage(mSessionStatusHandler 459 .obtainMessage(DISCONNECT_MAP, 0, 0, device)); 460 return true; 461 } 462 463 public boolean disconnectMap(BluetoothDevice device) { 464 boolean result = false; 465 if (DEBUG) Log.d(TAG, "disconnectMap"); 466 if (getRemoteDevice().equals(device)) { 467 switch (mState) { 468 case BluetoothMap.STATE_CONNECTED: 469 /* Disconnect all connections and restart all MAS instances */ 470 stopObexServerSessions(-1); 471 result = true; 472 break; 473 default: 474 break; 475 } 476 } 477 return result; 478 } 479 480 public List<BluetoothDevice> getConnectedDevices() { 481 List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); 482 synchronized(this) { 483 if (mState == BluetoothMap.STATE_CONNECTED && mRemoteDevice != null) { 484 devices.add(mRemoteDevice); 485 } 486 } 487 return devices; 488 } 489 490 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 491 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 492 Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); 493 int connectionState; 494 synchronized (this) { 495 for (BluetoothDevice device : bondedDevices) { 496 ParcelUuid[] featureUuids = device.getUuids(); 497 if (!BluetoothUuid.containsAnyUuid(featureUuids, MAP_UUIDS)) { 498 continue; 499 } 500 connectionState = getConnectionState(device); 501 for(int i = 0; i < states.length; i++) { 502 if (connectionState == states[i]) { 503 deviceList.add(device); 504 } 505 } 506 } 507 } 508 return deviceList; 509 } 510 511 public int getConnectionState(BluetoothDevice device) { 512 synchronized(this) { 513 if (getState() == BluetoothMap.STATE_CONNECTED && getRemoteDevice().equals(device)) { 514 return BluetoothProfile.STATE_CONNECTED; 515 } else { 516 return BluetoothProfile.STATE_DISCONNECTED; 517 } 518 } 519 } 520 521 public boolean setPriority(BluetoothDevice device, int priority) { 522 Settings.Global.putInt(getContentResolver(), 523 Settings.Global.getBluetoothMapPriorityKey(device.getAddress()), 524 priority); 525 if (VERBOSE) Log.v(TAG, "Saved priority " + device + " = " + priority); 526 return true; 527 } 528 529 public int getPriority(BluetoothDevice device) { 530 int priority = Settings.Global.getInt(getContentResolver(), 531 Settings.Global.getBluetoothMapPriorityKey(device.getAddress()), 532 BluetoothProfile.PRIORITY_UNDEFINED); 533 return priority; 534 } 535 536 @Override 537 protected IProfileServiceBinder initBinder() { 538 return new BluetoothMapBinder(this); 539 } 540 541 @Override 542 protected boolean start() { 543 if (DEBUG) Log.d(TAG, "start()"); 544 IntentFilter filter = new IntentFilter(); 545 filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); 546 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 547 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 548 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 549 filter.addAction(ACTION_SHOW_MAPS_SETTINGS); 550 filter.addAction(USER_CONFIRM_TIMEOUT_ACTION); 551 552 // We need two filters, since Type only applies to the ACTION_MESSAGE_SENT 553 IntentFilter filterMessageSent = new IntentFilter(); 554 filterMessageSent.addAction(BluetoothMapContentObserver.ACTION_MESSAGE_SENT); 555 try{ 556 filterMessageSent.addDataType("message/*"); 557 } catch (MalformedMimeTypeException e) { 558 Log.e(TAG, "Wrong mime type!!!", e); 559 } 560 561 try { 562 registerReceiver(mMapReceiver, filter); 563 registerReceiver(mMapReceiver, filterMessageSent); 564 } catch (Exception e) { 565 Log.w(TAG,"Unable to register map receiver",e); 566 } 567 mAdapter = BluetoothAdapter.getDefaultAdapter(); 568 mAppObserver = new BluetoothMapAppObserver(this, this); 569 570 mEnabledAccounts = mAppObserver.getEnabledAccountItems(); 571 // Uses mEnabledAccounts, hence getEnabledAccountItems() must be called before this. 572 createMasInstances(); 573 574 // start RFCOMM listener 575 sendStartListenerMessage(-1); 576 return true; 577 } 578 579 /** 580 * Call this to trigger an update of the MAS instance list. 581 * No changes will be applied unless in disconnected state 582 */ 583 public void updateMasInstances(int action) { 584 mSessionStatusHandler.obtainMessage (UPDATE_MAS_INSTANCES, 585 action, 0).sendToTarget(); 586 } 587 588 /** 589 * Update the active MAS Instances according the difference between mEnabledDevices 590 * and the current list of accounts. 591 * Will only make changes if state is disconnected. 592 * 593 * How it works: 594 * 1) Build lists of account changes from last update of mEnabledAccounts. 595 * newAccounts - accounts that have been enabled since mEnabledAccounts 596 * was last updated. 597 * removedAccounts - Accounts that is on mEnabledAccounts, but no longer 598 * enabled. 599 * enabledAccounts - A new list of all enabled accounts. 600 * 2) Stop and remove all MasInstances on the remove list 601 * 3) Add and start MAS instances for accounts on the new list. 602 * Called at: 603 * - Each change in accounts 604 * - Each disconnect - before MasInstances restart. 605 * 606 * @return true is any changes are made, false otherwise. 607 */ 608 private boolean updateMasInstancesHandler(){ 609 if (DEBUG) Log.d(TAG,"updateMasInstancesHandler() state = " + getState()); 610 boolean changed = false; 611 612 if(getState() == BluetoothMap.STATE_DISCONNECTED) { 613 ArrayList<BluetoothMapAccountItem> newAccountList = 614 mAppObserver.getEnabledAccountItems(); 615 ArrayList<BluetoothMapAccountItem> newAccounts = null; 616 ArrayList<BluetoothMapAccountItem> removedAccounts = null; 617 newAccounts = new ArrayList<BluetoothMapAccountItem>(); 618 removedAccounts = mEnabledAccounts; // reuse the current enabled list, to track removed 619 // accounts 620 for(BluetoothMapAccountItem account: newAccountList) { 621 if(!removedAccounts.remove(account)) { 622 newAccounts.add(account); 623 } 624 } 625 626 if(removedAccounts != null) { 627 /* Remove all disabled/removed accounts */ 628 for(BluetoothMapAccountItem account : removedAccounts) { 629 BluetoothMapMasInstance masInst = mMasInstanceMap.remove(account); 630 if (VERBOSE) Log.v(TAG," Removing account: " + account + " masInst = " + masInst); 631 if(masInst != null) { 632 masInst.shutdown(); 633 mMasInstances.remove(masInst.getMasId()); 634 changed = true; 635 } 636 } 637 } 638 639 if(newAccounts != null) { 640 /* Add any newly created accounts */ 641 for(BluetoothMapAccountItem account : newAccounts) { 642 if (VERBOSE) Log.v(TAG," Adding account: " + account); 643 int masId = getNextMasId(); 644 BluetoothMapMasInstance newInst = 645 new BluetoothMapMasInstance(this, 646 this, 647 account, 648 masId, 649 false); 650 mMasInstances.append(masId, newInst); 651 mMasInstanceMap.put(account, newInst); 652 changed = true; 653 /* Start the new instance */ 654 if (mAdapter.isEnabled()) { 655 newInst.startRfcommSocketListener(); 656 } 657 } 658 } 659 mEnabledAccounts = newAccountList; 660 if (VERBOSE) { 661 Log.v(TAG," Enabled accounts:"); 662 for(BluetoothMapAccountItem account : mEnabledAccounts) { 663 Log.v(TAG, " " + account); 664 } 665 Log.v(TAG," Active MAS instances:"); 666 for(int i=0, c=mMasInstances.size(); i < c; i++) { 667 BluetoothMapMasInstance masInst = mMasInstances.valueAt(i); 668 Log.v(TAG, " " + masInst); 669 } 670 } 671 mAccountChanged = false; 672 } else { 673 mAccountChanged = true; 674 } 675 return changed; 676 } 677 678 /** 679 * Will return the next MasId to use. 680 * Will ensure the key returned is greater than the largest key in use. 681 * Unless the key 255 is in use, in which case the first free masId 682 * will be returned. 683 * @return 684 */ 685 private int getNextMasId() { 686 /* Find the largest masId in use */ 687 int largestMasId = 0; 688 for(int i=0, c=mMasInstances.size(); i < c; i++) { 689 int masId = mMasInstances.keyAt(i); 690 if(masId > largestMasId) { 691 largestMasId = masId; 692 } 693 } 694 if(largestMasId < 0xff) { 695 return largestMasId + 1; 696 } 697 /* If 0xff is already in use, wrap and choose the first free 698 * MasId. */ 699 for(int i = 1; i <= 0xff; i++) { 700 if(mMasInstances.get(i) == null) { 701 return i; 702 } 703 } 704 return 0xff; // This will never happen, as we only allow 10 e-mail accounts to be enabled 705 } 706 707 private void createMasInstances() { 708 int masId = MAS_ID_SMS_MMS; 709 710 // Add the SMS/MMS instance 711 BluetoothMapMasInstance smsMmsInst = 712 new BluetoothMapMasInstance(this, 713 this, 714 null, 715 masId, 716 true); 717 mMasInstances.append(masId, smsMmsInst); 718 mMasInstanceMap.put(null, smsMmsInst); 719 720 // get list of accounts already set to be visible through MAP 721 for(BluetoothMapAccountItem account : mEnabledAccounts) { 722 masId++; // SMS/MMS is masId=0, increment before adding next 723 BluetoothMapMasInstance newInst = 724 new BluetoothMapMasInstance(this, 725 this, 726 account, 727 masId, 728 false); 729 mMasInstances.append(masId, newInst); 730 mMasInstanceMap.put(account, newInst); 731 } 732 } 733 734 @Override 735 protected boolean stop() { 736 if (DEBUG) Log.d(TAG, "stop()"); 737 try { 738 unregisterReceiver(mMapReceiver); 739 mAppObserver.shutdown(); 740 } catch (Exception e) { 741 Log.w(TAG,"Unable to unregister map receiver",e); 742 } 743 744 setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED); 745 sendShutdownMessage(); 746 return true; 747 } 748 749 public boolean cleanup() { 750 if (DEBUG) Log.d(TAG, "cleanup()"); 751 setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED); 752 // TODO: Change to use message? - do we need to wait for completion? 753 closeService(); 754 return true; 755 } 756 757 /** 758 * Called from each MAS instance when a connection is received. 759 * @param remoteDevice The device connecting 760 * @param masInst a reference to the calling MAS instance. 761 * @return 762 */ 763 public boolean onConnect(BluetoothDevice remoteDevice, BluetoothMapMasInstance masInst) { 764 boolean sendIntent = false; 765 boolean cancelConnection = false; 766 767 // As this can be called from each MasInstance, we need to lock access to member variables 768 synchronized(this) { 769 if (mRemoteDevice == null) { 770 mRemoteDevice = remoteDevice; 771 sRemoteDeviceName = mRemoteDevice.getName(); 772 // In case getRemoteName failed and return null 773 if (TextUtils.isEmpty(sRemoteDeviceName)) { 774 sRemoteDeviceName = getString(R.string.defaultname); 775 } 776 777 mPermission = mRemoteDevice.getMessageAccessPermission(); 778 if (mPermission == BluetoothDevice.ACCESS_UNKNOWN) { 779 sendIntent = true; 780 mIsWaitingAuthorization = true; 781 setUserTimeoutAlarm(); 782 } else if (mPermission == BluetoothDevice.ACCESS_REJECTED) { 783 cancelConnection = true; 784 } else if(mPermission == BluetoothDevice.ACCESS_ALLOWED) { 785 mRemoteDevice.sdpSearch(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS); 786 mSdpSearchInitiated = true; 787 } 788 } else if (!mRemoteDevice.equals(remoteDevice)) { 789 Log.w(TAG, "Unexpected connection from a second Remote Device received. name: " + 790 ((remoteDevice==null)?"unknown":remoteDevice.getName())); 791 return false; /* The connecting device is different from what is already 792 connected, reject the connection. */ 793 } // Else second connection to same device, just continue 794 } 795 796 if (sendIntent) { 797 // This will trigger Settings app's dialog. 798 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 799 intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); 800 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 801 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 802 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 803 sendOrderedBroadcast(intent, BLUETOOTH_ADMIN_PERM); 804 805 if (VERBOSE) Log.v(TAG, "waiting for authorization for connection from: " 806 + sRemoteDeviceName); 807 //Queue USER_TIMEOUT to disconnect MAP OBEX session. If user doesn't 808 //accept or reject authorization request 809 } else if (cancelConnection) { 810 sendConnectCancelMessage(); 811 } else if (mPermission == BluetoothDevice.ACCESS_ALLOWED) { 812 /* Signal to the service that we have a incoming connection. */ 813 sendConnectMessage(masInst.getMasId()); 814 } 815 return true; 816 }; 817 818 819 private void setUserTimeoutAlarm(){ 820 if (DEBUG) Log.d(TAG,"SetUserTimeOutAlarm()"); 821 if(mAlarmManager == null){ 822 mAlarmManager =(AlarmManager) this.getSystemService (Context.ALARM_SERVICE); 823 } 824 mRemoveTimeoutMsg = true; 825 Intent timeoutIntent = 826 new Intent(USER_CONFIRM_TIMEOUT_ACTION); 827 PendingIntent pIntent = PendingIntent.getBroadcast(this, 0, timeoutIntent, 0); 828 mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 829 USER_CONFIRM_TIMEOUT_VALUE,pIntent); 830 } 831 832 private void cancelUserTimeoutAlarm(){ 833 if (DEBUG) Log.d(TAG,"cancelUserTimeOutAlarm()"); 834 Intent intent = new Intent(this, BluetoothMapService.class); 835 PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, 0); 836 AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); 837 alarmManager.cancel(sender); 838 mRemoveTimeoutMsg = false; 839 } 840 841 /** 842 * Start the incoming connection listeners for a MAS ID 843 * @param masId the MasID to start. Use -1 to start all listeners. 844 */ 845 public void sendStartListenerMessage(int masId) { 846 if(mSessionStatusHandler != null) { 847 Message msg = mSessionStatusHandler.obtainMessage(START_LISTENER, masId, 0); 848 /* We add a small delay here to ensure the call returns true before this message is 849 * handled. It seems wrong to add a delay, but the alternative is to build a lock 850 * system to handle synchronization, which isn't nice either... */ 851 mSessionStatusHandler.sendMessageDelayed(msg, 20); 852 } // Can only be null during shutdown 853 } 854 855 private void sendConnectMessage(int masId) { 856 if(mSessionStatusHandler != null) { 857 Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT, masId, 0); 858 /* We add a small delay here to ensure onConnect returns true before this message is 859 * handled. It seems wrong, but the alternative is to store a reference to the 860 * connection in this message, which isn't nice either... */ 861 mSessionStatusHandler.sendMessageDelayed(msg, 20); 862 } // Can only be null during shutdown 863 } 864 private void sendConnectTimeoutMessage() { 865 if (DEBUG) Log.d(TAG, "sendConnectTimeoutMessage()"); 866 if(mSessionStatusHandler != null) { 867 Message msg = mSessionStatusHandler.obtainMessage(USER_TIMEOUT); 868 msg.sendToTarget(); 869 } // Can only be null during shutdown 870 } 871 private void sendConnectCancelMessage() { 872 if(mSessionStatusHandler != null) { 873 Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT_CANCEL); 874 msg.sendToTarget(); 875 } // Can only be null during shutdown 876 } 877 878 private void sendShutdownMessage() { 879 /* Any pending messages are no longer valid. 880 To speed up things, simply delete them. */ 881 if (mRemoveTimeoutMsg) { 882 Intent timeoutIntent = 883 new Intent(USER_CONFIRM_TIMEOUT_ACTION); 884 sendBroadcast(timeoutIntent, BLUETOOTH_PERM); 885 mIsWaitingAuthorization = false; 886 cancelUserTimeoutAlarm(); 887 } 888 mSessionStatusHandler.removeCallbacksAndMessages(null); 889 // Request release of all resources 890 mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget(); 891 } 892 893 private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); 894 895 private class MapBroadcastReceiver extends BroadcastReceiver { 896 @Override 897 public void onReceive(Context context, Intent intent) { 898 if (DEBUG) Log.d(TAG, "onReceive"); 899 String action = intent.getAction(); 900 if (DEBUG) Log.d(TAG, "onReceive: " + action); 901 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 902 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 903 BluetoothAdapter.ERROR); 904 if (state == BluetoothAdapter.STATE_TURNING_OFF) { 905 if (DEBUG) Log.d(TAG, "STATE_TURNING_OFF"); 906 sendShutdownMessage(); 907 } else if (state == BluetoothAdapter.STATE_ON) { 908 if (DEBUG) Log.d(TAG, "STATE_ON"); 909 // start ServerSocket listener threads 910 sendStartListenerMessage(-1); 911 } 912 913 }else if (action.equals(USER_CONFIRM_TIMEOUT_ACTION)){ 914 if (DEBUG) Log.d(TAG, "USER_CONFIRM_TIMEOUT ACTION Received."); 915 // send us self a message about the timeout. 916 sendConnectTimeoutMessage(); 917 918 } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { 919 920 int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 921 BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); 922 923 if (DEBUG) Log.d(TAG, "Received ACTION_CONNECTION_ACCESS_REPLY:" + 924 requestType + "isWaitingAuthorization:" + mIsWaitingAuthorization); 925 if ((!mIsWaitingAuthorization) 926 || (requestType != BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS)) { 927 // this reply is not for us 928 return; 929 } 930 931 mIsWaitingAuthorization = false; 932 if (mRemoveTimeoutMsg) { 933 mSessionStatusHandler.removeMessages(USER_TIMEOUT); 934 cancelUserTimeoutAlarm(); 935 setState(BluetoothMap.STATE_DISCONNECTED); 936 } 937 938 if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, 939 BluetoothDevice.CONNECTION_ACCESS_NO) 940 == BluetoothDevice.CONNECTION_ACCESS_YES) { 941 // Bluetooth connection accepted by user 942 mPermission = BluetoothDevice.ACCESS_ALLOWED; 943 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { 944 boolean result = mRemoteDevice.setMessageAccessPermission( 945 BluetoothDevice.ACCESS_ALLOWED); 946 if (DEBUG) { 947 Log.d(TAG, "setMessageAccessPermission(ACCESS_ALLOWED) result=" 948 + result); 949 } 950 } 951 952 mRemoteDevice.sdpSearch(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS); 953 mSdpSearchInitiated = true; 954 } else { 955 // Auth. declined by user, serverSession should not be running, but 956 // call stop anyway to restart listener. 957 mPermission = BluetoothDevice.ACCESS_REJECTED; 958 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { 959 boolean result = mRemoteDevice.setMessageAccessPermission( 960 BluetoothDevice.ACCESS_REJECTED); 961 if (DEBUG) { 962 Log.d(TAG, "setMessageAccessPermission(ACCESS_REJECTED) result=" 963 + result); 964 } 965 } 966 sendConnectCancelMessage(); 967 } 968 } else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)){ 969// Log.v(TAG, "Received ACTION_SDP_RECORD."); 970 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); 971 if (VERBOSE) { 972 Log.v(TAG, "Received UUID: " + uuid.toString()); 973 Log.v(TAG, "expected UUID: " + 974 BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS.toString()); 975 } 976 if(uuid.equals(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS) 977 && mSdpSearchInitiated) 978 { 979 mMnsRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); 980 int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); 981 if (VERBOSE) { 982 Log.v(TAG, " -> MNS Record:" + mMnsRecord); 983 Log.v(TAG, " -> status: " + status); 984 } 985 mSdpSearchInitiated = false; // done searching 986 if(status != -1 && mMnsRecord != null){ 987 for(int i=0, c=mMasInstances.size(); i < c; i++) { 988 mMasInstances.valueAt(i).setRemoteFeatureMask( 989 mMnsRecord.getSupportedFeatures()); 990 } 991 } 992 sendConnectMessage(-1); // -1 indicates all MAS instances 993 } 994 } else if (action.equals(ACTION_SHOW_MAPS_SETTINGS)) { 995 if (VERBOSE) Log.v(TAG, "Received ACTION_SHOW_MAPS_SETTINGS."); 996 997 Intent in = new Intent(context, BluetoothMapSettings.class); 998 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 999 context.startActivity(in); 1000 } else if (action.equals(BluetoothMapContentObserver.ACTION_MESSAGE_SENT)) { 1001 BluetoothMapMasInstance masInst = null; 1002 int result = getResultCode(); 1003 boolean handled = false; 1004 if(mMasInstances != null && (masInst = mMasInstances.get(MAS_ID_SMS_MMS)) != null) { 1005 intent.putExtra(BluetoothMapContentObserver.EXTRA_MESSAGE_SENT_RESULT, result); 1006 if(masInst.handleSmsSendIntent(context, intent)) { 1007 // The intent was handled by the mas instance it self 1008 handled = true; 1009 } 1010 } 1011 if(handled == false) 1012 { 1013 /* We do not have a connection to a device, hence we need to move 1014 the SMS to the correct folder. */ 1015 BluetoothMapContentObserver 1016 .actionMessageSentDisconnected(context, intent, result); 1017 } 1018 } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) && 1019 mIsWaitingAuthorization) { 1020 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 1021 1022 if (mRemoteDevice == null || device == null) { 1023 Log.e(TAG, "Unexpected error!"); 1024 return; 1025 } 1026 1027 if (VERBOSE) Log.v(TAG,"ACL disconnected for " + device); 1028 1029 if (mRemoteDevice.equals(device)) { 1030 // Send any pending timeout now, as ACL got disconnected. 1031 mSessionStatusHandler.removeMessages(USER_TIMEOUT); 1032 1033 Intent timeoutIntent = 1034 new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); 1035 timeoutIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 1036 timeoutIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 1037 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 1038 sendBroadcast(timeoutIntent, BLUETOOTH_PERM); 1039 mIsWaitingAuthorization = false; 1040 cancelUserTimeoutAlarm(); 1041 mSessionStatusHandler.obtainMessage(MSG_SERVERSESSION_CLOSE, -1, 0) 1042 .sendToTarget(); 1043 } 1044 } 1045 } 1046 }; 1047 1048 //Binder object: Must be static class or memory leak may occur 1049 /** 1050 * This class implements the IBluetoothMap interface - or actually it validates the 1051 * preconditions for calling the actual functionality in the MapService, and calls it. 1052 */ 1053 private static class BluetoothMapBinder extends IBluetoothMap.Stub 1054 implements IProfileServiceBinder { 1055 private BluetoothMapService mService; 1056 1057 private BluetoothMapService getService() { 1058 if (!Utils.checkCaller()) { 1059 Log.w(TAG,"MAP call not allowed for non-active user"); 1060 return null; 1061 } 1062 1063 if (mService != null && mService.isAvailable()) { 1064 mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM,"Need BLUETOOTH permission"); 1065 return mService; 1066 } 1067 return null; 1068 } 1069 1070 BluetoothMapBinder(BluetoothMapService service) { 1071 if (VERBOSE) Log.v(TAG, "BluetoothMapBinder()"); 1072 mService = service; 1073 } 1074 1075 public boolean cleanup() { 1076 mService = null; 1077 return true; 1078 } 1079 1080 public int getState() { 1081 if (VERBOSE) Log.v(TAG, "getState()"); 1082 BluetoothMapService service = getService(); 1083 if (service == null) return BluetoothMap.STATE_DISCONNECTED; 1084 return getService().getState(); 1085 } 1086 1087 public BluetoothDevice getClient() { 1088 if (VERBOSE) Log.v(TAG, "getClient()"); 1089 BluetoothMapService service = getService(); 1090 if (service == null) return null; 1091 if (VERBOSE) Log.v(TAG, "getClient() - returning " + service.getRemoteDevice()); 1092 return service.getRemoteDevice(); 1093 } 1094 1095 public boolean isConnected(BluetoothDevice device) { 1096 if (VERBOSE) Log.v(TAG, "isConnected()"); 1097 BluetoothMapService service = getService(); 1098 if (service == null) return false; 1099 return service.getState() == BluetoothMap.STATE_CONNECTED 1100 && service.getRemoteDevice().equals(device); 1101 } 1102 1103 public boolean connect(BluetoothDevice device) { 1104 if (VERBOSE) Log.v(TAG, "connect()"); 1105 BluetoothMapService service = getService(); 1106 if (service == null) return false; 1107 return false; 1108 } 1109 1110 public boolean disconnect(BluetoothDevice device) { 1111 if (VERBOSE) Log.v(TAG, "disconnect()"); 1112 BluetoothMapService service = getService(); 1113 if (service == null) return false; 1114 return service.disconnect(device); 1115 } 1116 1117 public List<BluetoothDevice> getConnectedDevices() { 1118 if (VERBOSE) Log.v(TAG, "getConnectedDevices()"); 1119 BluetoothMapService service = getService(); 1120 if (service == null) return new ArrayList<BluetoothDevice>(0); 1121 return service.getConnectedDevices(); 1122 } 1123 1124 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 1125 if (VERBOSE) Log.v(TAG, "getDevicesMatchingConnectionStates()"); 1126 BluetoothMapService service = getService(); 1127 if (service == null) return new ArrayList<BluetoothDevice>(0); 1128 return service.getDevicesMatchingConnectionStates(states); 1129 } 1130 1131 public int getConnectionState(BluetoothDevice device) { 1132 if (VERBOSE) Log.v(TAG, "getConnectionState()"); 1133 BluetoothMapService service = getService(); 1134 if (service == null) return BluetoothProfile.STATE_DISCONNECTED; 1135 return service.getConnectionState(device); 1136 } 1137 1138 public boolean setPriority(BluetoothDevice device, int priority) { 1139 BluetoothMapService service = getService(); 1140 if (service == null) return false; 1141 return service.setPriority(device, priority); 1142 } 1143 1144 public int getPriority(BluetoothDevice device) { 1145 BluetoothMapService service = getService(); 1146 if (service == null) return BluetoothProfile.PRIORITY_UNDEFINED; 1147 return service.getPriority(device); 1148 } 1149 } 1150 1151 @Override 1152 public void dump(StringBuilder sb) { 1153 super.dump(sb); 1154 println(sb, "mRemoteDevice: " + mRemoteDevice); 1155 println(sb, "sRemoteDeviceName: " + sRemoteDeviceName); 1156 println(sb, "mState: " + mState); 1157 println(sb, "mAppObserver: " + mAppObserver); 1158 println(sb, "mIsWaitingAuthorization: " + mIsWaitingAuthorization); 1159 println(sb, "mRemoveTimeoutMsg: " + mRemoveTimeoutMsg); 1160 println(sb, "mPermission: " + mPermission); 1161 println(sb, "mAccountChanged: " + mAccountChanged); 1162 println(sb, "mBluetoothMnsObexClient: " + mBluetoothMnsObexClient); 1163 println(sb, "mMasInstanceMap:"); 1164 for (BluetoothMapAccountItem key : mMasInstanceMap.keySet()) { 1165 println(sb, " " + key + " : " + mMasInstanceMap.get(key)); 1166 } 1167 println(sb, "mEnabledAccounts:"); 1168 for (BluetoothMapAccountItem account : mEnabledAccounts) { 1169 println(sb, " " + account); 1170 } 1171 } 1172} 1173