1/*
2 * Copyright (C) 2010 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 android.bluetooth;
18
19import android.bluetooth.BluetoothPan;
20import android.bluetooth.BluetoothProfile;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.media.AudioManager;
26import android.os.Environment;
27import android.util.Log;
28
29import junit.framework.Assert;
30
31import java.io.BufferedWriter;
32import java.io.File;
33import java.io.FileWriter;
34import java.io.IOException;
35import java.util.ArrayList;
36import java.util.List;
37import java.util.Set;
38import java.util.concurrent.Semaphore;
39import java.util.concurrent.TimeUnit;
40
41public class BluetoothTestUtils extends Assert {
42
43    /** Timeout for enable/disable in ms. */
44    private static final int ENABLE_DISABLE_TIMEOUT = 20000;
45    /** Timeout for discoverable/undiscoverable in ms. */
46    private static final int DISCOVERABLE_UNDISCOVERABLE_TIMEOUT = 5000;
47    /** Timeout for starting/stopping a scan in ms. */
48    private static final int START_STOP_SCAN_TIMEOUT = 5000;
49    /** Timeout for pair/unpair in ms. */
50    private static final int PAIR_UNPAIR_TIMEOUT = 20000;
51    /** Timeout for connecting/disconnecting a profile in ms. */
52    private static final int CONNECT_DISCONNECT_PROFILE_TIMEOUT = 20000;
53    /** Timeout to start or stop a SCO channel in ms. */
54    private static final int START_STOP_SCO_TIMEOUT = 10000;
55    /** Timeout to connect a profile proxy in ms. */
56    private static final int CONNECT_PROXY_TIMEOUT = 5000;
57    /** Time between polls in ms. */
58    private static final int POLL_TIME = 100;
59
60    private abstract class FlagReceiver extends BroadcastReceiver {
61        private int mExpectedFlags = 0;
62        private int mFiredFlags = 0;
63        private long mCompletedTime = -1;
64
65        public FlagReceiver(int expectedFlags) {
66            mExpectedFlags = expectedFlags;
67        }
68
69        public int getFiredFlags() {
70            synchronized (this) {
71                return mFiredFlags;
72            }
73        }
74
75        public long getCompletedTime() {
76            synchronized (this) {
77                return mCompletedTime;
78            }
79        }
80
81        protected void setFiredFlag(int flag) {
82            synchronized (this) {
83                mFiredFlags |= flag;
84                if ((mFiredFlags & mExpectedFlags) == mExpectedFlags) {
85                    mCompletedTime = System.currentTimeMillis();
86                }
87            }
88        }
89    }
90
91    private class BluetoothReceiver extends FlagReceiver {
92        private static final int DISCOVERY_STARTED_FLAG = 1;
93        private static final int DISCOVERY_FINISHED_FLAG = 1 << 1;
94        private static final int SCAN_MODE_NONE_FLAG = 1 << 2;
95        private static final int SCAN_MODE_CONNECTABLE_FLAG = 1 << 3;
96        private static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG = 1 << 4;
97        private static final int STATE_OFF_FLAG = 1 << 5;
98        private static final int STATE_TURNING_ON_FLAG = 1 << 6;
99        private static final int STATE_ON_FLAG = 1 << 7;
100        private static final int STATE_TURNING_OFF_FLAG = 1 << 8;
101
102        public BluetoothReceiver(int expectedFlags) {
103            super(expectedFlags);
104        }
105
106        @Override
107        public void onReceive(Context context, Intent intent) {
108            if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(intent.getAction())) {
109                setFiredFlag(DISCOVERY_STARTED_FLAG);
110            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
111                setFiredFlag(DISCOVERY_FINISHED_FLAG);
112            } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
113                int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1);
114                assertNotSame(-1, mode);
115                switch (mode) {
116                    case BluetoothAdapter.SCAN_MODE_NONE:
117                        setFiredFlag(SCAN_MODE_NONE_FLAG);
118                        break;
119                    case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
120                        setFiredFlag(SCAN_MODE_CONNECTABLE_FLAG);
121                        break;
122                    case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
123                        setFiredFlag(SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG);
124                        break;
125                }
126            } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
127                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
128                assertNotSame(-1, state);
129                switch (state) {
130                    case BluetoothAdapter.STATE_OFF:
131                        setFiredFlag(STATE_OFF_FLAG);
132                        break;
133                    case BluetoothAdapter.STATE_TURNING_ON:
134                        setFiredFlag(STATE_TURNING_ON_FLAG);
135                        break;
136                    case BluetoothAdapter.STATE_ON:
137                        setFiredFlag(STATE_ON_FLAG);
138                        break;
139                    case BluetoothAdapter.STATE_TURNING_OFF:
140                        setFiredFlag(STATE_TURNING_OFF_FLAG);
141                        break;
142                }
143            }
144        }
145    }
146
147    private class PairReceiver extends FlagReceiver {
148        private static final int STATE_BONDED_FLAG = 1;
149        private static final int STATE_BONDING_FLAG = 1 << 1;
150        private static final int STATE_NONE_FLAG = 1 << 2;
151
152        private BluetoothDevice mDevice;
153        private int mPasskey;
154        private byte[] mPin;
155
156        public PairReceiver(BluetoothDevice device, int passkey, byte[] pin, int expectedFlags) {
157            super(expectedFlags);
158
159            mDevice = device;
160            mPasskey = passkey;
161            mPin = pin;
162        }
163
164        @Override
165        public void onReceive(Context context, Intent intent) {
166            if (!mDevice.equals(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE))) {
167                return;
168            }
169
170            if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
171                int varient = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, -1);
172                assertNotSame(-1, varient);
173                switch (varient) {
174                    case BluetoothDevice.PAIRING_VARIANT_PIN:
175                    case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
176                        mDevice.setPin(mPin);
177                        break;
178                    case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
179                        mDevice.setPasskey(mPasskey);
180                        break;
181                    case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
182                    case BluetoothDevice.PAIRING_VARIANT_CONSENT:
183                        mDevice.setPairingConfirmation(true);
184                        break;
185                    case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
186                        mDevice.setRemoteOutOfBandData();
187                        break;
188                }
189            } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
190                int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
191                assertNotSame(-1, state);
192                switch (state) {
193                    case BluetoothDevice.BOND_NONE:
194                        setFiredFlag(STATE_NONE_FLAG);
195                        break;
196                    case BluetoothDevice.BOND_BONDING:
197                        setFiredFlag(STATE_BONDING_FLAG);
198                        break;
199                    case BluetoothDevice.BOND_BONDED:
200                        setFiredFlag(STATE_BONDED_FLAG);
201                        break;
202                }
203            }
204        }
205    }
206
207    private class ConnectProfileReceiver extends FlagReceiver {
208        private static final int STATE_DISCONNECTED_FLAG = 1;
209        private static final int STATE_CONNECTING_FLAG = 1 << 1;
210        private static final int STATE_CONNECTED_FLAG = 1 << 2;
211        private static final int STATE_DISCONNECTING_FLAG = 1 << 3;
212
213        private BluetoothDevice mDevice;
214        private int mProfile;
215        private String mConnectionAction;
216
217        public ConnectProfileReceiver(BluetoothDevice device, int profile, int expectedFlags) {
218            super(expectedFlags);
219
220            mDevice = device;
221            mProfile = profile;
222
223            switch (mProfile) {
224                case BluetoothProfile.A2DP:
225                    mConnectionAction = BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED;
226                    break;
227                case BluetoothProfile.HEADSET:
228                    mConnectionAction = BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED;
229                    break;
230                case BluetoothProfile.INPUT_DEVICE:
231                    mConnectionAction = BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED;
232                    break;
233                case BluetoothProfile.PAN:
234                    mConnectionAction = BluetoothPan.ACTION_CONNECTION_STATE_CHANGED;
235                    break;
236                default:
237                    mConnectionAction = null;
238            }
239        }
240
241        @Override
242        public void onReceive(Context context, Intent intent) {
243            if (mConnectionAction != null && mConnectionAction.equals(intent.getAction())) {
244                if (!mDevice.equals(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE))) {
245                    return;
246                }
247
248                int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
249                assertNotSame(-1, state);
250                switch (state) {
251                    case BluetoothProfile.STATE_DISCONNECTED:
252                        setFiredFlag(STATE_DISCONNECTED_FLAG);
253                        break;
254                    case BluetoothProfile.STATE_CONNECTING:
255                        setFiredFlag(STATE_CONNECTING_FLAG);
256                        break;
257                    case BluetoothProfile.STATE_CONNECTED:
258                        setFiredFlag(STATE_CONNECTED_FLAG);
259                        break;
260                    case BluetoothProfile.STATE_DISCONNECTING:
261                        setFiredFlag(STATE_DISCONNECTING_FLAG);
262                        break;
263                }
264            }
265        }
266    }
267
268    private class ConnectPanReceiver extends ConnectProfileReceiver {
269        private int mRole;
270
271        public ConnectPanReceiver(BluetoothDevice device, int role, int expectedFlags) {
272            super(device, BluetoothProfile.PAN, expectedFlags);
273
274            mRole = role;
275        }
276
277        @Override
278        public void onReceive(Context context, Intent intent) {
279            if (mRole != intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, -1)) {
280                return;
281            }
282
283            super.onReceive(context, intent);
284        }
285    }
286
287    private class StartStopScoReceiver extends FlagReceiver {
288        private static final int STATE_CONNECTED_FLAG = 1;
289        private static final int STATE_DISCONNECTED_FLAG = 1 << 1;
290
291        public StartStopScoReceiver(int expectedFlags) {
292            super(expectedFlags);
293        }
294
295        @Override
296        public void onReceive(Context context, Intent intent) {
297            if (AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED.equals(intent.getAction())) {
298                int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
299                        AudioManager.SCO_AUDIO_STATE_ERROR);
300                assertNotSame(AudioManager.SCO_AUDIO_STATE_ERROR, state);
301                switch(state) {
302                    case AudioManager.SCO_AUDIO_STATE_CONNECTED:
303                        setFiredFlag(STATE_CONNECTED_FLAG);
304                        break;
305                    case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
306                        setFiredFlag(STATE_DISCONNECTED_FLAG);
307                        break;
308                }
309            }
310        }
311    }
312
313    private BluetoothProfile.ServiceListener mServiceListener =
314            new BluetoothProfile.ServiceListener() {
315        @Override
316        public void onServiceConnected(int profile, BluetoothProfile proxy) {
317            synchronized (this) {
318                switch (profile) {
319                    case BluetoothProfile.A2DP:
320                        mA2dp = (BluetoothA2dp) proxy;
321                        break;
322                    case BluetoothProfile.HEADSET:
323                        mHeadset = (BluetoothHeadset) proxy;
324                        break;
325                    case BluetoothProfile.INPUT_DEVICE:
326                        mInput = (BluetoothInputDevice) proxy;
327                        break;
328                    case BluetoothProfile.PAN:
329                        mPan = (BluetoothPan) proxy;
330                        break;
331                }
332            }
333        }
334
335        @Override
336        public void onServiceDisconnected(int profile) {
337            synchronized (this) {
338                switch (profile) {
339                    case BluetoothProfile.A2DP:
340                        mA2dp = null;
341                        break;
342                    case BluetoothProfile.HEADSET:
343                        mHeadset = null;
344                        break;
345                    case BluetoothProfile.INPUT_DEVICE:
346                        mInput = null;
347                        break;
348                    case BluetoothProfile.PAN:
349                        mPan = null;
350                        break;
351                }
352            }
353        }
354    };
355
356    private List<BroadcastReceiver> mReceivers = new ArrayList<BroadcastReceiver>();
357
358    private BufferedWriter mOutputWriter;
359    private String mTag;
360    private String mOutputFile;
361
362    private Context mContext;
363    private BluetoothA2dp mA2dp = null;
364    private BluetoothHeadset mHeadset = null;
365    private BluetoothInputDevice mInput = null;
366    private BluetoothPan mPan = null;
367
368    /**
369     * Creates a utility instance for testing Bluetooth.
370     *
371     * @param context The context of the application using the utility.
372     * @param tag The log tag of the application using the utility.
373     */
374    public BluetoothTestUtils(Context context, String tag) {
375        this(context, tag, null);
376    }
377
378    /**
379     * Creates a utility instance for testing Bluetooth.
380     *
381     * @param context The context of the application using the utility.
382     * @param tag The log tag of the application using the utility.
383     * @param outputFile The path to an output file if the utility is to write results to a
384     *        separate file.
385     */
386    public BluetoothTestUtils(Context context, String tag, String outputFile) {
387        mContext = context;
388        mTag = tag;
389        mOutputFile = outputFile;
390
391        if (mOutputFile == null) {
392            mOutputWriter = null;
393        } else {
394            try {
395                mOutputWriter = new BufferedWriter(new FileWriter(new File(
396                        Environment.getExternalStorageDirectory(), mOutputFile), true));
397            } catch (IOException e) {
398                Log.w(mTag, "Test output file could not be opened", e);
399                mOutputWriter = null;
400            }
401        }
402    }
403
404    /**
405     * Closes the utility instance and unregisters any BroadcastReceivers.
406     */
407    public void close() {
408        while (!mReceivers.isEmpty()) {
409            mContext.unregisterReceiver(mReceivers.remove(0));
410        }
411
412        if (mOutputWriter != null) {
413            try {
414                mOutputWriter.close();
415            } catch (IOException e) {
416                Log.w(mTag, "Test output file could not be closed", e);
417            }
418        }
419    }
420
421    /**
422     * Enables Bluetooth and checks to make sure that Bluetooth was turned on and that the correct
423     * actions were broadcast.
424     *
425     * @param adapter The BT adapter.
426     */
427    public void enable(BluetoothAdapter adapter) {
428        writeOutput("Enabling Bluetooth adapter.");
429        assertFalse(adapter.isEnabled());
430        int btState = adapter.getState();
431        final Semaphore completionSemaphore = new Semaphore(0);
432        final BroadcastReceiver receiver = new BroadcastReceiver() {
433            @Override
434            public void onReceive(Context context, Intent intent) {
435                final String action = intent.getAction();
436                if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
437                    return;
438                }
439                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
440                        BluetoothAdapter.ERROR);
441                if (state == BluetoothAdapter.STATE_ON) {
442                    completionSemaphore.release();
443                }
444            }
445        };
446
447        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
448        mContext.registerReceiver(receiver, filter);
449        assertTrue(adapter.enable());
450        boolean success = false;
451        try {
452            success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
453            writeOutput(String.format("enable() completed in 0 ms"));
454        } catch (final InterruptedException e) {
455            // This should never happen but just in case it does, the test will fail anyway.
456        }
457        mContext.unregisterReceiver(receiver);
458        if (!success) {
459            fail(String.format("enable() timeout: state=%d (expected %d)", btState,
460                    BluetoothAdapter.STATE_ON));
461        }
462    }
463
464    /**
465     * Disables Bluetooth and checks to make sure that Bluetooth was turned off and that the correct
466     * actions were broadcast.
467     *
468     * @param adapter The BT adapter.
469     */
470    public void disable(BluetoothAdapter adapter) {
471        writeOutput("Disabling Bluetooth adapter.");
472        assertTrue(adapter.isEnabled());
473        int btState = adapter.getState();
474        final Semaphore completionSemaphore = new Semaphore(0);
475        final BroadcastReceiver receiver = new BroadcastReceiver() {
476            @Override
477            public void onReceive(Context context, Intent intent) {
478                final String action = intent.getAction();
479                if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
480                    return;
481                }
482                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
483                        BluetoothAdapter.ERROR);
484                if (state == BluetoothAdapter.STATE_OFF) {
485                    completionSemaphore.release();
486                }
487            }
488        };
489
490        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
491        mContext.registerReceiver(receiver, filter);
492        assertTrue(adapter.disable());
493        boolean success = false;
494        try {
495            success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
496            writeOutput(String.format("disable() completed in 0 ms"));
497        } catch (final InterruptedException e) {
498            // This should never happen but just in case it does, the test will fail anyway.
499        }
500        mContext.unregisterReceiver(receiver);
501        if (!success) {
502            fail(String.format("disable() timeout: state=%d (expected %d)", btState,
503                    BluetoothAdapter.STATE_OFF));
504        }
505    }
506
507    /**
508     * Puts the local device into discoverable mode and checks to make sure that the local device
509     * is in discoverable mode and that the correct actions were broadcast.
510     *
511     * @param adapter The BT adapter.
512     */
513    public void discoverable(BluetoothAdapter adapter) {
514        if (!adapter.isEnabled()) {
515            fail("discoverable() bluetooth not enabled");
516        }
517
518        int scanMode = adapter.getScanMode();
519        if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
520            return;
521        }
522
523        final Semaphore completionSemaphore = new Semaphore(0);
524        final BroadcastReceiver receiver = new BroadcastReceiver() {
525            @Override
526            public void onReceive(Context context, Intent intent) {
527                final String action = intent.getAction();
528                if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
529                    return;
530                }
531                final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
532                        BluetoothAdapter.SCAN_MODE_NONE);
533                if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
534                    completionSemaphore.release();
535                }
536            }
537        };
538
539        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
540        mContext.registerReceiver(receiver, filter);
541        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
542        boolean success = false;
543        try {
544            success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
545                    TimeUnit.MILLISECONDS);
546            writeOutput(String.format("discoverable() completed in 0 ms"));
547        } catch (final InterruptedException e) {
548            // This should never happen but just in case it does, the test will fail anyway.
549        }
550        mContext.unregisterReceiver(receiver);
551        if (!success) {
552            fail(String.format("discoverable() timeout: scanMode=%d (expected %d)", scanMode,
553                    BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
554        }
555    }
556
557    /**
558     * Puts the local device into connectable only mode and checks to make sure that the local
559     * device is in in connectable mode and that the correct actions were broadcast.
560     *
561     * @param adapter The BT adapter.
562     */
563    public void undiscoverable(BluetoothAdapter adapter) {
564        if (!adapter.isEnabled()) {
565            fail("undiscoverable() bluetooth not enabled");
566        }
567
568        int scanMode = adapter.getScanMode();
569        if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
570            return;
571        }
572
573        final Semaphore completionSemaphore = new Semaphore(0);
574        final BroadcastReceiver receiver = new BroadcastReceiver() {
575            @Override
576            public void onReceive(Context context, Intent intent) {
577                final String action = intent.getAction();
578                if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
579                    return;
580                }
581                final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
582                        BluetoothAdapter.SCAN_MODE_NONE);
583                if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
584                    completionSemaphore.release();
585                }
586            }
587        };
588
589        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
590        mContext.registerReceiver(receiver, filter);
591        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
592        boolean success = false;
593        try {
594            success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
595                    TimeUnit.MILLISECONDS);
596            writeOutput(String.format("undiscoverable() completed in 0 ms"));
597        } catch (InterruptedException e) {
598            // This should never happen but just in case it does, the test will fail anyway.
599        }
600        mContext.unregisterReceiver(receiver);
601        if (!success) {
602            fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d)", scanMode,
603                    BluetoothAdapter.SCAN_MODE_CONNECTABLE));
604        }
605    }
606
607    /**
608     * Starts a scan for remote devices and checks to make sure that the local device is scanning
609     * and that the correct actions were broadcast.
610     *
611     * @param adapter The BT adapter.
612     */
613    public void startScan(BluetoothAdapter adapter) {
614        int mask = BluetoothReceiver.DISCOVERY_STARTED_FLAG;
615
616        if (!adapter.isEnabled()) {
617            fail("startScan() bluetooth not enabled");
618        }
619
620        if (adapter.isDiscovering()) {
621            return;
622        }
623
624        BluetoothReceiver receiver = getBluetoothReceiver(mask);
625
626        long start = System.currentTimeMillis();
627        assertTrue(adapter.startDiscovery());
628
629        while (System.currentTimeMillis() - start < START_STOP_SCAN_TIMEOUT) {
630            if (adapter.isDiscovering() && ((receiver.getFiredFlags() & mask) == mask)) {
631                writeOutput(String.format("startScan() completed in %d ms",
632                        (receiver.getCompletedTime() - start)));
633                removeReceiver(receiver);
634                return;
635            }
636            sleep(POLL_TIME);
637        }
638
639        int firedFlags = receiver.getFiredFlags();
640        removeReceiver(receiver);
641        fail(String.format("startScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
642                adapter.isDiscovering(), firedFlags, mask));
643    }
644
645    /**
646     * Stops a scan for remote devices and checks to make sure that the local device is not scanning
647     * and that the correct actions were broadcast.
648     *
649     * @param adapter The BT adapter.
650     */
651    public void stopScan(BluetoothAdapter adapter) {
652        int mask = BluetoothReceiver.DISCOVERY_FINISHED_FLAG;
653
654        if (!adapter.isEnabled()) {
655            fail("stopScan() bluetooth not enabled");
656        }
657
658        if (!adapter.isDiscovering()) {
659            return;
660        }
661
662        BluetoothReceiver receiver = getBluetoothReceiver(mask);
663
664        long start = System.currentTimeMillis();
665        assertTrue(adapter.cancelDiscovery());
666
667        while (System.currentTimeMillis() - start < START_STOP_SCAN_TIMEOUT) {
668            if (!adapter.isDiscovering() && ((receiver.getFiredFlags() & mask) == mask)) {
669                writeOutput(String.format("stopScan() completed in %d ms",
670                        (receiver.getCompletedTime() - start)));
671                removeReceiver(receiver);
672                return;
673            }
674            sleep(POLL_TIME);
675        }
676
677        int firedFlags = receiver.getFiredFlags();
678        removeReceiver(receiver);
679        fail(String.format("stopScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
680                adapter.isDiscovering(), firedFlags, mask));
681
682    }
683
684    /**
685     * Enables PAN tethering on the local device and checks to make sure that tethering is enabled.
686     *
687     * @param adapter The BT adapter.
688     */
689    public void enablePan(BluetoothAdapter adapter) {
690        if (mPan == null) mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
691        assertNotNull(mPan);
692
693        long start = System.currentTimeMillis();
694        mPan.setBluetoothTethering(true);
695        long stop = System.currentTimeMillis();
696        assertTrue(mPan.isTetheringOn());
697
698        writeOutput(String.format("enablePan() completed in %d ms", (stop - start)));
699    }
700
701    /**
702     * Disables PAN tethering on the local device and checks to make sure that tethering is
703     * disabled.
704     *
705     * @param adapter The BT adapter.
706     */
707    public void disablePan(BluetoothAdapter adapter) {
708        if (mPan == null) mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
709        assertNotNull(mPan);
710
711        long start = System.currentTimeMillis();
712        mPan.setBluetoothTethering(false);
713        long stop = System.currentTimeMillis();
714        assertFalse(mPan.isTetheringOn());
715
716        writeOutput(String.format("disablePan() completed in %d ms", (stop - start)));
717    }
718
719    /**
720     * Initiates a pairing with a remote device and checks to make sure that the devices are paired
721     * and that the correct actions were broadcast.
722     *
723     * @param adapter The BT adapter.
724     * @param device The remote device.
725     * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
726     * @param pin The pairing pin if pairing requires a pin. Any value if not.
727     */
728    public void pair(BluetoothAdapter adapter, BluetoothDevice device, int passkey, byte[] pin) {
729        pairOrAcceptPair(adapter, device, passkey, pin, true);
730    }
731
732    /**
733     * Accepts a pairing with a remote device and checks to make sure that the devices are paired
734     * and that the correct actions were broadcast.
735     *
736     * @param adapter The BT adapter.
737     * @param device The remote device.
738     * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
739     * @param pin The pairing pin if pairing requires a pin. Any value if not.
740     */
741    public void acceptPair(BluetoothAdapter adapter, BluetoothDevice device, int passkey,
742            byte[] pin) {
743        pairOrAcceptPair(adapter, device, passkey, pin, false);
744    }
745
746    /**
747     * Helper method used by {@link #pair(BluetoothAdapter, BluetoothDevice, int, byte[])} and
748     * {@link #acceptPair(BluetoothAdapter, BluetoothDevice, int, byte[])} to either pair or accept
749     * a pairing request.
750     *
751     * @param adapter The BT adapter.
752     * @param device The remote device.
753     * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
754     * @param pin The pairing pin if pairing requires a pin. Any value if not.
755     * @param shouldPair Whether to pair or accept the pair.
756     */
757    private void pairOrAcceptPair(BluetoothAdapter adapter, BluetoothDevice device, int passkey,
758            byte[] pin, boolean shouldPair) {
759        int mask = PairReceiver.STATE_BONDING_FLAG | PairReceiver.STATE_BONDED_FLAG;
760        long start = -1;
761        String methodName;
762        if (shouldPair) {
763            methodName = String.format("pair(device=%s)", device);
764        } else {
765            methodName = String.format("acceptPair(device=%s)", device);
766        }
767
768        if (!adapter.isEnabled()) {
769            fail(String.format("%s bluetooth not enabled", methodName));
770        }
771
772        PairReceiver receiver = getPairReceiver(device, passkey, pin, mask);
773
774        int state = device.getBondState();
775        switch (state) {
776            case BluetoothDevice.BOND_NONE:
777                assertFalse(adapter.getBondedDevices().contains(device));
778                start = System.currentTimeMillis();
779                if (shouldPair) {
780                    assertTrue(device.createBond());
781                }
782                break;
783            case BluetoothDevice.BOND_BONDING:
784                mask = 0; // Don't check for received intents since we might have missed them.
785                break;
786            case BluetoothDevice.BOND_BONDED:
787                assertTrue(adapter.getBondedDevices().contains(device));
788                return;
789            default:
790                removeReceiver(receiver);
791                fail(String.format("%s invalid state: state=%d", methodName, state));
792        }
793
794        long s = System.currentTimeMillis();
795        while (System.currentTimeMillis() - s < PAIR_UNPAIR_TIMEOUT) {
796            state = device.getBondState();
797            if (state == BluetoothDevice.BOND_BONDED && (receiver.getFiredFlags() & mask) == mask) {
798                assertTrue(adapter.getBondedDevices().contains(device));
799                long finish = receiver.getCompletedTime();
800                if (start != -1 && finish != -1) {
801                    writeOutput(String.format("%s completed in %d ms", methodName,
802                            (finish - start)));
803                } else {
804                    writeOutput(String.format("%s completed", methodName));
805                }
806                removeReceiver(receiver);
807                return;
808            }
809            sleep(POLL_TIME);
810        }
811
812        int firedFlags = receiver.getFiredFlags();
813        removeReceiver(receiver);
814        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
815                methodName, state, BluetoothDevice.BOND_BONDED, firedFlags, mask));
816    }
817
818    /**
819     * Deletes a pairing with a remote device and checks to make sure that the devices are unpaired
820     * and that the correct actions were broadcast.
821     *
822     * @param adapter The BT adapter.
823     * @param device The remote device.
824     */
825    public void unpair(BluetoothAdapter adapter, BluetoothDevice device) {
826        int mask = PairReceiver.STATE_NONE_FLAG;
827        long start = -1;
828        String methodName = String.format("unpair(device=%s)", device);
829
830        if (!adapter.isEnabled()) {
831            fail(String.format("%s bluetooth not enabled", methodName));
832        }
833
834        PairReceiver receiver = getPairReceiver(device, 0, null, mask);
835
836        int state = device.getBondState();
837        switch (state) {
838            case BluetoothDevice.BOND_NONE:
839                assertFalse(adapter.getBondedDevices().contains(device));
840                removeReceiver(receiver);
841                return;
842            case BluetoothDevice.BOND_BONDING:
843                start = System.currentTimeMillis();
844                assertTrue(device.removeBond());
845                break;
846            case BluetoothDevice.BOND_BONDED:
847                assertTrue(adapter.getBondedDevices().contains(device));
848                start = System.currentTimeMillis();
849                assertTrue(device.removeBond());
850                break;
851            default:
852                removeReceiver(receiver);
853                fail(String.format("%s invalid state: state=%d", methodName, state));
854        }
855
856        long s = System.currentTimeMillis();
857        while (System.currentTimeMillis() - s < PAIR_UNPAIR_TIMEOUT) {
858            if (device.getBondState() == BluetoothDevice.BOND_NONE
859                    && (receiver.getFiredFlags() & mask) == mask) {
860                assertFalse(adapter.getBondedDevices().contains(device));
861                long finish = receiver.getCompletedTime();
862                if (start != -1 && finish != -1) {
863                    writeOutput(String.format("%s completed in %d ms", methodName,
864                            (finish - start)));
865                } else {
866                    writeOutput(String.format("%s completed", methodName));
867                }
868                removeReceiver(receiver);
869                return;
870            }
871        }
872
873        int firedFlags = receiver.getFiredFlags();
874        removeReceiver(receiver);
875        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
876                methodName, state, BluetoothDevice.BOND_BONDED, firedFlags, mask));
877    }
878
879    /**
880     * Deletes all pairings of remote devices
881     * @param adapter the BT adapter
882     */
883    public void unpairAll(BluetoothAdapter adapter) {
884        Set<BluetoothDevice> devices = adapter.getBondedDevices();
885        for (BluetoothDevice device : devices) {
886            unpair(adapter, device);
887        }
888    }
889
890    /**
891     * Connects a profile from the local device to a remote device and checks to make sure that the
892     * profile is connected and that the correct actions were broadcast.
893     *
894     * @param adapter The BT adapter.
895     * @param device The remote device.
896     * @param profile The profile to connect. One of {@link BluetoothProfile#A2DP},
897     * {@link BluetoothProfile#HEADSET}, or {@link BluetoothProfile#INPUT_DEVICE}.
898     * @param methodName The method name to printed in the logs.  If null, will be
899     * "connectProfile(profile=&lt;profile&gt;, device=&lt;device&gt;)"
900     */
901    public void connectProfile(BluetoothAdapter adapter, BluetoothDevice device, int profile,
902            String methodName) {
903        if (methodName == null) {
904            methodName = String.format("connectProfile(profile=%d, device=%s)", profile, device);
905        }
906        int mask = (ConnectProfileReceiver.STATE_CONNECTING_FLAG
907                | ConnectProfileReceiver.STATE_CONNECTED_FLAG);
908        long start = -1;
909
910        if (!adapter.isEnabled()) {
911            fail(String.format("%s bluetooth not enabled", methodName));
912        }
913
914        if (!adapter.getBondedDevices().contains(device)) {
915            fail(String.format("%s device not paired", methodName));
916        }
917
918        BluetoothProfile proxy = connectProxy(adapter, profile);
919        assertNotNull(proxy);
920
921        ConnectProfileReceiver receiver = getConnectProfileReceiver(device, profile, mask);
922
923        int state = proxy.getConnectionState(device);
924        switch (state) {
925            case BluetoothProfile.STATE_CONNECTED:
926                removeReceiver(receiver);
927                return;
928            case BluetoothProfile.STATE_CONNECTING:
929                mask = 0; // Don't check for received intents since we might have missed them.
930                break;
931            case BluetoothProfile.STATE_DISCONNECTED:
932            case BluetoothProfile.STATE_DISCONNECTING:
933                start = System.currentTimeMillis();
934                if (profile == BluetoothProfile.A2DP) {
935                    assertTrue(((BluetoothA2dp)proxy).connect(device));
936                } else if (profile == BluetoothProfile.HEADSET) {
937                    assertTrue(((BluetoothHeadset)proxy).connect(device));
938                } else if (profile == BluetoothProfile.INPUT_DEVICE) {
939                    assertTrue(((BluetoothInputDevice)proxy).connect(device));
940                }
941                break;
942            default:
943                removeReceiver(receiver);
944                fail(String.format("%s invalid state: state=%d", methodName, state));
945        }
946
947        long s = System.currentTimeMillis();
948        while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
949            state = proxy.getConnectionState(device);
950            if (state == BluetoothProfile.STATE_CONNECTED
951                    && (receiver.getFiredFlags() & mask) == mask) {
952                long finish = receiver.getCompletedTime();
953                if (start != -1 && finish != -1) {
954                    writeOutput(String.format("%s completed in %d ms", methodName,
955                            (finish - start)));
956                } else {
957                    writeOutput(String.format("%s completed", methodName));
958                }
959                removeReceiver(receiver);
960                return;
961            }
962            sleep(POLL_TIME);
963        }
964
965        int firedFlags = receiver.getFiredFlags();
966        removeReceiver(receiver);
967        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
968                methodName, state, BluetoothProfile.STATE_CONNECTED, firedFlags, mask));
969    }
970
971    /**
972     * Disconnects a profile between the local device and a remote device and checks to make sure
973     * that the profile is disconnected and that the correct actions were broadcast.
974     *
975     * @param adapter The BT adapter.
976     * @param device The remote device.
977     * @param profile The profile to disconnect. One of {@link BluetoothProfile#A2DP},
978     * {@link BluetoothProfile#HEADSET}, or {@link BluetoothProfile#INPUT_DEVICE}.
979     * @param methodName The method name to printed in the logs.  If null, will be
980     * "connectProfile(profile=&lt;profile&gt;, device=&lt;device&gt;)"
981     */
982    public void disconnectProfile(BluetoothAdapter adapter, BluetoothDevice device, int profile,
983            String methodName) {
984        if (methodName == null) {
985            methodName = String.format("disconnectProfile(profile=%d, device=%s)", profile, device);
986        }
987        int mask = (ConnectProfileReceiver.STATE_DISCONNECTING_FLAG
988                | ConnectProfileReceiver.STATE_DISCONNECTED_FLAG);
989        long start = -1;
990
991        if (!adapter.isEnabled()) {
992            fail(String.format("%s bluetooth not enabled", methodName));
993        }
994
995        if (!adapter.getBondedDevices().contains(device)) {
996            fail(String.format("%s device not paired", methodName));
997        }
998
999        BluetoothProfile proxy = connectProxy(adapter, profile);
1000        assertNotNull(proxy);
1001
1002        ConnectProfileReceiver receiver = getConnectProfileReceiver(device, profile, mask);
1003
1004        int state = proxy.getConnectionState(device);
1005        switch (state) {
1006            case BluetoothProfile.STATE_CONNECTED:
1007            case BluetoothProfile.STATE_CONNECTING:
1008                start = System.currentTimeMillis();
1009                if (profile == BluetoothProfile.A2DP) {
1010                    assertTrue(((BluetoothA2dp)proxy).disconnect(device));
1011                } else if (profile == BluetoothProfile.HEADSET) {
1012                    assertTrue(((BluetoothHeadset)proxy).disconnect(device));
1013                } else if (profile == BluetoothProfile.INPUT_DEVICE) {
1014                    assertTrue(((BluetoothInputDevice)proxy).disconnect(device));
1015                }
1016                break;
1017            case BluetoothProfile.STATE_DISCONNECTED:
1018                removeReceiver(receiver);
1019                return;
1020            case BluetoothProfile.STATE_DISCONNECTING:
1021                mask = 0; // Don't check for received intents since we might have missed them.
1022                break;
1023            default:
1024                removeReceiver(receiver);
1025                fail(String.format("%s invalid state: state=%d", methodName, state));
1026        }
1027
1028        long s = System.currentTimeMillis();
1029        while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
1030            state = proxy.getConnectionState(device);
1031            if (state == BluetoothProfile.STATE_DISCONNECTED
1032                    && (receiver.getFiredFlags() & mask) == mask) {
1033                long finish = receiver.getCompletedTime();
1034                if (start != -1 && finish != -1) {
1035                    writeOutput(String.format("%s completed in %d ms", methodName,
1036                            (finish - start)));
1037                } else {
1038                    writeOutput(String.format("%s completed", methodName));
1039                }
1040                removeReceiver(receiver);
1041                return;
1042            }
1043            sleep(POLL_TIME);
1044        }
1045
1046        int firedFlags = receiver.getFiredFlags();
1047        removeReceiver(receiver);
1048        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
1049                methodName, state, BluetoothProfile.STATE_DISCONNECTED, firedFlags, mask));
1050    }
1051
1052    /**
1053     * Connects the PANU to a remote NAP and checks to make sure that the PANU is connected and that
1054     * the correct actions were broadcast.
1055     *
1056     * @param adapter The BT adapter.
1057     * @param device The remote device.
1058     */
1059    public void connectPan(BluetoothAdapter adapter, BluetoothDevice device) {
1060        connectPanOrIncomingPanConnection(adapter, device, true);
1061    }
1062
1063    /**
1064     * Checks that a remote PANU connects to the local NAP correctly and that the correct actions
1065     * were broadcast.
1066     *
1067     * @param adapter The BT adapter.
1068     * @param device The remote device.
1069     */
1070    public void incomingPanConnection(BluetoothAdapter adapter, BluetoothDevice device) {
1071        connectPanOrIncomingPanConnection(adapter, device, false);
1072    }
1073
1074    /**
1075     * Helper method used by {@link #connectPan(BluetoothAdapter, BluetoothDevice)} and
1076     * {@link #incomingPanConnection(BluetoothAdapter, BluetoothDevice)} to either connect to a
1077     * remote NAP or verify that a remote device connected to the local NAP.
1078     *
1079     * @param adapter The BT adapter.
1080     * @param device The remote device.
1081     * @param connect If the method should initiate the connection (is PANU)
1082     */
1083    private void connectPanOrIncomingPanConnection(BluetoothAdapter adapter, BluetoothDevice device,
1084            boolean connect) {
1085        long start = -1;
1086        int mask, role;
1087        String methodName;
1088
1089        if (connect) {
1090            methodName = String.format("connectPan(device=%s)", device);
1091            mask = (ConnectProfileReceiver.STATE_CONNECTED_FLAG |
1092                    ConnectProfileReceiver.STATE_CONNECTING_FLAG);
1093            role = BluetoothPan.LOCAL_PANU_ROLE;
1094        } else {
1095            methodName = String.format("incomingPanConnection(device=%s)", device);
1096            mask = ConnectProfileReceiver.STATE_CONNECTED_FLAG;
1097            role = BluetoothPan.LOCAL_NAP_ROLE;
1098        }
1099
1100        if (!adapter.isEnabled()) {
1101            fail(String.format("%s bluetooth not enabled", methodName));
1102        }
1103
1104        if (!adapter.getBondedDevices().contains(device)) {
1105            fail(String.format("%s device not paired", methodName));
1106        }
1107
1108        mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
1109        assertNotNull(mPan);
1110        ConnectPanReceiver receiver = getConnectPanReceiver(device, role, mask);
1111
1112        int state = mPan.getConnectionState(device);
1113        switch (state) {
1114            case BluetoothPan.STATE_CONNECTED:
1115                removeReceiver(receiver);
1116                return;
1117            case BluetoothPan.STATE_CONNECTING:
1118                mask = 0; // Don't check for received intents since we might have missed them.
1119                break;
1120            case BluetoothPan.STATE_DISCONNECTED:
1121            case BluetoothPan.STATE_DISCONNECTING:
1122                start = System.currentTimeMillis();
1123                if (role == BluetoothPan.LOCAL_PANU_ROLE) {
1124                    Log.i("BT", "connect to pan");
1125                    assertTrue(mPan.connect(device));
1126                }
1127                break;
1128            default:
1129                removeReceiver(receiver);
1130                fail(String.format("%s invalid state: state=%d", methodName, state));
1131        }
1132
1133        long s = System.currentTimeMillis();
1134        while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
1135            state = mPan.getConnectionState(device);
1136            if (state == BluetoothPan.STATE_CONNECTED
1137                    && (receiver.getFiredFlags() & mask) == mask) {
1138                long finish = receiver.getCompletedTime();
1139                if (start != -1 && finish != -1) {
1140                    writeOutput(String.format("%s completed in %d ms", methodName,
1141                            (finish - start)));
1142                } else {
1143                    writeOutput(String.format("%s completed", methodName));
1144                }
1145                removeReceiver(receiver);
1146                return;
1147            }
1148            sleep(POLL_TIME);
1149        }
1150
1151        int firedFlags = receiver.getFiredFlags();
1152        removeReceiver(receiver);
1153        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
1154                methodName, state, BluetoothPan.STATE_CONNECTED, firedFlags, mask));
1155    }
1156
1157    /**
1158     * Disconnects the PANU from a remote NAP and checks to make sure that the PANU is disconnected
1159     * and that the correct actions were broadcast.
1160     *
1161     * @param adapter The BT adapter.
1162     * @param device The remote device.
1163     */
1164    public void disconnectPan(BluetoothAdapter adapter, BluetoothDevice device) {
1165        disconnectFromRemoteOrVerifyConnectNap(adapter, device, true);
1166    }
1167
1168    /**
1169     * Checks that a remote PANU disconnects from the local NAP correctly and that the correct
1170     * actions were broadcast.
1171     *
1172     * @param adapter The BT adapter.
1173     * @param device The remote device.
1174     */
1175    public void incomingPanDisconnection(BluetoothAdapter adapter, BluetoothDevice device) {
1176        disconnectFromRemoteOrVerifyConnectNap(adapter, device, false);
1177    }
1178
1179    /**
1180     * Helper method used by {@link #disconnectPan(BluetoothAdapter, BluetoothDevice)} and
1181     * {@link #incomingPanDisconnection(BluetoothAdapter, BluetoothDevice)} to either disconnect
1182     * from a remote NAP or verify that a remote device disconnected from the local NAP.
1183     *
1184     * @param adapter The BT adapter.
1185     * @param device The remote device.
1186     * @param disconnect Whether the method should connect or verify.
1187     */
1188    private void disconnectFromRemoteOrVerifyConnectNap(BluetoothAdapter adapter,
1189            BluetoothDevice device, boolean disconnect) {
1190        long start = -1;
1191        int mask, role;
1192        String methodName;
1193
1194        if (disconnect) {
1195            methodName = String.format("disconnectPan(device=%s)", device);
1196            mask = (ConnectProfileReceiver.STATE_DISCONNECTED_FLAG |
1197                    ConnectProfileReceiver.STATE_DISCONNECTING_FLAG);
1198            role = BluetoothPan.LOCAL_PANU_ROLE;
1199        } else {
1200            methodName = String.format("incomingPanDisconnection(device=%s)", device);
1201            mask = ConnectProfileReceiver.STATE_DISCONNECTED_FLAG;
1202            role = BluetoothPan.LOCAL_NAP_ROLE;
1203        }
1204
1205        if (!adapter.isEnabled()) {
1206            fail(String.format("%s bluetooth not enabled", methodName));
1207        }
1208
1209        if (!adapter.getBondedDevices().contains(device)) {
1210            fail(String.format("%s device not paired", methodName));
1211        }
1212
1213        mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
1214        assertNotNull(mPan);
1215        ConnectPanReceiver receiver = getConnectPanReceiver(device, role, mask);
1216
1217        int state = mPan.getConnectionState(device);
1218        switch (state) {
1219            case BluetoothPan.STATE_CONNECTED:
1220            case BluetoothPan.STATE_CONNECTING:
1221                start = System.currentTimeMillis();
1222                if (role == BluetoothPan.LOCAL_PANU_ROLE) {
1223                    assertTrue(mPan.disconnect(device));
1224                }
1225                break;
1226            case BluetoothPan.STATE_DISCONNECTED:
1227                removeReceiver(receiver);
1228                return;
1229            case BluetoothPan.STATE_DISCONNECTING:
1230                mask = 0; // Don't check for received intents since we might have missed them.
1231                break;
1232            default:
1233                removeReceiver(receiver);
1234                fail(String.format("%s invalid state: state=%d", methodName, state));
1235        }
1236
1237        long s = System.currentTimeMillis();
1238        while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
1239            state = mPan.getConnectionState(device);
1240            if (state == BluetoothInputDevice.STATE_DISCONNECTED
1241                    && (receiver.getFiredFlags() & mask) == mask) {
1242                long finish = receiver.getCompletedTime();
1243                if (start != -1 && finish != -1) {
1244                    writeOutput(String.format("%s completed in %d ms", methodName,
1245                            (finish - start)));
1246                } else {
1247                    writeOutput(String.format("%s completed", methodName));
1248                }
1249                removeReceiver(receiver);
1250                return;
1251            }
1252            sleep(POLL_TIME);
1253        }
1254
1255        int firedFlags = receiver.getFiredFlags();
1256        removeReceiver(receiver);
1257        fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
1258                methodName, state, BluetoothInputDevice.STATE_DISCONNECTED, firedFlags, mask));
1259    }
1260
1261    /**
1262     * Opens a SCO channel using {@link android.media.AudioManager#startBluetoothSco()} and checks
1263     * to make sure that the channel is opened and that the correct actions were broadcast.
1264     *
1265     * @param adapter The BT adapter.
1266     * @param device The remote device.
1267     */
1268    public void startSco(BluetoothAdapter adapter, BluetoothDevice device) {
1269        startStopSco(adapter, device, true);
1270    }
1271
1272    /**
1273     * Closes a SCO channel using {@link android.media.AudioManager#stopBluetoothSco()} and checks
1274     *  to make sure that the channel is closed and that the correct actions were broadcast.
1275     *
1276     * @param adapter The BT adapter.
1277     * @param device The remote device.
1278     */
1279    public void stopSco(BluetoothAdapter adapter, BluetoothDevice device) {
1280        startStopSco(adapter, device, false);
1281    }
1282    /**
1283     * Helper method for {@link #startSco(BluetoothAdapter, BluetoothDevice)} and
1284     * {@link #stopSco(BluetoothAdapter, BluetoothDevice)}.
1285     *
1286     * @param adapter The BT adapter.
1287     * @param device The remote device.
1288     * @param isStart Whether the SCO channel should be opened.
1289     */
1290    private void startStopSco(BluetoothAdapter adapter, BluetoothDevice device, boolean isStart) {
1291        long start = -1;
1292        int mask;
1293        String methodName;
1294
1295        if (isStart) {
1296            methodName = String.format("startSco(device=%s)", device);
1297            mask = StartStopScoReceiver.STATE_CONNECTED_FLAG;
1298        } else {
1299            methodName = String.format("stopSco(device=%s)", device);
1300            mask = StartStopScoReceiver.STATE_DISCONNECTED_FLAG;
1301        }
1302
1303        if (!adapter.isEnabled()) {
1304            fail(String.format("%s bluetooth not enabled", methodName));
1305        }
1306
1307        if (!adapter.getBondedDevices().contains(device)) {
1308            fail(String.format("%s device not paired", methodName));
1309        }
1310
1311        AudioManager manager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
1312        assertNotNull(manager);
1313
1314        if (!manager.isBluetoothScoAvailableOffCall()) {
1315            fail(String.format("%s device does not support SCO", methodName));
1316        }
1317
1318        boolean isScoOn = manager.isBluetoothScoOn();
1319        if (isStart == isScoOn) {
1320            return;
1321        }
1322
1323        StartStopScoReceiver receiver = getStartStopScoReceiver(mask);
1324        start = System.currentTimeMillis();
1325        if (isStart) {
1326            manager.startBluetoothSco();
1327        } else {
1328            manager.stopBluetoothSco();
1329        }
1330
1331        long s = System.currentTimeMillis();
1332        while (System.currentTimeMillis() - s < START_STOP_SCO_TIMEOUT) {
1333            isScoOn = manager.isBluetoothScoOn();
1334            if (isStart == isScoOn && (receiver.getFiredFlags() & mask) == mask) {
1335                long finish = receiver.getCompletedTime();
1336                if (start != -1 && finish != -1) {
1337                    writeOutput(String.format("%s completed in %d ms", methodName,
1338                            (finish - start)));
1339                } else {
1340                    writeOutput(String.format("%s completed", methodName));
1341                }
1342                removeReceiver(receiver);
1343                return;
1344            }
1345            sleep(POLL_TIME);
1346        }
1347
1348        int firedFlags = receiver.getFiredFlags();
1349        removeReceiver(receiver);
1350        fail(String.format("%s timeout: on=%b (expected %b), flags=0x%x (expected 0x%x)",
1351                methodName, isScoOn, isStart, firedFlags, mask));
1352    }
1353
1354    /**
1355     * Writes a string to the logcat and a file if a file has been specified in the constructor.
1356     *
1357     * @param s The string to be written.
1358     */
1359    public void writeOutput(String s) {
1360        Log.i(mTag, s);
1361        if (mOutputWriter == null) {
1362            return;
1363        }
1364        try {
1365            mOutputWriter.write(s + "\n");
1366            mOutputWriter.flush();
1367        } catch (IOException e) {
1368            Log.w(mTag, "Could not write to output file", e);
1369        }
1370    }
1371
1372    private void addReceiver(BroadcastReceiver receiver, String[] actions) {
1373        IntentFilter filter = new IntentFilter();
1374        for (String action: actions) {
1375            filter.addAction(action);
1376        }
1377        mContext.registerReceiver(receiver, filter);
1378        mReceivers.add(receiver);
1379    }
1380
1381    private BluetoothReceiver getBluetoothReceiver(int expectedFlags) {
1382        String[] actions = {
1383                BluetoothAdapter.ACTION_DISCOVERY_FINISHED,
1384                BluetoothAdapter.ACTION_DISCOVERY_STARTED,
1385                BluetoothAdapter.ACTION_SCAN_MODE_CHANGED,
1386                BluetoothAdapter.ACTION_STATE_CHANGED};
1387        BluetoothReceiver receiver = new BluetoothReceiver(expectedFlags);
1388        addReceiver(receiver, actions);
1389        return receiver;
1390    }
1391
1392    private PairReceiver getPairReceiver(BluetoothDevice device, int passkey, byte[] pin,
1393            int expectedFlags) {
1394        String[] actions = {
1395                BluetoothDevice.ACTION_PAIRING_REQUEST,
1396                BluetoothDevice.ACTION_BOND_STATE_CHANGED};
1397        PairReceiver receiver = new PairReceiver(device, passkey, pin, expectedFlags);
1398        addReceiver(receiver, actions);
1399        return receiver;
1400    }
1401
1402    private ConnectProfileReceiver getConnectProfileReceiver(BluetoothDevice device, int profile,
1403            int expectedFlags) {
1404        String[] actions = {
1405                BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED,
1406                BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
1407                BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED};
1408        ConnectProfileReceiver receiver = new ConnectProfileReceiver(device, profile,
1409                expectedFlags);
1410        addReceiver(receiver, actions);
1411        return receiver;
1412    }
1413
1414    private ConnectPanReceiver getConnectPanReceiver(BluetoothDevice device, int role,
1415            int expectedFlags) {
1416        String[] actions = {BluetoothPan.ACTION_CONNECTION_STATE_CHANGED};
1417        ConnectPanReceiver receiver = new ConnectPanReceiver(device, role, expectedFlags);
1418        addReceiver(receiver, actions);
1419        return receiver;
1420    }
1421
1422    private StartStopScoReceiver getStartStopScoReceiver(int expectedFlags) {
1423        String[] actions = {AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED};
1424        StartStopScoReceiver receiver = new StartStopScoReceiver(expectedFlags);
1425        addReceiver(receiver, actions);
1426        return receiver;
1427    }
1428
1429    private void removeReceiver(BroadcastReceiver receiver) {
1430        mContext.unregisterReceiver(receiver);
1431        mReceivers.remove(receiver);
1432    }
1433
1434    private BluetoothProfile connectProxy(BluetoothAdapter adapter, int profile) {
1435        switch (profile) {
1436            case BluetoothProfile.A2DP:
1437                if (mA2dp != null) {
1438                    return mA2dp;
1439                }
1440                break;
1441            case BluetoothProfile.HEADSET:
1442                if (mHeadset != null) {
1443                    return mHeadset;
1444                }
1445                break;
1446            case BluetoothProfile.INPUT_DEVICE:
1447                if (mInput != null) {
1448                    return mInput;
1449                }
1450                break;
1451            case BluetoothProfile.PAN:
1452                if (mPan != null) {
1453                    return mPan;
1454                }
1455                break;
1456            default:
1457                return null;
1458        }
1459        adapter.getProfileProxy(mContext, mServiceListener, profile);
1460        long s = System.currentTimeMillis();
1461        switch (profile) {
1462            case BluetoothProfile.A2DP:
1463                while (mA2dp == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
1464                    sleep(POLL_TIME);
1465                }
1466                return mA2dp;
1467            case BluetoothProfile.HEADSET:
1468                while (mHeadset == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
1469                    sleep(POLL_TIME);
1470                }
1471                return mHeadset;
1472            case BluetoothProfile.INPUT_DEVICE:
1473                while (mInput == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
1474                    sleep(POLL_TIME);
1475                }
1476                return mInput;
1477            case BluetoothProfile.PAN:
1478                while (mPan == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
1479                    sleep(POLL_TIME);
1480                }
1481                return mPan;
1482            default:
1483                return null;
1484        }
1485    }
1486
1487    private void sleep(long time) {
1488        try {
1489            Thread.sleep(time);
1490        } catch (InterruptedException e) {
1491        }
1492    }
1493}
1494