1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.statusbar.policy;
18
19import static android.bluetooth.BluetoothAdapter.ERROR;
20import static com.android.systemui.statusbar.policy.BluetoothUtil.connectionStateToString;
21import static com.android.systemui.statusbar.policy.BluetoothUtil.deviceToString;
22import static com.android.systemui.statusbar.policy.BluetoothUtil.profileToString;
23import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToProfile;
24import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToString;
25import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidsToString;
26
27import android.bluetooth.BluetoothA2dp;
28import android.bluetooth.BluetoothA2dpSink;
29import android.bluetooth.BluetoothAdapter;
30import android.bluetooth.BluetoothDevice;
31import android.bluetooth.BluetoothHeadset;
32import android.bluetooth.BluetoothHeadsetClient;
33import android.bluetooth.BluetoothInputDevice;
34import android.bluetooth.BluetoothManager;
35import android.bluetooth.BluetoothMap;
36import android.bluetooth.BluetoothPan;
37import android.bluetooth.BluetoothProfile;
38import android.bluetooth.BluetoothProfile.ServiceListener;
39import android.content.BroadcastReceiver;
40import android.content.Context;
41import android.content.Intent;
42import android.content.IntentFilter;
43import android.os.Handler;
44import android.os.Looper;
45import android.os.Message;
46import android.os.ParcelUuid;
47import android.util.ArrayMap;
48import android.util.ArraySet;
49import android.util.Log;
50import android.util.SparseArray;
51
52import com.android.systemui.statusbar.policy.BluetoothUtil.Profile;
53
54import java.io.FileDescriptor;
55import java.io.PrintWriter;
56import java.util.ArrayList;
57import java.util.Set;
58
59public class BluetoothControllerImpl implements BluetoothController {
60    private static final String TAG = "BluetoothController";
61    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
62    // This controls the order in which we check the states.  Since a device can only have
63    // one state on screen, but can have multiple profiles, the later states override the
64    // value of earlier states.  So if a device has a profile in CONNECTING and one in
65    // CONNECTED, it will show as CONNECTED, theoretically this shouldn't really happen often,
66    // but seemed worth noting.
67    private static final int[] CONNECTION_STATES = {
68        BluetoothProfile.STATE_DISCONNECTED,
69        BluetoothProfile.STATE_DISCONNECTING,
70        BluetoothProfile.STATE_CONNECTING,
71        BluetoothProfile.STATE_CONNECTED,
72    };
73    // Update all the BT device states.
74    private static final int MSG_UPDATE_CONNECTION_STATES = 1;
75    // Update just one BT device.
76    private static final int MSG_UPDATE_SINGLE_CONNECTION_STATE = 2;
77    // Update whether devices are bonded or not.
78    private static final int MSG_UPDATE_BONDED_DEVICES = 3;
79
80    private static final int MSG_ADD_PROFILE = 4;
81    private static final int MSG_REM_PROFILE = 5;
82
83    private final Context mContext;
84    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
85    private final BluetoothAdapter mAdapter;
86    private final Receiver mReceiver = new Receiver();
87    private final ArrayMap<BluetoothDevice, DeviceInfo> mDeviceInfo = new ArrayMap<>();
88    private final SparseArray<BluetoothProfile> mProfiles = new SparseArray<>();
89
90    private final H mHandler;
91
92    private boolean mEnabled;
93    private boolean mConnecting;
94    private BluetoothDevice mLastDevice;
95
96    public BluetoothControllerImpl(Context context, Looper bgLooper) {
97        mContext = context;
98        mHandler = new H(bgLooper);
99
100        final BluetoothManager bluetoothManager =
101                (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
102        mAdapter = bluetoothManager.getAdapter();
103        if (mAdapter == null) {
104            Log.w(TAG, "Default BT adapter not found");
105            return;
106        }
107
108        mReceiver.register();
109        setAdapterState(mAdapter.getState());
110        updateBondedDevices();
111        bindAllProfiles();
112    }
113
114    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
115        pw.println("BluetoothController state:");
116        pw.print("  mAdapter="); pw.println(mAdapter);
117        pw.print("  mEnabled="); pw.println(mEnabled);
118        pw.print("  mConnecting="); pw.println(mConnecting);
119        pw.print("  mLastDevice="); pw.println(mLastDevice);
120        pw.print("  mCallbacks.size="); pw.println(mCallbacks.size());
121        pw.print("  mProfiles="); pw.println(profilesToString(mProfiles));
122        pw.print("  mDeviceInfo.size="); pw.println(mDeviceInfo.size());
123        for (int i = 0; i < mDeviceInfo.size(); i++) {
124            final BluetoothDevice device = mDeviceInfo.keyAt(i);
125            final DeviceInfo info = mDeviceInfo.valueAt(i);
126            pw.print("    "); pw.print(deviceToString(device));
127            pw.print('('); pw.print(uuidsToString(device)); pw.print(')');
128            pw.print("    "); pw.println(infoToString(info));
129        }
130    }
131
132    private static String infoToString(DeviceInfo info) {
133        return info == null ? null : ("connectionState=" +
134                connectionStateToString(CONNECTION_STATES[info.connectionStateIndex])
135                + ",bonded=" + info.bonded + ",profiles="
136                + profilesToString(info.connectedProfiles));
137    }
138
139    private static String profilesToString(SparseArray<?> profiles) {
140        final int N = profiles.size();
141        final StringBuffer buffer = new StringBuffer();
142        buffer.append('[');
143        for (int i = 0; i < N; i++) {
144            if (i != 0) {
145                buffer.append(',');
146            }
147            buffer.append(BluetoothUtil.profileToString(profiles.keyAt(i)));
148        }
149        buffer.append(']');
150        return buffer.toString();
151    }
152
153    public void addStateChangedCallback(Callback cb) {
154        mCallbacks.add(cb);
155        fireStateChange(cb);
156    }
157
158    @Override
159    public void removeStateChangedCallback(Callback cb) {
160        mCallbacks.remove(cb);
161    }
162
163    @Override
164    public boolean isBluetoothEnabled() {
165        return mAdapter != null && mAdapter.isEnabled();
166    }
167
168    @Override
169    public boolean isBluetoothConnected() {
170        return mAdapter != null
171                && mAdapter.getConnectionState() == BluetoothAdapter.STATE_CONNECTED;
172    }
173
174    @Override
175    public boolean isBluetoothConnecting() {
176        return mAdapter != null
177                && mAdapter.getConnectionState() == BluetoothAdapter.STATE_CONNECTING;
178    }
179
180    @Override
181    public void setBluetoothEnabled(boolean enabled) {
182        if (mAdapter != null) {
183            if (enabled) {
184                mAdapter.enable();
185            } else {
186                mAdapter.disable();
187            }
188        }
189    }
190
191    @Override
192    public boolean isBluetoothSupported() {
193        return mAdapter != null;
194    }
195
196    @Override
197    public ArraySet<PairedDevice> getPairedDevices() {
198        final ArraySet<PairedDevice> rt = new ArraySet<>();
199        for (int i = 0; i < mDeviceInfo.size(); i++) {
200            final BluetoothDevice device = mDeviceInfo.keyAt(i);
201            final DeviceInfo info = mDeviceInfo.valueAt(i);
202            if (!info.bonded) continue;
203            final PairedDevice paired = new PairedDevice();
204            paired.id = device.getAddress();
205            paired.tag = device;
206            paired.name = device.getAliasName();
207            paired.state = connectionStateToPairedDeviceState(info.connectionStateIndex);
208            rt.add(paired);
209        }
210        return rt;
211    }
212
213    private static int connectionStateToPairedDeviceState(int index) {
214        int state = CONNECTION_STATES[index];
215        if (state == BluetoothAdapter.STATE_CONNECTED) return PairedDevice.STATE_CONNECTED;
216        if (state == BluetoothAdapter.STATE_CONNECTING) return PairedDevice.STATE_CONNECTING;
217        if (state == BluetoothAdapter.STATE_DISCONNECTING) return PairedDevice.STATE_DISCONNECTING;
218        return PairedDevice.STATE_DISCONNECTED;
219    }
220
221    @Override
222    public void connect(final PairedDevice pd) {
223        connect(pd, true);
224    }
225
226    @Override
227    public void disconnect(PairedDevice pd) {
228        connect(pd, false);
229    }
230
231    private void connect(PairedDevice pd, final boolean connect) {
232        if (mAdapter == null || pd == null || pd.tag == null) return;
233        final BluetoothDevice device = (BluetoothDevice) pd.tag;
234        final DeviceInfo info = mDeviceInfo.get(device);
235        final String action = connect ? "connect" : "disconnect";
236        if (DEBUG) Log.d(TAG, action + " " + deviceToString(device));
237        final ParcelUuid[] uuids = device.getUuids();
238        if (uuids == null) {
239            Log.w(TAG, "No uuids returned, aborting " + action + " for " + deviceToString(device));
240            return;
241        }
242        SparseArray<Boolean> profiles = new SparseArray<>();
243        if (connect) {
244            // When connecting add every profile we can recognize by uuid.
245            for (ParcelUuid uuid : uuids) {
246                final int profile = uuidToProfile(uuid);
247                if (profile == 0) {
248                    Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: "
249                            + uuidToString(uuid));
250                    continue;
251                }
252                final boolean connected = info.connectedProfiles.get(profile, false);
253                if (!connected) {
254                    profiles.put(profile, true);
255                }
256            }
257        } else {
258            // When disconnecting, just add every profile we know they are connected to.
259            profiles = info.connectedProfiles;
260        }
261        for (int i = 0; i < profiles.size(); i++) {
262            final int profile = profiles.keyAt(i);
263            if (mProfiles.indexOfKey(profile) >= 0) {
264                final Profile p = BluetoothUtil.getProfile(mProfiles.get(profile));
265                final boolean ok = connect ? p.connect(device) : p.disconnect(device);
266                if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " "
267                        + (ok ? "succeeded" : "failed"));
268            } else {
269                Log.w(TAG, "Unable get get Profile for " + profileToString(profile));
270            }
271        }
272    }
273
274    @Override
275    public String getLastDeviceName() {
276        return mLastDevice != null ? mLastDevice.getAliasName() : null;
277    }
278
279    private void updateBondedDevices() {
280        mHandler.removeMessages(MSG_UPDATE_BONDED_DEVICES);
281        mHandler.sendEmptyMessage(MSG_UPDATE_BONDED_DEVICES);
282    }
283
284    private void updateConnectionStates() {
285        mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES);
286        mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE);
287        mHandler.sendEmptyMessage(MSG_UPDATE_CONNECTION_STATES);
288    }
289
290    private void updateConnectionState(BluetoothDevice device, int profile, int state) {
291        if (mHandler.hasMessages(MSG_UPDATE_CONNECTION_STATES)) {
292            // If we are about to update all the devices, then we don't need to update this one.
293            return;
294        }
295        mHandler.obtainMessage(MSG_UPDATE_SINGLE_CONNECTION_STATE, profile, state, device)
296                .sendToTarget();
297    }
298
299    private void handleUpdateBondedDevices() {
300        if (mAdapter == null) return;
301        final Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
302        for (DeviceInfo info : mDeviceInfo.values()) {
303            info.bonded = false;
304        }
305        int bondedCount = 0;
306        BluetoothDevice lastBonded = null;
307        if (bondedDevices != null) {
308            for (BluetoothDevice bondedDevice : bondedDevices) {
309                final boolean bonded = bondedDevice.getBondState() != BluetoothDevice.BOND_NONE;
310                updateInfo(bondedDevice).bonded = bonded;
311                if (bonded) {
312                    bondedCount++;
313                    lastBonded = bondedDevice;
314                }
315            }
316        }
317        if (mLastDevice == null && bondedCount == 1) {
318            mLastDevice = lastBonded;
319        }
320        updateConnectionStates();
321        firePairedDevicesChanged();
322    }
323
324    private void handleUpdateConnectionStates() {
325        final int N = mDeviceInfo.size();
326        for (int i = 0; i < N; i++) {
327            BluetoothDevice device = mDeviceInfo.keyAt(i);
328            DeviceInfo info = updateInfo(device);
329            info.connectionStateIndex = 0;
330            info.connectedProfiles.clear();
331            for (int j = 0; j < mProfiles.size(); j++) {
332                int state = mProfiles.valueAt(j).getConnectionState(device);
333                handleUpdateConnectionState(device, mProfiles.keyAt(j), state);
334            }
335        }
336        handleConnectionChange();
337        firePairedDevicesChanged();
338    }
339
340    private void handleUpdateConnectionState(BluetoothDevice device, int profile, int state) {
341        if (DEBUG) Log.d(TAG, "updateConnectionState " + BluetoothUtil.deviceToString(device)
342                + " " + BluetoothUtil.profileToString(profile)
343                + " " + BluetoothUtil.connectionStateToString(state));
344        DeviceInfo info = updateInfo(device);
345        int stateIndex = 0;
346        for (int i = 0; i < CONNECTION_STATES.length; i++) {
347            if (CONNECTION_STATES[i] == state) {
348                stateIndex = i;
349                break;
350            }
351        }
352        info.profileStates.put(profile, stateIndex);
353
354        info.connectionStateIndex = 0;
355        final int N = info.profileStates.size();
356        for (int i = 0; i < N; i++) {
357            if (info.profileStates.valueAt(i) > info.connectionStateIndex) {
358                info.connectionStateIndex = info.profileStates.valueAt(i);
359            }
360        }
361        if (state == BluetoothProfile.STATE_CONNECTED) {
362            info.connectedProfiles.put(profile, true);
363        } else {
364            info.connectedProfiles.remove(profile);
365        }
366    }
367
368    private void handleConnectionChange() {
369        // If we are no longer connected to the current device, see if we are connected to
370        // something else, so we don't display a name we aren't connected to.
371        if (mLastDevice != null &&
372                CONNECTION_STATES[mDeviceInfo.get(mLastDevice).connectionStateIndex]
373                        != BluetoothProfile.STATE_CONNECTED) {
374            // Make sure we don't keep this device while it isn't connected.
375            mLastDevice = null;
376            // Look for anything else connected.
377            final int size = mDeviceInfo.size();
378            for (int i = 0; i < size; i++) {
379                BluetoothDevice device = mDeviceInfo.keyAt(i);
380                DeviceInfo info = mDeviceInfo.valueAt(i);
381                if (CONNECTION_STATES[info.connectionStateIndex]
382                        == BluetoothProfile.STATE_CONNECTED) {
383                    mLastDevice = device;
384                    break;
385                }
386            }
387        }
388    }
389
390    private void bindAllProfiles() {
391        // Note: This needs to contain all of the types that can be returned by BluetoothUtil
392        // otherwise we can't find the profiles we need when we connect/disconnect.
393        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
394        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP_SINK);
395        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.AVRCP_CONTROLLER);
396        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET);
397        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET_CLIENT);
398        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.INPUT_DEVICE);
399        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.MAP);
400        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.PAN);
401        // Note Health is not in this list because health devices aren't 'connected'.
402        // If profiles are expanded to use more than just connection state and connect/disconnect
403        // then it should be added.
404    }
405
406    private void firePairedDevicesChanged() {
407        for (Callback cb : mCallbacks) {
408            cb.onBluetoothPairedDevicesChanged();
409        }
410    }
411
412    private void setAdapterState(int adapterState) {
413        final boolean enabled = adapterState == BluetoothAdapter.STATE_ON;
414        if (mEnabled == enabled) return;
415        mEnabled = enabled;
416        fireStateChange();
417    }
418
419    private void setConnecting(boolean connecting) {
420        if (mConnecting == connecting) return;
421        mConnecting = connecting;
422        fireStateChange();
423    }
424
425    private void fireStateChange() {
426        for (Callback cb : mCallbacks) {
427            fireStateChange(cb);
428        }
429    }
430
431    private void fireStateChange(Callback cb) {
432        cb.onBluetoothStateChange(mEnabled, mConnecting);
433    }
434
435    private static int getProfileFromAction(String action) {
436        if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
437            return BluetoothProfile.A2DP;
438        } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
439            return BluetoothProfile.HEADSET;
440        } else if (BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
441            return BluetoothProfile.A2DP_SINK;
442        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
443            return BluetoothProfile.HEADSET_CLIENT;
444        } else if (BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
445            return BluetoothProfile.INPUT_DEVICE;
446        } else if (BluetoothMap.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
447            return BluetoothProfile.MAP;
448        } else if (BluetoothPan.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
449            return BluetoothProfile.PAN;
450        }
451        if (DEBUG) Log.d(TAG, "Unknown action " + action);
452        return -1;
453    }
454
455    private final ServiceListener mProfileListener = new ServiceListener() {
456        @Override
457        public void onServiceDisconnected(int profile) {
458            if (DEBUG) Log.d(TAG, "Disconnected from " + BluetoothUtil.profileToString(profile));
459            // We lost a profile, don't do any updates until it gets removed.
460            mHandler.removeMessages(MSG_UPDATE_CONNECTION_STATES);
461            mHandler.removeMessages(MSG_UPDATE_SINGLE_CONNECTION_STATE);
462            mHandler.obtainMessage(MSG_REM_PROFILE, profile, 0).sendToTarget();
463        }
464
465        @Override
466        public void onServiceConnected(int profile, BluetoothProfile proxy) {
467            if (DEBUG) Log.d(TAG, "Connected to " + BluetoothUtil.profileToString(profile));
468            mHandler.obtainMessage(MSG_ADD_PROFILE, profile, 0, proxy).sendToTarget();
469        }
470    };
471
472    private final class Receiver extends BroadcastReceiver {
473        public void register() {
474            final IntentFilter filter = new IntentFilter();
475            filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
476            filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
477            filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
478            filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED);
479            filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
480            filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
481            filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
482            filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
483            filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
484            filter.addAction(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
485            filter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
486            mContext.registerReceiver(this, filter);
487        }
488
489        @Override
490        public void onReceive(Context context, Intent intent) {
491            final String action = intent.getAction();
492            final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
493
494            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
495                setAdapterState(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR));
496                updateBondedDevices();
497                if (DEBUG) Log.d(TAG, "ACTION_STATE_CHANGED " + mEnabled);
498            } else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) {
499                updateInfo(device);
500                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
501                        ERROR);
502                mLastDevice = device;
503                if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED "
504                        + connectionStateToString(state) + " " + deviceToString(device));
505                setConnecting(state == BluetoothAdapter.STATE_CONNECTING);
506            } else if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) {
507                updateInfo(device);
508                mLastDevice = device;
509            } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
510                if (DEBUG) Log.d(TAG, "ACTION_BOND_STATE_CHANGED " + device);
511                updateBondedDevices();
512            } else {
513                int profile = getProfileFromAction(intent.getAction());
514                int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
515                if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGE "
516                        + BluetoothUtil.profileToString(profile)
517                        + " " + BluetoothUtil.connectionStateToString(state));
518                if ((profile != -1) && (state != -1)) {
519                    updateConnectionState(device, profile, state);
520                }
521            }
522        }
523    }
524
525    private DeviceInfo updateInfo(BluetoothDevice device) {
526        DeviceInfo info = mDeviceInfo.get(device);
527        info = info != null ? info : new DeviceInfo();
528        mDeviceInfo.put(device, info);
529        return info;
530    }
531
532    private class H extends Handler {
533        public H(Looper l) {
534            super(l);
535        }
536
537        public void handleMessage(Message msg) {
538            switch (msg.what) {
539                case MSG_UPDATE_CONNECTION_STATES:
540                    handleUpdateConnectionStates();
541                    firePairedDevicesChanged();
542                    break;
543                case MSG_UPDATE_SINGLE_CONNECTION_STATE:
544                    handleUpdateConnectionState((BluetoothDevice) msg.obj, msg.arg1, msg.arg2);
545                    handleConnectionChange();
546                    firePairedDevicesChanged();
547                    break;
548                case MSG_UPDATE_BONDED_DEVICES:
549                    handleUpdateBondedDevices();
550                    firePairedDevicesChanged();
551                    break;
552                case MSG_ADD_PROFILE:
553                    mProfiles.put(msg.arg1, (BluetoothProfile) msg.obj);
554                    handleUpdateConnectionStates();
555                    firePairedDevicesChanged();
556                    break;
557                case MSG_REM_PROFILE:
558                    mProfiles.remove(msg.arg1);
559                    handleUpdateConnectionStates();
560                    firePairedDevicesChanged();
561                    break;
562            }
563        };
564    };
565
566    private static class DeviceInfo {
567        int connectionStateIndex = 0;
568        boolean bonded;  // per getBondedDevices
569        SparseArray<Boolean> connectedProfiles = new SparseArray<>();
570        SparseArray<Integer> profileStates = new SparseArray<>();
571    }
572}
573