Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / media / base / android / java / src / org / chromium / media / AudioManagerAndroid.java
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
5 package org.chromium.media;
6
7 import android.bluetooth.BluetoothAdapter;
8 import android.bluetooth.BluetoothManager;
9 import android.content.BroadcastReceiver;
10 import android.content.ContentResolver;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.content.IntentFilter;
14 import android.content.pm.PackageManager;
15 import android.database.ContentObserver;
16 import android.media.AudioFormat;
17 import android.media.AudioManager;
18 import android.media.AudioRecord;
19 import android.media.AudioTrack;
20 import android.media.audiofx.AcousticEchoCanceler;
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Process;
25 import android.provider.Settings;
26 import android.util.Log;
27
28 import org.chromium.base.CalledByNative;
29 import org.chromium.base.JNINamespace;
30
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.List;
34
35 @JNINamespace("media")
36 class 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 }