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