8005ddcb58caafec7e5a1f6f749e00c95e30ebf5
[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         int profileConnectionState;
668         profileConnectionState = btAdapter.getProfileConnectionState(
669                 android.bluetooth.BluetoothProfile.HEADSET);
670
671         // Ensure that Bluetooth is enabled and that a device which supports the
672         // headset and handsfree profile is connected.
673         // TODO(henrika): it is possible that btAdapter.isEnabled() is
674         // redundant. It might be sufficient to only check the profile state.
675         return btAdapter.isEnabled() && profileConnectionState ==
676             android.bluetooth.BluetoothProfile.STATE_CONNECTED;
677     }
678
679     /**
680      * Registers receiver for the broadcasted intent when a wired headset is
681      * plugged in or unplugged. The received intent will have an extra
682      * 'state' value where 0 means unplugged, and 1 means plugged.
683      */
684     private void registerForWiredHeadsetIntentBroadcast() {
685         IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
686
687         /** Receiver which handles changes in wired headset availability. */
688         mWiredHeadsetReceiver = new BroadcastReceiver() {
689             private static final int STATE_UNPLUGGED = 0;
690             private static final int STATE_PLUGGED = 1;
691             private static final int HAS_NO_MIC = 0;
692             private static final int HAS_MIC = 1;
693
694             @Override
695             public void onReceive(Context context, Intent intent) {
696                 int state = intent.getIntExtra("state", STATE_UNPLUGGED);
697                 if (DEBUG) {
698                     int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
699                     String name = intent.getStringExtra("name");
700                     logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
701                         ", s=" + state +
702                         ", m=" + microphone +
703                         ", n=" + name +
704                         ", sb=" + isInitialStickyBroadcast());
705                 }
706                 switch (state) {
707                     case STATE_UNPLUGGED:
708                         synchronized (mLock) {
709                             // Wired headset and earpiece are mutually exclusive.
710                             mAudioDevices[DEVICE_WIRED_HEADSET] = false;
711                             if (hasEarpiece()) {
712                                 mAudioDevices[DEVICE_EARPIECE] = true;
713                             }
714                         }
715                         break;
716                     case STATE_PLUGGED:
717                         synchronized (mLock) {
718                             // Wired headset and earpiece are mutually exclusive.
719                             mAudioDevices[DEVICE_WIRED_HEADSET] = true;
720                             mAudioDevices[DEVICE_EARPIECE] = false;
721                         }
722                         break;
723                     default:
724                         loge("Invalid state");
725                         break;
726                 }
727
728                 // Update the existing device selection, but only if a specific
729                 // device has already been selected explicitly.
730                 if (deviceHasBeenRequested()) {
731                     updateDeviceActivation();
732                 } else if (DEBUG) {
733                     reportUpdate();
734                 }
735             }
736         };
737
738         // Note: the intent we register for here is sticky, so it'll tell us
739         // immediately what the last action was (plugged or unplugged).
740         // It will enable us to set the speakerphone correctly.
741         mContext.registerReceiver(mWiredHeadsetReceiver, filter);
742     }
743
744     /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
745     private void unregisterForWiredHeadsetIntentBroadcast() {
746         mContext.unregisterReceiver(mWiredHeadsetReceiver);
747         mWiredHeadsetReceiver = null;
748     }
749
750     /**
751      * Registers receiver for the broadcasted intent related to BT headset
752      * availability or a change in connection state of the local Bluetooth
753      * adapter. Example: triggers when the BT device is turned on or off.
754      * BLUETOOTH permission is required to receive this one.
755      */
756     private void registerForBluetoothHeadsetIntentBroadcast() {
757         IntentFilter filter = new IntentFilter(
758             android.bluetooth.BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
759
760         /** Receiver which handles changes in BT headset availability. */
761         mBluetoothHeadsetReceiver = new BroadcastReceiver() {
762             @Override
763             public void onReceive(Context context, Intent intent) {
764                 // A change in connection state of the Headset profile has
765                 // been detected, e.g. BT headset has been connected or
766                 // disconnected. This broadcast is *not* sticky.
767                 int profileState = intent.getIntExtra(
768                     android.bluetooth.BluetoothHeadset.EXTRA_STATE,
769                     android.bluetooth.BluetoothHeadset.STATE_DISCONNECTED);
770                 if (DEBUG) {
771                     logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
772                         ", s=" + profileState +
773                         ", sb=" + isInitialStickyBroadcast());
774                 }
775
776                 switch (profileState) {
777                     case android.bluetooth.BluetoothProfile.STATE_DISCONNECTED:
778                         // We do not have to explicitly call stopBluetoothSco()
779                         // since BT SCO will be disconnected automatically when
780                         // the BT headset is disabled.
781                         synchronized (mLock) {
782                             // Remove the BT device from the list of devices.
783                             mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = false;
784                         }
785                         break;
786                     case android.bluetooth.BluetoothProfile.STATE_CONNECTED:
787                         synchronized (mLock) {
788                             // Add the BT device to the list of devices.
789                             mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true;
790                         }
791                         break;
792                     case android.bluetooth.BluetoothProfile.STATE_CONNECTING:
793                         // Bluetooth service is switching from off to on.
794                         break;
795                     case android.bluetooth.BluetoothProfile.STATE_DISCONNECTING:
796                         // Bluetooth service is switching from on to off.
797                         break;
798                     default:
799                         loge("Invalid state");
800                         break;
801                 }
802
803                 // Update the existing device selection, but only if a specific
804                 // device has already been selected explicitly.
805                 if (deviceHasBeenRequested()) {
806                     updateDeviceActivation();
807                 } else if (DEBUG) {
808                     reportUpdate();
809                 }
810            }
811         };
812
813         mContext.registerReceiver(mBluetoothHeadsetReceiver, filter);
814     }
815
816     private void unregisterForBluetoothHeadsetIntentBroadcast() {
817         mContext.unregisterReceiver(mBluetoothHeadsetReceiver);
818         mBluetoothHeadsetReceiver = null;
819     }
820
821     /**
822      * Registers receiver for the broadcasted intent related the existence
823      * of a BT SCO channel. Indicates if BT SCO streaming is on or off.
824      */
825     private void registerForBluetoothScoIntentBroadcast() {
826         IntentFilter filter = new IntentFilter(
827             AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
828
829         /** BroadcastReceiver implementation which handles changes in BT SCO. */
830         mBluetoothScoReceiver = new BroadcastReceiver() {
831             @Override
832             public void onReceive(Context context, Intent intent) {
833                 int state = intent.getIntExtra(
834                     AudioManager.EXTRA_SCO_AUDIO_STATE,
835                     AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
836                 if (DEBUG) {
837                     logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
838                         ", s=" + state +
839                         ", sb=" + isInitialStickyBroadcast());
840                 }
841
842                 switch (state) {
843                     case AudioManager.SCO_AUDIO_STATE_CONNECTED:
844                         mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
845                         break;
846                     case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
847                         mBluetoothScoState = STATE_BLUETOOTH_SCO_OFF;
848                         break;
849                     case AudioManager.SCO_AUDIO_STATE_CONNECTING:
850                         // do nothing
851                         break;
852                     default:
853                         loge("Invalid state");
854                 }
855                 if (DEBUG) {
856                     reportUpdate();
857                 }
858            }
859         };
860
861         mContext.registerReceiver(mBluetoothScoReceiver, filter);
862     }
863
864     private void unregisterForBluetoothScoIntentBroadcast() {
865         mContext.unregisterReceiver(mBluetoothScoReceiver);
866         mBluetoothScoReceiver = null;
867     }
868
869     /** Enables BT audio using the SCO audio channel. */
870     private void startBluetoothSco() {
871         if (!mHasBluetoothPermission) {
872             return;
873         }
874         if (mBluetoothScoState == STATE_BLUETOOTH_SCO_ON ||
875             mBluetoothScoState == STATE_BLUETOOTH_SCO_TURNING_ON) {
876             // Unable to turn on BT in this state.
877             return;
878         }
879
880         // Check if audio is already routed to BT SCO; if so, just update
881         // states but don't try to enable it again.
882         if (mAudioManager.isBluetoothScoOn()) {
883             mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
884             return;
885         }
886
887         if (DEBUG) logd("startBluetoothSco: turning BT SCO on...");
888         mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_ON;
889         mAudioManager.startBluetoothSco();
890     }
891
892     /** Disables BT audio using the SCO audio channel. */
893     private void stopBluetoothSco() {
894         if (!mHasBluetoothPermission) {
895             return;
896         }
897         if (mBluetoothScoState != STATE_BLUETOOTH_SCO_ON &&
898             mBluetoothScoState != STATE_BLUETOOTH_SCO_TURNING_ON) {
899             // No need to turn off BT in this state.
900             return;
901         }
902         if (!mAudioManager.isBluetoothScoOn()) {
903             // TODO(henrika): can we do anything else than logging here?
904             loge("Unable to stop BT SCO since it is already disabled");
905             return;
906         }
907
908         if (DEBUG) logd("stopBluetoothSco: turning BT SCO off...");
909         mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_OFF;
910         mAudioManager.stopBluetoothSco();
911     }
912
913     /**
914      * Changes selection of the currently active audio device.
915      *
916      * @param device Specifies the selected audio device.
917      */
918     private void setAudioDevice(int device) {
919         if (DEBUG) logd("setAudioDevice(device=" + device + ")");
920
921         // Ensure that the Bluetooth SCO audio channel is always disabled
922         // unless the BT headset device is selected.
923         if (device == DEVICE_BLUETOOTH_HEADSET) {
924             startBluetoothSco();
925         } else {
926             stopBluetoothSco();
927         }
928
929         switch (device) {
930             case DEVICE_BLUETOOTH_HEADSET:
931                 break;
932             case DEVICE_SPEAKERPHONE:
933                 setSpeakerphoneOn(true);
934                 break;
935             case DEVICE_WIRED_HEADSET:
936                 setSpeakerphoneOn(false);
937                 break;
938             case DEVICE_EARPIECE:
939                 setSpeakerphoneOn(false);
940                 break;
941             default:
942                 loge("Invalid audio device selection");
943                 break;
944         }
945         reportUpdate();
946     }
947
948     /**
949      * Use a special selection scheme if the default device is selected.
950      * The "most unique" device will be selected; Wired headset first,
951      * then Bluetooth and last the speaker phone.
952      */
953     private static int selectDefaultDevice(boolean[] devices) {
954         if (devices[DEVICE_WIRED_HEADSET]) {
955             return DEVICE_WIRED_HEADSET;
956         } else if (devices[DEVICE_BLUETOOTH_HEADSET]) {
957             // TODO(henrika): possibly need improvements here if we are
958             // in a state where Bluetooth is turning off.
959             return DEVICE_BLUETOOTH_HEADSET;
960         }
961         return DEVICE_SPEAKERPHONE;
962     }
963
964     /** Returns true if setDevice() has been called with a valid device id. */
965     private boolean deviceHasBeenRequested() {
966         synchronized (mLock) {
967             return (mRequestedAudioDevice != DEVICE_INVALID);
968         }
969     }
970
971     /**
972      * Updates the active device given the current list of devices and
973      * information about if a specific device has been selected or if
974      * the default device is selected.
975      */
976     private void updateDeviceActivation() {
977         boolean devices[] = null;
978         int requested = DEVICE_INVALID;
979         synchronized (mLock) {
980             requested = mRequestedAudioDevice;
981             devices = mAudioDevices.clone();
982         }
983         if (requested == DEVICE_INVALID) {
984             loge("Unable to activate device since no device is selected");
985             return;
986         }
987
988         // Update default device if it has been selected explicitly, or
989         // the selected device has been removed from the list.
990         if (requested == DEVICE_DEFAULT || !devices[requested]) {
991             // Get default device given current list and activate the device.
992             int defaultDevice = selectDefaultDevice(devices);
993             setAudioDevice(defaultDevice);
994         } else {
995             // Activate the selected device since we know that it exists in
996             // the list.
997             setAudioDevice(requested);
998         }
999     }
1000
1001     /** Returns number of available devices */
1002     private static int getNumOfAudioDevices(boolean[] devices) {
1003         int count = 0;
1004         for (int i = 0; i < DEVICE_COUNT; ++i) {
1005             if (devices[i])
1006                 ++count;
1007         }
1008         return count;
1009     }
1010
1011     /**
1012      * For now, just log the state change but the idea is that we should
1013      * notify a registered state change listener (if any) that there has
1014      * been a change in the state.
1015      * TODO(henrika): add support for state change listener.
1016      */
1017     private void reportUpdate() {
1018         synchronized (mLock) {
1019             List<String> devices = new ArrayList<String>();
1020             for (int i = 0; i < DEVICE_COUNT; ++i) {
1021                 if (mAudioDevices[i])
1022                     devices.add(DEVICE_NAMES[i]);
1023             }
1024             if (DEBUG) {
1025                 logd("reportUpdate: requested=" + mRequestedAudioDevice +
1026                     ", btSco=" + mBluetoothScoState +
1027                     ", devices=" + devices);
1028             }
1029         }
1030     }
1031
1032     /** Information about the current build, taken from system properties. */
1033     private void logDeviceInfo() {
1034         logd("Android SDK: " + Build.VERSION.SDK_INT + ", " +
1035             "Release: " + Build.VERSION.RELEASE + ", " +
1036             "Brand: " + Build.BRAND + ", " +
1037             "CPU_ABI: " + Build.CPU_ABI + ", " +
1038             "Device: " + Build.DEVICE + ", " +
1039             "Id: " + Build.ID + ", " +
1040             "Hardware: " + Build.HARDWARE + ", " +
1041             "Manufacturer: " + Build.MANUFACTURER + ", " +
1042             "Model: " + Build.MODEL + ", " +
1043             "Product: " + Build.PRODUCT);
1044     }
1045
1046     /** Trivial helper method for debug logging */
1047     private static void logd(String msg) {
1048         Log.d(TAG, msg);
1049     }
1050
1051     /** Trivial helper method for error logging */
1052     private static void loge(String msg) {
1053         Log.e(TAG, msg);
1054     }
1055
1056     /** Start thread which observes volume changes on the voice stream. */
1057     private void startObservingVolumeChanges() {
1058         if (DEBUG) logd("startObservingVolumeChanges");
1059         if (mSettingsObserverThread != null)
1060             return;
1061         mSettingsObserverThread = new HandlerThread("SettingsObserver");
1062         mSettingsObserverThread.start();
1063
1064         mSettingsObserver = new ContentObserver(
1065             new Handler(mSettingsObserverThread.getLooper())) {
1066
1067                 @Override
1068                 public void onChange(boolean selfChange) {
1069                     if (DEBUG) logd("SettingsObserver.onChange: " + selfChange);
1070                     super.onChange(selfChange);
1071
1072                     // Ensure that the observer is activated during communication mode.
1073                     if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) {
1074                         throw new IllegalStateException(
1075                                 "Only enable SettingsObserver in COMM mode");
1076                     }
1077
1078                     // Get stream volume for the voice stream and deliver callback if
1079                     // the volume index is zero. It is not possible to move the volume
1080                     // slider all the way down in communication mode but the callback
1081                     // implementation can ensure that the volume is completely muted.
1082                     int volume = mAudioManager.getStreamVolume(
1083                         AudioManager.STREAM_VOICE_CALL);
1084                     if (DEBUG) logd("nativeSetMute: " + (volume == 0));
1085                     nativeSetMute(mNativeAudioManagerAndroid, (volume == 0));
1086                 }
1087         };
1088
1089         mContentResolver.registerContentObserver(
1090             Settings.System.CONTENT_URI, true, mSettingsObserver);
1091     }
1092
1093     /** Quit observer thread and stop listening for volume changes. */
1094     private void stopObservingVolumeChanges() {
1095         if (DEBUG) logd("stopObservingVolumeChanges");
1096         if (mSettingsObserverThread == null)
1097             return;
1098
1099         mContentResolver.unregisterContentObserver(mSettingsObserver);
1100         mSettingsObserver = null;
1101
1102         mSettingsObserverThread.quit();
1103         try {
1104             mSettingsObserverThread.join();
1105         } catch (InterruptedException e) {
1106             Log.e(TAG, "Thread.join() exception: ", e);
1107         }
1108         mSettingsObserverThread = null;
1109     }
1110
1111     private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted);
1112 }