BluetoothMapService.java revision bbb4110b455b3aa29106d5b4f0a37e1be8e09475
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; 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_EMAIL_SETTINGS = 78 "android.btmap.intent.action.SHOW_MAPS_EMAIL_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<BluetoothMapEmailSettingsItem, BluetoothMapMasInstance> mMasInstanceMap = 127 new HashMap<BluetoothMapEmailSettingsItem, BluetoothMapMasInstance>(1); 128 129 private BluetoothDevice mRemoteDevice = null; // The remote connected device - protect access 130 131 private ArrayList<BluetoothMapEmailSettingsItem> mEnabledAccounts = null; 132 private static String sRemoteDeviceName = null; 133 134 private int mState; 135 private BluetoothMapEmailAppObserver 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 private final void closeService() { 161 if (DEBUG) Log.d(TAG, "MAP Service closeService in"); 162 163 if (mBluetoothMnsObexClient != null) { 164 mBluetoothMnsObexClient.shutdown(); 165 mBluetoothMnsObexClient = null; 166 } 167 168 for(int i=0, c=mMasInstances.size(); i < c; i++) { 169 mMasInstances.valueAt(i).shutdown(); 170 } 171 mMasInstances.clear(); 172 173 if (mSessionStatusHandler != null) { 174 mSessionStatusHandler.removeCallbacksAndMessages(null); 175 } 176 177 mIsWaitingAuthorization = false; 178 mPermission = BluetoothDevice.ACCESS_UNKNOWN; 179 setState(BluetoothMap.STATE_DISCONNECTED); 180 181 if (mWakeLock != null) { 182 mWakeLock.release(); 183 if(VERBOSE)Log.i(TAG, "CloseService(): Release Wake Lock"); 184 mWakeLock = null; 185 } 186 mRemoteDevice = null; 187 188 if (VERBOSE) Log.v(TAG, "MAP Service closeService out"); 189 } 190 191 /** 192 * Starts the RFComm listener threads for each MAS 193 * @throws IOException 194 */ 195 private final void startRfcommSocketListeners(int masId) { 196 if(masId == -1) { 197 for(int i=0, c=mMasInstances.size(); i < c; i++) { 198 mMasInstances.valueAt(i).startRfcommSocketListener(); 199 } 200 } else { 201 BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1 202 if(masInst != null) { 203 masInst.startRfcommSocketListener(); 204 } else { 205 Log.w(TAG, "startRfcommSocketListeners(): Invalid MasId: " + masId); 206 } 207 } 208 } 209 210 /** 211 * Start a MAS instance for SMS/MMS and each e-mail account. 212 */ 213 private final void startObexServerSessions() { 214 if (DEBUG) Log.d(TAG, "Map Service START ObexServerSessions()"); 215 216 // acquire the wakeLock before start Obex transaction thread 217 if (mWakeLock == null) { 218 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 219 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 220 "StartingObexMapTransaction"); 221 mWakeLock.setReferenceCounted(false); 222 mWakeLock.acquire(); 223 if(VERBOSE)Log.i(TAG, "startObexSessions(): Acquire Wake Lock"); 224 } 225 226 if(mBluetoothMnsObexClient == null) { 227 mBluetoothMnsObexClient = 228 new BluetoothMnsObexClient(mRemoteDevice, mMnsRecord, mSessionStatusHandler); 229 } 230 231 boolean connected = false; 232 for(int i=0, c=mMasInstances.size(); i < c; i++) { 233 try { 234 if(mMasInstances.valueAt(i) 235 .startObexServerSession(mBluetoothMnsObexClient) == true) { 236 connected = true; 237 } 238 } catch (IOException e) { 239 Log.w(TAG,"IOException occured while starting an obexServerSession restarting" + 240 " the listener",e); 241 mMasInstances.valueAt(i).restartObexServerSession(); 242 } catch (RemoteException e) { 243 Log.w(TAG,"RemoteException occured while starting an obexServerSession restarting" + 244 " the listener",e); 245 mMasInstances.valueAt(i).restartObexServerSession(); 246 } 247 } 248 if(connected) { 249 setState(BluetoothMap.STATE_CONNECTED); 250 } 251 252 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 253 mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler 254 .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY); 255 256 if (VERBOSE) { 257 Log.v(TAG, "startObexServerSessions() success!"); 258 } 259 } 260 261 public Handler getHandler() { 262 return mSessionStatusHandler; 263 } 264 265 /** 266 * Restart a MAS instances. 267 * @param masId use -1 to stop all instances 268 */ 269 private void stopObexServerSessions(int masId) { 270 if (DEBUG) Log.d(TAG, "MAP Service STOP ObexServerSessions()"); 271 272 boolean lastMasInst = true; 273 274 if(masId != -1) { 275 for(int i=0, c=mMasInstances.size(); i < c; i++) { 276 BluetoothMapMasInstance masInst = mMasInstances.valueAt(i); 277 if(masInst.getMasId() != masId && masInst.isStarted()) { 278 lastMasInst = false; 279 } 280 } 281 } // Else just close down it all 282 283 /* Shutdown the MNS client - currently must happen before MAS close */ 284 if(mBluetoothMnsObexClient != null && lastMasInst) { 285 mBluetoothMnsObexClient.shutdown(); 286 mBluetoothMnsObexClient = null; 287 } 288 289 BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1 290 if(masInst != null) { 291 masInst.restartObexServerSession(); 292 } else { 293 for(int i=0, c=mMasInstances.size(); i < c; i++) { 294 mMasInstances.valueAt(i).restartObexServerSession(); 295 } 296 } 297 298 if(lastMasInst) { 299 setState(BluetoothMap.STATE_DISCONNECTED); 300 mPermission = BluetoothDevice.ACCESS_UNKNOWN; 301 mRemoteDevice = null; 302 if(mAccountChanged) { 303 updateMasInstances(UPDATE_MAS_INSTANCES_ACCOUNT_DISCONNECT); 304 } 305 } 306 307 // Release the wake lock at disconnect 308 if (mWakeLock != null && lastMasInst) { 309 mSessionStatusHandler.removeMessages(MSG_ACQUIRE_WAKE_LOCK); 310 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 311 mWakeLock.release(); 312 if(VERBOSE)Log.i(TAG, "stopObexServerSessions(): Release Wake Lock"); 313 } 314 } 315 316 private final Handler mSessionStatusHandler = new Handler() { 317 @Override 318 public void handleMessage(Message msg) { 319 if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what); 320 321 switch (msg.what) { 322 case UPDATE_MAS_INSTANCES: 323 updateMasInstancesHandler(); 324 break; 325 case START_LISTENER: 326 if (mAdapter.isEnabled()) { 327 startRfcommSocketListeners(msg.arg1); 328 } 329 break; 330 case MSG_MAS_CONNECT: 331 onConnectHandler(msg.arg1); 332 break; 333 case MSG_MAS_CONNECT_CANCEL: 334 /* TODO: We need to handle this by accepting the connection and reject at 335 * OBEX level, by using ObexRejectServer - add timeout to handle clients not 336 * closing the transport channel. 337 */ 338 stopObexServerSessions(-1); 339 break; 340 case USER_TIMEOUT: 341 if (mIsWaitingAuthorization){ 342 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); 343 intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); 344 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 345 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 346 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 347 sendBroadcast(intent); 348 cancelUserTimeoutAlarm(); 349 mIsWaitingAuthorization = false; 350 stopObexServerSessions(-1); 351 } 352 break; 353 case MSG_SERVERSESSION_CLOSE: 354 stopObexServerSessions(msg.arg1); 355 break; 356 case MSG_SESSION_ESTABLISHED: 357 break; 358 case MSG_SESSION_DISCONNECTED: 359 // handled elsewhere 360 break; 361 case DISCONNECT_MAP: 362 disconnectMap((BluetoothDevice)msg.obj); 363 break; 364 case SHUTDOWN: 365 /* Ensure to call close from this handler to avoid starting new stuff 366 because of pending messages */ 367 closeService(); 368 break; 369 case MSG_ACQUIRE_WAKE_LOCK: 370 if(VERBOSE)Log.i(TAG, "Acquire Wake Lock request message"); 371 if (mWakeLock == null) { 372 PowerManager pm = (PowerManager)getSystemService( 373 Context.POWER_SERVICE); 374 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 375 "StartingObexMapTransaction"); 376 mWakeLock.setReferenceCounted(false); 377 } 378 if(!mWakeLock.isHeld()) { 379 mWakeLock.acquire(); 380 if(DEBUG)Log.i(TAG, " Acquired Wake Lock by message"); 381 } 382 mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK); 383 mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler 384 .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY); 385 break; 386 case MSG_RELEASE_WAKE_LOCK: 387 if(VERBOSE)Log.i(TAG, "Release Wake Lock request message"); 388 if (mWakeLock != null) { 389 mWakeLock.release(); 390 if(DEBUG) Log.i(TAG, " Released Wake Lock by message"); 391 } 392 break; 393 default: 394 break; 395 } 396 } 397 }; 398 399 private void onConnectHandler(int masId) { 400 if (mIsWaitingAuthorization == true || mRemoteDevice == null) { 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 (DEBUG) Log.d(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 (DEBUG) Log.d(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_EMAIL_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 BluetoothMapEmailAppObserver(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<BluetoothMapEmailSettingsItem> newAccountList = 614 mAppObserver.getEnabledAccountItems(); 615 ArrayList<BluetoothMapEmailSettingsItem> newAccounts = null; 616 ArrayList<BluetoothMapEmailSettingsItem> removedAccounts = null; 617 newAccounts = new ArrayList<BluetoothMapEmailSettingsItem>(); 618 removedAccounts = mEnabledAccounts; // reuse the current enabled list, to track removed 619 // accounts 620 for(BluetoothMapEmailSettingsItem 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(BluetoothMapEmailSettingsItem account : removedAccounts) { 629 BluetoothMapMasInstance masInst = mMasInstanceMap.remove(account); 630 if(DEBUG)Log.d(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(BluetoothMapEmailSettingsItem account : newAccounts) { 642 if(DEBUG)Log.d(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.d(TAG," Enabled accounts:"); 662 for(BluetoothMapEmailSettingsItem account : mEnabledAccounts) { 663 Log.d(TAG, " " + account); 664 } 665 Log.d(TAG," Active MAS instances:"); 666 for(int i=0, c=mMasInstances.size(); i < c; i++) { 667 BluetoothMapMasInstance masInst = mMasInstances.valueAt(i); 668 Log.d(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(BluetoothMapEmailSettingsItem 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 } 785 } else if (!mRemoteDevice.equals(remoteDevice)) { 786 Log.w(TAG, "Unexpected connection from a second Remote Device received. name: " + 787 ((remoteDevice==null)?"unknown":remoteDevice.getName())); 788 return false; /* The connecting device is different from what is already 789 connected, reject the connection. */ 790 } // Else second connection to same device, just continue 791 } 792 793 if (sendIntent) { 794 // This will trigger Settings app's dialog. 795 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 796 intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); 797 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 798 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 799 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 800 sendOrderedBroadcast(intent, BLUETOOTH_ADMIN_PERM); 801 802 if (DEBUG) Log.d(TAG, "waiting for authorization for connection from: " 803 + sRemoteDeviceName); 804 //Queue USER_TIMEOUT to disconnect MAP OBEX session. If user doesn't 805 //accept or reject authorization request 806 } else if (cancelConnection) { 807 sendConnectCancelMessage(); 808 } else if (mPermission == BluetoothDevice.ACCESS_ALLOWED) { 809 /* Signal to the service that we have a incoming connection. */ 810 sendConnectMessage(masInst.getMasId()); 811 } 812 return true; 813 }; 814 815 816 private void setUserTimeoutAlarm(){ 817 if(DEBUG)Log.d(TAG,"SetUserTimeOutAlarm()"); 818 if(mAlarmManager == null){ 819 mAlarmManager =(AlarmManager) this.getSystemService (Context.ALARM_SERVICE); 820 } 821 mRemoveTimeoutMsg = true; 822 Intent timeoutIntent = 823 new Intent(USER_CONFIRM_TIMEOUT_ACTION); 824 PendingIntent pIntent = PendingIntent.getBroadcast(this, 0, timeoutIntent, 0); 825 mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 826 USER_CONFIRM_TIMEOUT_VALUE,pIntent); 827 } 828 829 private void cancelUserTimeoutAlarm(){ 830 if(DEBUG)Log.d(TAG,"cancelUserTimeOutAlarm()"); 831 Intent intent = new Intent(this, BluetoothMapService.class); 832 PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, 0); 833 AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); 834 alarmManager.cancel(sender); 835 mRemoveTimeoutMsg = false; 836 } 837 838 /** 839 * Start the incoming connection listeners for a MAS ID 840 * @param masId the MasID to start. Use -1 to start all listeners. 841 */ 842 public void sendStartListenerMessage(int masId) { 843 if(mSessionStatusHandler != null) { 844 Message msg = mSessionStatusHandler.obtainMessage(START_LISTENER, masId, 0); 845 /* We add a small delay here to ensure the call returns true before this message is 846 * handled. It seems wrong to add a delay, but the alternative is to build a lock 847 * system to handle synchronization, which isn't nice either... */ 848 mSessionStatusHandler.sendMessageDelayed(msg, 20); 849 } // Can only be null during shutdown 850 } 851 852 private void sendConnectMessage(int masId) { 853 if(mSessionStatusHandler != null) { 854 Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT, masId, 0); 855 /* We add a small delay here to ensure onConnect returns true before this message is 856 * handled. It seems wrong, but the alternative is to store a reference to the 857 * connection in this message, which isn't nice either... */ 858 mSessionStatusHandler.sendMessageDelayed(msg, 20); 859 } // Can only be null during shutdown 860 } 861 private void sendConnectTimeoutMessage() { 862 if (DEBUG) Log.d(TAG, "sendConnectTimeoutMessage()"); 863 if(mSessionStatusHandler != null) { 864 Message msg = mSessionStatusHandler.obtainMessage(USER_TIMEOUT); 865 msg.sendToTarget(); 866 } // Can only be null during shutdown 867 } 868 private void sendConnectCancelMessage() { 869 if(mSessionStatusHandler != null) { 870 Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT_CANCEL); 871 msg.sendToTarget(); 872 } // Can only be null during shutdown 873 } 874 875 private void sendShutdownMessage() { 876 /* Any pending messages are no longer valid. 877 To speed up things, simply delete them. */ 878 if (mRemoveTimeoutMsg) { 879 Intent timeoutIntent = 880 new Intent(USER_CONFIRM_TIMEOUT_ACTION); 881 sendBroadcast(timeoutIntent, BLUETOOTH_PERM); 882 mIsWaitingAuthorization = false; 883 cancelUserTimeoutAlarm(); 884 } 885 mSessionStatusHandler.removeCallbacksAndMessages(null); 886 // Request release of all resources 887 mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget(); 888 } 889 890 private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); 891 892 private class MapBroadcastReceiver extends BroadcastReceiver { 893 @Override 894 public void onReceive(Context context, Intent intent) { 895 if (DEBUG) Log.d(TAG, "onReceive"); 896 String action = intent.getAction(); 897 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 898 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 899 BluetoothAdapter.ERROR); 900 if (state == BluetoothAdapter.STATE_TURNING_OFF) { 901 if (DEBUG) Log.d(TAG, "STATE_TURNING_OFF"); 902 sendShutdownMessage(); 903 } else if (state == BluetoothAdapter.STATE_ON) { 904 if (DEBUG) Log.d(TAG, "STATE_ON"); 905 // start ServerSocket listener threads 906 sendStartListenerMessage(-1); 907 } 908 909 }else if (action.equals(USER_CONFIRM_TIMEOUT_ACTION)){ 910 if (DEBUG) Log.d(TAG, "USER_CONFIRM_TIMEOUT ACTION Received."); 911 // send us self a message about the timeout. 912 sendConnectTimeoutMessage(); 913 914 } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { 915 916 int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 917 BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); 918 919 if (DEBUG) Log.d(TAG, "Received ACTION_CONNECTION_ACCESS_REPLY:" + 920 requestType + "isWaitingAuthorization:" + mIsWaitingAuthorization); 921 if ((!mIsWaitingAuthorization) 922 || (requestType != BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS)) { 923 // this reply is not for us 924 return; 925 } 926 927 mIsWaitingAuthorization = false; 928 if (mRemoveTimeoutMsg) { 929 mSessionStatusHandler.removeMessages(USER_TIMEOUT); 930 cancelUserTimeoutAlarm(); 931 setState(BluetoothMap.STATE_DISCONNECTED); 932 } 933 934 if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, 935 BluetoothDevice.CONNECTION_ACCESS_NO) 936 == BluetoothDevice.CONNECTION_ACCESS_YES) { 937 // Bluetooth connection accepted by user 938 mPermission = BluetoothDevice.ACCESS_ALLOWED; 939 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { 940 boolean result = mRemoteDevice.setMessageAccessPermission( 941 BluetoothDevice.ACCESS_ALLOWED); 942 if (DEBUG) { 943 Log.d(TAG, "setMessageAccessPermission(ACCESS_ALLOWED) result=" 944 + result); 945 } 946 } 947 mRemoteDevice.sdpSearch(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS); 948 mSdpSearchInitiated = true; 949 } else { 950 // Auth. declined by user, serverSession should not be running, but 951 // call stop anyway to restart listener. 952 mPermission = BluetoothDevice.ACCESS_REJECTED; 953 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { 954 boolean result = mRemoteDevice.setMessageAccessPermission( 955 BluetoothDevice.ACCESS_REJECTED); 956 if (DEBUG) { 957 Log.d(TAG, "setMessageAccessPermission(ACCESS_REJECTED) result=" 958 + result); 959 } 960 } 961 sendConnectCancelMessage(); 962 } 963 } else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)){ 964 Log.v(TAG, "Received ACTION_SDP_RECORD."); 965 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); 966 Log.v(TAG, "Received UUID: " + uuid.toString()); 967 Log.v(TAG, "expected UUID: " + 968 BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS.toString()); 969 if(uuid.equals(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS) 970 && mSdpSearchInitiated) 971 { 972 mMnsRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); 973 Log.v(TAG, " -> MNS Record:" + mMnsRecord); 974 int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); 975 Log.v(TAG, " -> status: " + status); 976 mSdpSearchInitiated = false; // done searching 977 if(status != -1 && mMnsRecord != null){ 978 for(int i=0, c=mMasInstances.size(); i < c; i++) { 979 mMasInstances.valueAt(i).setRemoteFeatureMask( 980 mMnsRecord.getSupportedFeatures()); 981 } 982 } 983 sendConnectMessage(-1); // -1 indicates all MAS instances 984 } 985 } else if (action.equals(ACTION_SHOW_MAPS_EMAIL_SETTINGS)) { 986 Log.v(TAG, "Received ACTION_SHOW_MAPS_EMAIL_SETTINGS."); 987 988 Intent in = new Intent(context, BluetoothMapEmailSettings.class); 989 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 990 context.startActivity(in); 991 } else if (action.equals(BluetoothMapContentObserver.ACTION_MESSAGE_SENT)) { 992 BluetoothMapMasInstance masInst = null; 993 int result = getResultCode(); 994 boolean handled = false; 995 if(mMasInstances != null && (masInst = mMasInstances.get(MAS_ID_SMS_MMS)) != null) { 996 intent.putExtra(BluetoothMapContentObserver.EXTRA_MESSAGE_SENT_RESULT, result); 997 if(masInst.handleSmsSendIntent(context, intent)) { 998 // The intent was handled by the mas instance it self 999 handled = true; 1000 } 1001 } 1002 if(handled == false) 1003 { 1004 /* We do not have a connection to a device, hence we need to move 1005 the SMS to the correct folder. */ 1006 BluetoothMapContentObserver 1007 .actionMessageSentDisconnected(context, intent, result); 1008 } 1009 } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) && 1010 mIsWaitingAuthorization) { 1011 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 1012 1013 if (mRemoteDevice == null || device == null) { 1014 Log.e(TAG, "Unexpected error!"); 1015 return; 1016 } 1017 1018 if (DEBUG) Log.d(TAG,"ACL disconnected for "+ device); 1019 1020 if (mRemoteDevice.equals(device) && mRemoveTimeoutMsg) { 1021 // Send any pending timeout now, as ACL got disconnected. 1022 mSessionStatusHandler.removeMessages(USER_TIMEOUT); 1023 1024 Intent timeoutIntent = 1025 new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); 1026 timeoutIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); 1027 timeoutIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 1028 BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS); 1029 sendBroadcast(timeoutIntent, BLUETOOTH_PERM); 1030 mIsWaitingAuthorization = false; 1031 mRemoveTimeoutMsg = false; 1032 1033 } 1034 } 1035 } 1036 }; 1037 1038 //Binder object: Must be static class or memory leak may occur 1039 /** 1040 * This class implements the IBluetoothMap interface - or actually it validates the 1041 * preconditions for calling the actual functionality in the MapService, and calls it. 1042 */ 1043 private static class BluetoothMapBinder extends IBluetoothMap.Stub 1044 implements IProfileServiceBinder { 1045 private BluetoothMapService mService; 1046 1047 private BluetoothMapService getService() { 1048 if (!Utils.checkCaller()) { 1049 Log.w(TAG,"MAP call not allowed for non-active user"); 1050 return null; 1051 } 1052 1053 if (mService != null && mService.isAvailable()) { 1054 mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM,"Need BLUETOOTH permission"); 1055 return mService; 1056 } 1057 return null; 1058 } 1059 1060 BluetoothMapBinder(BluetoothMapService service) { 1061 if (VERBOSE) Log.v(TAG, "BluetoothMapBinder()"); 1062 mService = service; 1063 } 1064 1065 public boolean cleanup() { 1066 mService = null; 1067 return true; 1068 } 1069 1070 public int getState() { 1071 if (VERBOSE) Log.v(TAG, "getState()"); 1072 BluetoothMapService service = getService(); 1073 if (service == null) return BluetoothMap.STATE_DISCONNECTED; 1074 return getService().getState(); 1075 } 1076 1077 public BluetoothDevice getClient() { 1078 if (VERBOSE) Log.v(TAG, "getClient()"); 1079 BluetoothMapService service = getService(); 1080 if (service == null) return null; 1081 Log.v(TAG, "getClient() - returning " + service.getRemoteDevice()); 1082 return service.getRemoteDevice(); 1083 } 1084 1085 public boolean isConnected(BluetoothDevice device) { 1086 if (VERBOSE) Log.v(TAG, "isConnected()"); 1087 BluetoothMapService service = getService(); 1088 if (service == null) return false; 1089 return service.getState() == BluetoothMap.STATE_CONNECTED 1090 && service.getRemoteDevice().equals(device); 1091 } 1092 1093 public boolean connect(BluetoothDevice device) { 1094 if (VERBOSE) Log.v(TAG, "connect()"); 1095 BluetoothMapService service = getService(); 1096 if (service == null) return false; 1097 return false; 1098 } 1099 1100 public boolean disconnect(BluetoothDevice device) { 1101 if (VERBOSE) Log.v(TAG, "disconnect()"); 1102 BluetoothMapService service = getService(); 1103 if (service == null) return false; 1104 return service.disconnect(device); 1105 } 1106 1107 public List<BluetoothDevice> getConnectedDevices() { 1108 if (VERBOSE) Log.v(TAG, "getConnectedDevices()"); 1109 BluetoothMapService service = getService(); 1110 if (service == null) return new ArrayList<BluetoothDevice>(0); 1111 return service.getConnectedDevices(); 1112 } 1113 1114 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 1115 if (VERBOSE) Log.v(TAG, "getDevicesMatchingConnectionStates()"); 1116 BluetoothMapService service = getService(); 1117 if (service == null) return new ArrayList<BluetoothDevice>(0); 1118 return service.getDevicesMatchingConnectionStates(states); 1119 } 1120 1121 public int getConnectionState(BluetoothDevice device) { 1122 if (VERBOSE) Log.v(TAG, "getConnectionState()"); 1123 BluetoothMapService service = getService(); 1124 if (service == null) return BluetoothProfile.STATE_DISCONNECTED; 1125 return service.getConnectionState(device); 1126 } 1127 1128 public boolean setPriority(BluetoothDevice device, int priority) { 1129 BluetoothMapService service = getService(); 1130 if (service == null) return false; 1131 return service.setPriority(device, priority); 1132 } 1133 1134 public int getPriority(BluetoothDevice device) { 1135 BluetoothMapService service = getService(); 1136 if (service == null) return BluetoothProfile.PRIORITY_UNDEFINED; 1137 return service.getPriority(device); 1138 } 1139 } 1140 1141 @Override 1142 public void dump(StringBuilder sb) { 1143 super.dump(sb); 1144 println(sb, "mRemoteDevice: " + mRemoteDevice); 1145 println(sb, "sRemoteDeviceName: " + sRemoteDeviceName); 1146 println(sb, "mState: " + mState); 1147 println(sb, "mAppObserver: " + mAppObserver); 1148 println(sb, "mIsWaitingAuthorization: " + mIsWaitingAuthorization); 1149 println(sb, "mRemoveTimeoutMsg: " + mRemoveTimeoutMsg); 1150 println(sb, "mPermission: " + mPermission); 1151 println(sb, "mAccountChanged: " + mAccountChanged); 1152 println(sb, "mBluetoothMnsObexClient: " + mBluetoothMnsObexClient); 1153 println(sb, "mMasInstanceMap:"); 1154 for (BluetoothMapEmailSettingsItem key : mMasInstanceMap.keySet()) { 1155 println(sb, " " + key + " : " + mMasInstanceMap.get(key)); 1156 } 1157 println(sb, "mEnabledAccounts:"); 1158 for (BluetoothMapEmailSettingsItem account : mEnabledAccounts) { 1159 println(sb, " " + account); 1160 } 1161 } 1162} 1163