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