AudioManagerAndroid.java revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.media;
6
7import android.bluetooth.BluetoothAdapter;
8import android.bluetooth.BluetoothManager;
9import android.content.BroadcastReceiver;
10import android.content.ContentResolver;
11import android.content.Context;
12import android.content.Intent;
13import android.content.IntentFilter;
14import android.content.pm.PackageManager;
15import android.database.ContentObserver;
16import android.media.AudioFormat;
17import android.media.AudioManager;
18import android.media.AudioRecord;
19import android.media.AudioTrack;
20import android.media.audiofx.AcousticEchoCanceler;
21import android.os.Build;
22import android.os.Handler;
23import android.os.HandlerThread;
24import android.os.Process;
25import android.provider.Settings;
26import android.util.Log;
27
28import org.chromium.base.CalledByNative;
29import org.chromium.base.JNINamespace;
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.List;
34
35@JNINamespace("media")
36class AudioManagerAndroid {
37    private static final String TAG = "AudioManagerAndroid";
38
39    // Set to true to enable debug logs. Avoid in production builds.
40    // NOTE: always check in as false.
41    private static final boolean DEBUG = false;
42
43    /**
44     * NonThreadSafe is a helper class used to help verify that methods of a
45     * class are called from the same thread.
46     * Inspired by class in package com.google.android.apps.chrome.utilities.
47     * Is only utilized when DEBUG is set to true.
48     */
49    private static class NonThreadSafe {
50        private final Long mThreadId;
51
52        public NonThreadSafe() {
53            if (DEBUG) {
54                mThreadId = Thread.currentThread().getId();
55            } else {
56                // Avoids "Unread field" issue reported by findbugs.
57                mThreadId = 0L;
58            }
59        }
60
61        /**
62         * Checks if the method is called on the valid thread.
63         * Assigns the current thread if no thread was assigned.
64         */
65        public boolean calledOnValidThread() {
66            if (DEBUG) {
67                return mThreadId.equals(Thread.currentThread().getId());
68            }
69            return true;
70        }
71    }
72
73    private static boolean runningOnJellyBeanOrHigher() {
74        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
75    }
76
77    private static boolean runningOnJellyBeanMR1OrHigher() {
78        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
79    }
80
81    private static boolean runningOnJellyBeanMR2OrHigher() {
82        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
83    }
84
85    /** Simple container for device information. */
86    private static class AudioDeviceName {
87        private final int mId;
88        private final String mName;
89
90        private AudioDeviceName(int id, String name) {
91            mId = id;
92            mName = name;
93        }
94
95        @CalledByNative("AudioDeviceName")
96        private String id() { return String.valueOf(mId); }
97
98        @CalledByNative("AudioDeviceName")
99        private String name() { return mName; }
100    }
101
102    // List if device models which have been vetted for good quality platform
103    // echo cancellation.
104    // NOTE: only add new devices to this list if manual tests have been
105    // performed where the AEC performance is evaluated using e.g. a WebRTC
106    // audio client such as https://apprtc.appspot.com/?r=<ROOM NAME>.
107    private static final String[] SUPPORTED_AEC_MODELS = new String[] {
108         "GT-I9300",  // Galaxy S3
109         "GT-I9500",  // Galaxy S4
110         "GT-N7105",  // Galaxy Note 2
111         "Nexus 4",   // Nexus 4
112         "Nexus 5",   // Nexus 5
113         "Nexus 7",   // Nexus 7
114         "SM-N9005",  // Galaxy Note 3
115         "SM-T310",   // Galaxy Tab 3 8.0 (WiFi)
116    };
117
118    // Supported audio device types.
119    private static final int DEVICE_DEFAULT = -2;
120    private static final int DEVICE_INVALID = -1;
121    private static final int DEVICE_SPEAKERPHONE = 0;
122    private static final int DEVICE_WIRED_HEADSET = 1;
123    private static final int DEVICE_EARPIECE = 2;
124    private static final int DEVICE_BLUETOOTH_HEADSET = 3;
125    private static final int DEVICE_COUNT = 4;
126
127    // Maps audio device types to string values. This map must be in sync
128    // with the device types above.
129    // TODO(henrika): add support for proper detection of device names and
130    // localize the name strings by using resource strings.
131    // See http://crbug.com/333208 for details.
132    private static final String[] DEVICE_NAMES = new String[] {
133        "Speakerphone",
134        "Wired headset",      // With or without microphone.
135        "Headset earpiece",   // Only available on mobile phones.
136        "Bluetooth headset",  // Requires BLUETOOTH permission.
137    };
138
139    // List of valid device types.
140    private static final Integer[] VALID_DEVICES = new Integer[] {
141        DEVICE_SPEAKERPHONE,
142        DEVICE_WIRED_HEADSET,
143        DEVICE_EARPIECE,
144        DEVICE_BLUETOOTH_HEADSET,
145    };
146
147    // Bluetooth audio SCO states. Example of valid state sequence:
148    // SCO_INVALID -> SCO_TURNING_ON -> SCO_ON -> SCO_TURNING_OFF -> SCO_OFF.
149    private static final int STATE_BLUETOOTH_SCO_INVALID = -1;
150    private static final int STATE_BLUETOOTH_SCO_OFF = 0;
151    private static final int STATE_BLUETOOTH_SCO_ON = 1;
152    private static final int STATE_BLUETOOTH_SCO_TURNING_ON = 2;
153    private static final int STATE_BLUETOOTH_SCO_TURNING_OFF = 3;
154
155    // Use 44.1kHz as the default sampling rate.
156    private static final int DEFAULT_SAMPLING_RATE = 44100;
157    // Randomly picked up frame size which is close to return value on N4.
158    // Return this value when getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
159    // fails.
160    private static final int DEFAULT_FRAME_PER_BUFFER = 256;
161
162    private final AudioManager mAudioManager;
163    private final Context mContext;
164    private final long mNativeAudioManagerAndroid;
165
166    // Enabled during initialization if BLUETOOTH permission is granted.
167    private boolean mHasBluetoothPermission = false;
168
169    private int mSavedAudioMode = AudioManager.MODE_INVALID;
170
171    // Stores the audio states related to Bluetooth SCO audio, where some
172    // states are needed to keep track of intermediate states while the SCO
173    // channel is enabled or disabled (switching state can take a few seconds).
174    private int mBluetoothScoState = STATE_BLUETOOTH_SCO_INVALID;
175
176    private boolean mIsInitialized = false;
177    private boolean mSavedIsSpeakerphoneOn;
178    private boolean mSavedIsMicrophoneMute;
179
180    // Id of the requested audio device. Can only be modified by
181    // call to setDevice().
182    private int mRequestedAudioDevice = DEVICE_INVALID;
183
184    // This class should be created, initialized and closed on the audio thread
185    // in the audio manager. We use |mNonThreadSafe| to ensure that this is
186    // the case. Only active when |DEBUG| is set to true.
187    private final NonThreadSafe mNonThreadSafe = new NonThreadSafe();
188
189    // Lock to protect |mAudioDevices| and |mRequestedAudioDevice| which can
190    // be accessed from the main thread and the audio manager thread.
191    private final Object mLock = new Object();
192
193    // Contains a list of currently available audio devices.
194    private boolean[] mAudioDevices = new boolean[DEVICE_COUNT];
195
196    private final ContentResolver mContentResolver;
197    private ContentObserver mSettingsObserver = null;
198    private HandlerThread mSettingsObserverThread = null;
199    private int mCurrentVolume;
200
201    // Broadcast receiver for wired headset intent broadcasts.
202    private BroadcastReceiver mWiredHeadsetReceiver;
203
204    // Broadcast receiver for Bluetooth headset intent broadcasts.
205    // Utilized to detect changes in Bluetooth headset availability.
206    private BroadcastReceiver mBluetoothHeadsetReceiver;
207
208    // Broadcast receiver for Bluetooth SCO broadcasts.
209    // Utilized to detect if BT SCO streaming is on or off.
210    private BroadcastReceiver mBluetoothScoReceiver;
211
212    /** Construction */
213    @CalledByNative
214    private static AudioManagerAndroid createAudioManagerAndroid(
215            Context context,
216            long nativeAudioManagerAndroid) {
217        return new AudioManagerAndroid(context, nativeAudioManagerAndroid);
218    }
219
220    private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) {
221        mContext = context;
222        mNativeAudioManagerAndroid = nativeAudioManagerAndroid;
223        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
224        mContentResolver = mContext.getContentResolver();
225    }
226
227    /**
228     * Saves the initial speakerphone and microphone state.
229     * Populates the list of available audio devices and registers receivers
230     * for broadcast intents related to wired headset and Bluetooth devices.
231     */
232    @CalledByNative
233    private void init() {
234        checkIfCalledOnValidThread();
235        if (DEBUG) logd("init");
236        if (DEBUG) logDeviceInfo();
237        if (mIsInitialized)
238            return;
239
240        // Initialize audio device list with things we know is always available.
241        mAudioDevices[DEVICE_EARPIECE] = hasEarpiece();
242        mAudioDevices[DEVICE_WIRED_HEADSET] = hasWiredHeadset();
243        mAudioDevices[DEVICE_SPEAKERPHONE] = true;
244
245        // Register receivers for broadcast intents related to Bluetooth device
246        // and Bluetooth SCO notifications. Requires BLUETOOTH permission.
247        registerBluetoothIntentsIfNeeded();
248
249        // Register receiver for broadcast intents related to adding/
250        // removing a wired headset (Intent.ACTION_HEADSET_PLUG).
251        registerForWiredHeadsetIntentBroadcast();
252
253        mIsInitialized = true;
254
255        if (DEBUG) reportUpdate();
256    }
257
258    /**
259     * Unregister all previously registered intent receivers and restore
260     * the stored state (stored in {@link #init()}).
261     */
262    @CalledByNative
263    private void close() {
264        checkIfCalledOnValidThread();
265        if (DEBUG) logd("close");
266        if (!mIsInitialized)
267            return;
268
269        stopObservingVolumeChanges();
270        unregisterForWiredHeadsetIntentBroadcast();
271        unregisterBluetoothIntentsIfNeeded();
272
273        mIsInitialized = false;
274    }
275
276    /**
277     * Saves current audio mode and sets audio mode to MODE_IN_COMMUNICATION
278     * if input parameter is true. Restores saved audio mode if input parameter
279     * is false.
280     */
281    @CalledByNative
282    private void setCommunicationAudioModeOn(boolean on) {
283        if (DEBUG) logd("setCommunicationAudioModeOn(" + on + ")");
284
285        if (on) {
286            if (mSavedAudioMode != AudioManager.MODE_INVALID) {
287                Log.wtf(TAG, "Audio mode has already been set!");
288                return;
289            }
290
291            // Store the current audio mode the first time we try to
292            // switch to communication mode.
293            try {
294                mSavedAudioMode = mAudioManager.getMode();
295            } catch (SecurityException e) {
296                Log.wtf(TAG, "getMode exception: ", e);
297                logDeviceInfo();
298            }
299
300            // Store microphone mute state and speakerphone state so it can
301            // be restored when closing.
302            mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn();
303            mSavedIsMicrophoneMute = mAudioManager.isMicrophoneMute();
304
305            try {
306                mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
307            } catch (SecurityException e) {
308                Log.wtf(TAG, "setMode exception: ", e);
309                logDeviceInfo();
310            }
311
312            // Start observing volume changes to detect when the
313            // voice/communication stream volume is at its lowest level.
314            // It is only possible to pull down the volume slider to about 20%
315            // of the absolute minimum (slider at far left) in communication
316            // mode but we want to be able to mute it completely.
317            startObservingVolumeChanges();
318
319        } else {
320            if (mSavedAudioMode == AudioManager.MODE_INVALID) {
321                Log.wtf(TAG, "Audio mode has not yet been set!");
322                return;
323            }
324
325            stopObservingVolumeChanges();
326
327            // Restore previously stored audio states.
328            setMicrophoneMute(mSavedIsMicrophoneMute);
329            setSpeakerphoneOn(mSavedIsSpeakerphoneOn);
330
331            // Restore the mode that was used before we switched to
332            // communication mode.
333            try {
334                mAudioManager.setMode(mSavedAudioMode);
335            } catch (SecurityException e) {
336                Log.wtf(TAG, "setMode exception: ", e);
337                logDeviceInfo();
338            }
339            mSavedAudioMode = AudioManager.MODE_INVALID;
340        }
341    }
342
343    /**
344     * Activates, i.e., starts routing audio to, the specified audio device.
345     *
346     * @param deviceId Unique device ID (integer converted to string)
347     * representing the selected device. This string is empty if the so-called
348     * default device is requested.
349     */
350    @CalledByNative
351    private boolean setDevice(String deviceId) {
352        if (DEBUG) logd("setDevice: " + deviceId);
353        if (!mIsInitialized)
354            return false;
355        int intDeviceId = deviceId.isEmpty() ?
356            DEVICE_DEFAULT : Integer.parseInt(deviceId);
357
358        if (intDeviceId == DEVICE_DEFAULT) {
359            boolean devices[] = null;
360            synchronized (mLock) {
361                devices = mAudioDevices.clone();
362                mRequestedAudioDevice = DEVICE_DEFAULT;
363            }
364            int defaultDevice = selectDefaultDevice(devices);
365            setAudioDevice(defaultDevice);
366            return true;
367        }
368
369        // A non-default device is specified. Verify that it is valid
370        // device, and if so, start using it.
371        List<Integer> validIds = Arrays.asList(VALID_DEVICES);
372        if (!validIds.contains(intDeviceId) || !mAudioDevices[intDeviceId]) {
373            return false;
374        }
375        synchronized (mLock) {
376            mRequestedAudioDevice = intDeviceId;
377        }
378        setAudioDevice(intDeviceId);
379        return true;
380    }
381
382    /**
383     * @return the current list of available audio devices.
384     * Note that this call does not trigger any update of the list of devices,
385     * it only copies the current state in to the output array.
386     */
387    @CalledByNative
388    private AudioDeviceName[] getAudioInputDeviceNames() {
389        if (DEBUG) logd("getAudioInputDeviceNames");
390        if (!mIsInitialized)
391            return null;
392        boolean devices[] = null;
393        synchronized (mLock) {
394            devices = mAudioDevices.clone();
395        }
396        List<String> list = new ArrayList<String>();
397        AudioDeviceName[] array =
398            new AudioDeviceName[getNumOfAudioDevices(devices)];
399        int i = 0;
400        for (int id = 0; id < DEVICE_COUNT; ++id) {
401            if (devices[id]) {
402                array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]);
403                list.add(DEVICE_NAMES[id]);
404                i++;
405            }
406        }
407        if (DEBUG) logd("getAudioInputDeviceNames: " + list);
408        return array;
409    }
410
411    @CalledByNative
412    private int getNativeOutputSampleRate() {
413        if (runningOnJellyBeanMR1OrHigher()) {
414            String sampleRateString = mAudioManager.getProperty(
415                    AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
416            return (sampleRateString == null ?
417                    DEFAULT_SAMPLING_RATE : Integer.parseInt(sampleRateString));
418        } else {
419            return DEFAULT_SAMPLING_RATE;
420        }
421    }
422
423  /**
424   * Returns the minimum frame size required for audio input.
425   *
426   * @param sampleRate sampling rate
427   * @param channels number of channels
428   */
429    @CalledByNative
430    private static int getMinInputFrameSize(int sampleRate, int channels) {
431        int channelConfig;
432        if (channels == 1) {
433            channelConfig = AudioFormat.CHANNEL_IN_MONO;
434        } else if (channels == 2) {
435            channelConfig = AudioFormat.CHANNEL_IN_STEREO;
436        } else {
437            return -1;
438        }
439        return AudioRecord.getMinBufferSize(
440                sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
441    }
442
443  /**
444   * Returns the minimum frame size required for audio output.
445   *
446   * @param sampleRate sampling rate
447   * @param channels number of channels
448   */
449    @CalledByNative
450    private static int getMinOutputFrameSize(int sampleRate, int channels) {
451        int channelConfig;
452        if (channels == 1) {
453            channelConfig = AudioFormat.CHANNEL_OUT_MONO;
454        } else if (channels == 2) {
455            channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
456        } else {
457            return -1;
458        }
459        return AudioTrack.getMinBufferSize(
460                sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
461    }
462
463    @CalledByNative
464    private boolean isAudioLowLatencySupported() {
465        return mContext.getPackageManager().hasSystemFeature(
466                PackageManager.FEATURE_AUDIO_LOW_LATENCY);
467    }
468
469    @CalledByNative
470    private int getAudioLowLatencyOutputFrameSize() {
471        String framesPerBuffer =
472                mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
473        return (framesPerBuffer == null ?
474                DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer));
475    }
476
477    @CalledByNative
478    private static boolean shouldUseAcousticEchoCanceler() {
479        // AcousticEchoCanceler was added in API level 16 (Jelly Bean).
480        if (!runningOnJellyBeanOrHigher()) {
481            return false;
482        }
483
484        // Verify that this device is among the supported/tested models.
485        List<String> supportedModels = Arrays.asList(SUPPORTED_AEC_MODELS);
486        if (!supportedModels.contains(Build.MODEL)) {
487            return false;
488        }
489        if (DEBUG && AcousticEchoCanceler.isAvailable()) {
490            logd("Approved for use of hardware acoustic echo canceler.");
491        }
492
493        // As a final check, verify that the device supports acoustic echo
494        // cancellation.
495        return AcousticEchoCanceler.isAvailable();
496    }
497
498    /**
499     * Helper method for debugging purposes. Logs message if method is not
500     * called on same thread as this object was created on.
501     */
502    private void checkIfCalledOnValidThread() {
503        if (DEBUG && !mNonThreadSafe.calledOnValidThread()) {
504            Log.wtf(TAG, "Method is not called on valid thread!");
505        }
506    }
507
508    /**
509     * Register for BT intents if we have the BLUETOOTH permission.
510     * Also extends the list of available devices with a BT device if one exists.
511     */
512    private void registerBluetoothIntentsIfNeeded() {
513        // Check if this process has the BLUETOOTH permission or not.
514        mHasBluetoothPermission = hasBluetoothPermission();
515
516        // Add a Bluetooth headset to the list of available devices if a BT
517        // headset is detected and if we have the BLUETOOTH permission.
518        // We must do this initial check using a dedicated method since the
519        // broadcasted intent BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
520        // is not sticky and will only be received if a BT headset is connected
521        // after this method has been called.
522        if (!mHasBluetoothPermission) {
523            return;
524        }
525        mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = hasBluetoothHeadset();
526
527        // Register receivers for broadcast intents related to changes in
528        // Bluetooth headset availability and usage of the SCO channel.
529        registerForBluetoothHeadsetIntentBroadcast();
530        registerForBluetoothScoIntentBroadcast();
531    }
532
533    /** Unregister for BT intents if a registration has been made. */
534    private void unregisterBluetoothIntentsIfNeeded() {
535        if (mHasBluetoothPermission) {
536            mAudioManager.stopBluetoothSco();
537            unregisterForBluetoothHeadsetIntentBroadcast();
538            unregisterForBluetoothScoIntentBroadcast();
539        }
540    }
541
542    /** Sets the speaker phone mode. */
543    private void setSpeakerphoneOn(boolean on) {
544        boolean wasOn = mAudioManager.isSpeakerphoneOn();
545        if (wasOn == on) {
546            return;
547        }
548        mAudioManager.setSpeakerphoneOn(on);
549    }
550
551    /** Sets the microphone mute state. */
552    private void setMicrophoneMute(boolean on) {
553        boolean wasMuted = mAudioManager.isMicrophoneMute();
554        if (wasMuted == on) {
555            return;
556        }
557        mAudioManager.setMicrophoneMute(on);
558    }
559
560    /** Gets  the current microphone mute state. */
561    private boolean isMicrophoneMute() {
562        return mAudioManager.isMicrophoneMute();
563    }
564
565    /** Gets the current earpiece state. */
566    private boolean hasEarpiece() {
567        return mContext.getPackageManager().hasSystemFeature(
568            PackageManager.FEATURE_TELEPHONY);
569    }
570
571    /**
572     * Checks whether a wired headset is connected or not.
573     * This is not a valid indication that audio playback is actually over
574     * the wired headset as audio routing depends on other conditions. We
575     * only use it as an early indicator (during initialization) of an attached
576     * wired headset.
577     */
578    @Deprecated
579    private boolean hasWiredHeadset() {
580        return mAudioManager.isWiredHeadsetOn();
581    }
582
583    /** Checks if the process has BLUETOOTH permission or not. */
584    private boolean hasBluetoothPermission() {
585        boolean hasBluetooth = mContext.checkPermission(
586            android.Manifest.permission.BLUETOOTH,
587            Process.myPid(),
588            Process.myUid()) == PackageManager.PERMISSION_GRANTED;
589        if (DEBUG && !hasBluetooth) {
590            logd("BLUETOOTH permission is missing!");
591        }
592        return hasBluetooth;
593    }
594
595    /**
596     * Gets the current Bluetooth headset state.
597     * android.bluetooth.BluetoothAdapter.getProfileConnectionState() requires
598     * the BLUETOOTH permission.
599     */
600    private boolean hasBluetoothHeadset() {
601        if (!mHasBluetoothPermission) {
602            Log.wtf(TAG, "hasBluetoothHeadset() requires BLUETOOTH permission!");
603            return false;
604        }
605
606        // To get a BluetoothAdapter representing the local Bluetooth adapter,
607        // when running on JELLY_BEAN_MR1 (4.2) and below, call the static
608        // getDefaultAdapter() method; when running on JELLY_BEAN_MR2 (4.3) and
609        // higher, retrieve it through getSystemService(String) with
610        // BLUETOOTH_SERVICE.
611        BluetoothAdapter btAdapter = null;
612        if (runningOnJellyBeanMR2OrHigher()) {
613            // Use BluetoothManager to get the BluetoothAdapter for
614            // Android 4.3 and above.
615            try {
616                BluetoothManager btManager =
617                        (BluetoothManager)mContext.getSystemService(
618                                Context.BLUETOOTH_SERVICE);
619                btAdapter = btManager.getAdapter();
620            } catch (Exception e) {
621                Log.wtf(TAG, "BluetoothManager.getAdapter exception", e);
622                return false;
623            }
624        } else {
625            // Use static method for Android 4.2 and below to get the
626            // BluetoothAdapter.
627            try {
628                btAdapter = BluetoothAdapter.getDefaultAdapter();
629            } catch (Exception e) {
630                Log.wtf(TAG, "BluetoothAdapter.getDefaultAdapter exception", e);
631                return false;
632            }
633        }
634
635        int profileConnectionState;
636        try {
637            profileConnectionState = btAdapter.getProfileConnectionState(
638                android.bluetooth.BluetoothProfile.HEADSET);
639        } catch (Exception e) {
640            Log.wtf(TAG, "BluetoothAdapter.getProfileConnectionState exception", e);
641            profileConnectionState =
642                android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
643        }
644
645        // Ensure that Bluetooth is enabled and that a device which supports the
646        // headset and handsfree profile is connected.
647        // TODO(henrika): it is possible that btAdapter.isEnabled() is
648        // redundant. It might be sufficient to only check the profile state.
649        return btAdapter.isEnabled() && profileConnectionState ==
650            android.bluetooth.BluetoothProfile.STATE_CONNECTED;
651    }
652
653    /**
654     * Registers receiver for the broadcasted intent when a wired headset is
655     * plugged in or unplugged. The received intent will have an extra
656     * 'state' value where 0 means unplugged, and 1 means plugged.
657     */
658    private void registerForWiredHeadsetIntentBroadcast() {
659        IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
660
661        /** Receiver which handles changes in wired headset availability. */
662        mWiredHeadsetReceiver = new BroadcastReceiver() {
663            private static final int STATE_UNPLUGGED = 0;
664            private static final int STATE_PLUGGED = 1;
665            private static final int HAS_NO_MIC = 0;
666            private static final int HAS_MIC = 1;
667
668            @Override
669            public void onReceive(Context context, Intent intent) {
670                int state = intent.getIntExtra("state", STATE_UNPLUGGED);
671                if (DEBUG) {
672                    int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
673                    String name = intent.getStringExtra("name");
674                    logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
675                        ", s=" + state +
676                        ", m=" + microphone +
677                        ", n=" + name +
678                        ", sb=" + isInitialStickyBroadcast());
679                }
680                switch (state) {
681                    case STATE_UNPLUGGED:
682                        synchronized (mLock) {
683                            // Wired headset and earpiece are mutually exclusive.
684                            mAudioDevices[DEVICE_WIRED_HEADSET] = false;
685                            if (hasEarpiece()) {
686                                mAudioDevices[DEVICE_EARPIECE] = true;
687                            }
688                        }
689                        break;
690                    case STATE_PLUGGED:
691                        synchronized (mLock) {
692                            // Wired headset and earpiece are mutually exclusive.
693                            mAudioDevices[DEVICE_WIRED_HEADSET] = true;
694                            mAudioDevices[DEVICE_EARPIECE] = false;
695                        }
696                        break;
697                    default:
698                        loge("Invalid state!");
699                        break;
700                }
701
702                // Update the existing device selection, but only if a specific
703                // device has already been selected explicitly.
704                if (deviceHasBeenRequested()) {
705                    updateDeviceActivation();
706                } else if (DEBUG) {
707                    reportUpdate();
708                }
709            }
710        };
711
712        // Note: the intent we register for here is sticky, so it'll tell us
713        // immediately what the last action was (plugged or unplugged).
714        // It will enable us to set the speakerphone correctly.
715        mContext.registerReceiver(mWiredHeadsetReceiver, filter);
716    }
717
718    /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
719    private void unregisterForWiredHeadsetIntentBroadcast() {
720        mContext.unregisterReceiver(mWiredHeadsetReceiver);
721        mWiredHeadsetReceiver = null;
722    }
723
724    /**
725     * Registers receiver for the broadcasted intent related to BT headset
726     * availability or a change in connection state of the local Bluetooth
727     * adapter. Example: triggers when the BT device is turned on or off.
728     * BLUETOOTH permission is required to receive this one.
729     */
730    private void registerForBluetoothHeadsetIntentBroadcast() {
731        IntentFilter filter = new IntentFilter(
732            android.bluetooth.BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
733
734        /** Receiver which handles changes in BT headset availability. */
735        mBluetoothHeadsetReceiver = new BroadcastReceiver() {
736            @Override
737            public void onReceive(Context context, Intent intent) {
738                // A change in connection state of the Headset profile has
739                // been detected, e.g. BT headset has been connected or
740                // disconnected. This broadcast is *not* sticky.
741                int profileState = intent.getIntExtra(
742                    android.bluetooth.BluetoothHeadset.EXTRA_STATE,
743                    android.bluetooth.BluetoothHeadset.STATE_DISCONNECTED);
744                if (DEBUG) {
745                    logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
746                        ", s=" + profileState +
747                        ", sb=" + isInitialStickyBroadcast());
748                }
749
750                switch (profileState) {
751                    case android.bluetooth.BluetoothProfile.STATE_DISCONNECTED:
752                        // We do not have to explicitly call stopBluetoothSco()
753                        // since BT SCO will be disconnected automatically when
754                        // the BT headset is disabled.
755                        synchronized (mLock) {
756                            // Remove the BT device from the list of devices.
757                            mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = false;
758                        }
759                        break;
760                    case android.bluetooth.BluetoothProfile.STATE_CONNECTED:
761                        synchronized (mLock) {
762                            // Add the BT device to the list of devices.
763                            mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true;
764                        }
765                        break;
766                    case android.bluetooth.BluetoothProfile.STATE_CONNECTING:
767                        // Bluetooth service is switching from off to on.
768                        break;
769                    case android.bluetooth.BluetoothProfile.STATE_DISCONNECTING:
770                        // Bluetooth service is switching from on to off.
771                        break;
772                    default:
773                        loge("Invalid state!");
774                        break;
775                }
776
777                // Update the existing device selection, but only if a specific
778                // device has already been selected explicitly.
779                if (deviceHasBeenRequested()) {
780                    updateDeviceActivation();
781                } else if (DEBUG) {
782                    reportUpdate();
783                }
784           }
785        };
786
787        mContext.registerReceiver(mBluetoothHeadsetReceiver, filter);
788    }
789
790    private void unregisterForBluetoothHeadsetIntentBroadcast() {
791        mContext.unregisterReceiver(mBluetoothHeadsetReceiver);
792        mBluetoothHeadsetReceiver = null;
793    }
794
795    /**
796     * Registers receiver for the broadcasted intent related the existence
797     * of a BT SCO channel. Indicates if BT SCO streaming is on or off.
798     */
799    private void registerForBluetoothScoIntentBroadcast() {
800        IntentFilter filter = new IntentFilter(
801            AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
802
803        /** BroadcastReceiver implementation which handles changes in BT SCO. */
804        mBluetoothScoReceiver = new BroadcastReceiver() {
805            @Override
806            public void onReceive(Context context, Intent intent) {
807                int state = intent.getIntExtra(
808                    AudioManager.EXTRA_SCO_AUDIO_STATE,
809                    AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
810                if (DEBUG) {
811                    logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
812                        ", s=" + state +
813                        ", sb=" + isInitialStickyBroadcast());
814                }
815
816                switch (state) {
817                    case AudioManager.SCO_AUDIO_STATE_CONNECTED:
818                        mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
819                        break;
820                    case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
821                        mBluetoothScoState = STATE_BLUETOOTH_SCO_OFF;
822                        break;
823                    case AudioManager.SCO_AUDIO_STATE_CONNECTING:
824                        // do nothing
825                        break;
826                    default:
827                        loge("Invalid state!");
828                }
829                if (DEBUG) {
830                    reportUpdate();
831                }
832           }
833        };
834
835        mContext.registerReceiver(mBluetoothScoReceiver, filter);
836    }
837
838    private void unregisterForBluetoothScoIntentBroadcast() {
839        mContext.unregisterReceiver(mBluetoothScoReceiver);
840        mBluetoothScoReceiver = null;
841    }
842
843    /** Enables BT audio using the SCO audio channel. */
844    private void startBluetoothSco() {
845        if (!mHasBluetoothPermission) {
846            return;
847        }
848        if (mBluetoothScoState == STATE_BLUETOOTH_SCO_ON ||
849            mBluetoothScoState == STATE_BLUETOOTH_SCO_TURNING_ON) {
850            // Unable to turn on BT in this state.
851            return;
852        }
853
854        // Check if audio is already routed to BT SCO; if so, just update
855        // states but don't try to enable it again.
856        if (mAudioManager.isBluetoothScoOn()) {
857            mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
858            return;
859        }
860
861        if (DEBUG) logd("startBluetoothSco: turning BT SCO on...");
862        mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_ON;
863        mAudioManager.startBluetoothSco();
864    }
865
866    /** Disables BT audio using the SCO audio channel. */
867    private void stopBluetoothSco() {
868        if (!mHasBluetoothPermission) {
869            return;
870        }
871        if (mBluetoothScoState != STATE_BLUETOOTH_SCO_ON &&
872            mBluetoothScoState != STATE_BLUETOOTH_SCO_TURNING_ON) {
873            // No need to turn off BT in this state.
874            return;
875        }
876        if (!mAudioManager.isBluetoothScoOn()) {
877            // TODO(henrika): can we do anything else than logging here?
878            loge("Unable to stop BT SCO since it is already disabled!");
879            return;
880        }
881
882        if (DEBUG) logd("stopBluetoothSco: turning BT SCO off...");
883        mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_OFF;
884        mAudioManager.stopBluetoothSco();
885    }
886
887    /**
888     * Changes selection of the currently active audio device.
889     *
890     * @param device Specifies the selected audio device.
891     */
892    private void setAudioDevice(int device) {
893        if (DEBUG) logd("setAudioDevice(device=" + device + ")");
894
895        // Ensure that the Bluetooth SCO audio channel is always disabled
896        // unless the BT headset device is selected.
897        if (device == DEVICE_BLUETOOTH_HEADSET) {
898            startBluetoothSco();
899        } else {
900            stopBluetoothSco();
901        }
902
903        switch (device) {
904            case DEVICE_BLUETOOTH_HEADSET:
905                break;
906            case DEVICE_SPEAKERPHONE:
907                setSpeakerphoneOn(true);
908                break;
909            case DEVICE_WIRED_HEADSET:
910                setSpeakerphoneOn(false);
911                break;
912            case DEVICE_EARPIECE:
913                setSpeakerphoneOn(false);
914                break;
915            default:
916                loge("Invalid audio device selection!");
917                break;
918        }
919        reportUpdate();
920    }
921
922    /**
923     * Use a special selection scheme if the default device is selected.
924     * The "most unique" device will be selected; Wired headset first,
925     * then Bluetooth and last the speaker phone.
926     */
927    private static int selectDefaultDevice(boolean[] devices) {
928        if (devices[DEVICE_WIRED_HEADSET]) {
929            return DEVICE_WIRED_HEADSET;
930        } else if (devices[DEVICE_BLUETOOTH_HEADSET]) {
931            // TODO(henrika): possibly need improvements here if we are
932            // in a state where Bluetooth is turning off.
933            return DEVICE_BLUETOOTH_HEADSET;
934        }
935        return DEVICE_SPEAKERPHONE;
936    }
937
938    /** Returns true if setDevice() has been called with a valid device id. */
939    private boolean deviceHasBeenRequested() {
940        synchronized (mLock) {
941            return (mRequestedAudioDevice != DEVICE_INVALID);
942        }
943    }
944
945    /**
946     * Updates the active device given the current list of devices and
947     * information about if a specific device has been selected or if
948     * the default device is selected.
949     */
950    private void updateDeviceActivation() {
951        boolean devices[] = null;
952        int requested = DEVICE_INVALID;
953        synchronized (mLock) {
954            requested = mRequestedAudioDevice;
955            devices = mAudioDevices.clone();
956        }
957        if (requested == DEVICE_INVALID) {
958            loge("Unable to activate device since no device is selected!");
959            return;
960        }
961
962        // Update default device if it has been selected explicitly, or
963        // the selected device has been removed from the list.
964        if (requested == DEVICE_DEFAULT || !devices[requested]) {
965            // Get default device given current list and activate the device.
966            int defaultDevice = selectDefaultDevice(devices);
967            setAudioDevice(defaultDevice);
968        } else {
969            // Activate the selected device since we know that it exists in
970            // the list.
971            setAudioDevice(requested);
972        }
973    }
974
975    /** Returns number of available devices */
976    private static int getNumOfAudioDevices(boolean[] devices) {
977        int count = 0;
978        for (int i = 0; i < DEVICE_COUNT; ++i) {
979            if (devices[i])
980                ++count;
981        }
982        return count;
983    }
984
985    /**
986     * For now, just log the state change but the idea is that we should
987     * notify a registered state change listener (if any) that there has
988     * been a change in the state.
989     * TODO(henrika): add support for state change listener.
990     */
991    private void reportUpdate() {
992        synchronized (mLock) {
993            List<String> devices = new ArrayList<String>();
994            for (int i = 0; i < DEVICE_COUNT; ++i) {
995                if (mAudioDevices[i])
996                    devices.add(DEVICE_NAMES[i]);
997            }
998            if (DEBUG) {
999                logd("reportUpdate: requested=" + mRequestedAudioDevice +
1000                    ", btSco=" + mBluetoothScoState +
1001                    ", devices=" + devices);
1002            }
1003        }
1004    }
1005
1006    /** Information about the current build, taken from system properties. */
1007    private void logDeviceInfo() {
1008        logd("Android SDK: " + Build.VERSION.SDK_INT + ", " +
1009            "Release: " + Build.VERSION.RELEASE + ", " +
1010            "Brand: " + Build.BRAND + ", " +
1011            "CPU_ABI: " + Build.CPU_ABI + ", " +
1012            "Device: " + Build.DEVICE + ", " +
1013            "Id: " + Build.ID + ", " +
1014            "Hardware: " + Build.HARDWARE + ", " +
1015            "Manufacturer: " + Build.MANUFACTURER + ", " +
1016            "Model: " + Build.MODEL + ", " +
1017            "Product: " + Build.PRODUCT);
1018    }
1019
1020    /** Trivial helper method for debug logging */
1021    private static void logd(String msg) {
1022        Log.d(TAG, msg);
1023    }
1024
1025    /** Trivial helper method for error logging */
1026    private static void loge(String msg) {
1027        Log.e(TAG, msg);
1028    }
1029
1030    /** Start thread which observes volume changes on the voice stream. */
1031    private void startObservingVolumeChanges() {
1032        if (DEBUG) logd("startObservingVolumeChanges");
1033        if (mSettingsObserverThread != null)
1034            return;
1035        mSettingsObserverThread = new HandlerThread("SettingsObserver");
1036        mSettingsObserverThread.start();
1037
1038        mSettingsObserver = new ContentObserver(
1039            new Handler(mSettingsObserverThread.getLooper())) {
1040
1041                @Override
1042                public void onChange(boolean selfChange) {
1043                    if (DEBUG) logd("SettingsObserver.onChange: " + selfChange);
1044                    super.onChange(selfChange);
1045
1046                    // Ensure that the observer is activated during communication mode.
1047                    if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) {
1048                        Log.wtf(TAG, "Only enable SettingsObserver in COMM mode!");
1049                        return;
1050                    }
1051
1052                    // Get stream volume for the voice stream and deliver callback if
1053                    // the volume index is zero. It is not possible to move the volume
1054                    // slider all the way down in communication mode but the callback
1055                    // implementation can ensure that the volume is completely muted.
1056                    int volume = mAudioManager.getStreamVolume(
1057                        AudioManager.STREAM_VOICE_CALL);
1058                    if (DEBUG) logd("nativeSetMute: " + (volume == 0));
1059                    nativeSetMute(mNativeAudioManagerAndroid, (volume == 0));
1060                }
1061        };
1062
1063        mContentResolver.registerContentObserver(
1064            Settings.System.CONTENT_URI, true, mSettingsObserver);
1065    }
1066
1067    /** Quit observer thread and stop listening for volume changes. */
1068    private void stopObservingVolumeChanges() {
1069        if (DEBUG) logd("stopObservingVolumeChanges");
1070        if (mSettingsObserverThread == null)
1071            return;
1072
1073        mContentResolver.unregisterContentObserver(mSettingsObserver);
1074        mSettingsObserver = null;
1075
1076        mSettingsObserverThread.quit();
1077        try {
1078            mSettingsObserverThread.join();
1079        } catch (InterruptedException e) {
1080            Log.wtf(TAG, "Thread.join() exception: ", e);
1081        }
1082        mSettingsObserverThread = null;
1083    }
1084
1085    private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted);
1086}
1087