From 113e50c074fcc397de07ce3656e58d66f4bf527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Mon, 23 Jan 2017 16:59:34 -0600 Subject: [PATCH 1/5] [RN] Bump Andoroid minimum and target SDK versions Use a minimum SDK version of 19, that is Anroid 4.4 (KitKat) and a target SDK of 23, that is, Android 6.0 (Marshmallow). --- android/app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9b40fcb94..8fdbc4a8b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,8 +13,8 @@ + android:minSdkVersion="19" + android:targetSdkVersion="23" /> Date: Fri, 20 Jan 2017 15:24:43 -0600 Subject: [PATCH 2/5] [RN] Implement AudioMode module on Android This module chooses the most appropriate audio default based on the specified mode. --- android/app/src/main/AndroidManifest.xml | 5 +- .../java/org/jitsi/meet/MainApplication.java | 3 +- .../jitsi/meet/audiomode/AudioModeModule.java | 305 ++++++++++++++++++ .../meet/audiomode/AudioModePackage.java | 41 +++ .../audiomode/BluetoothHeadsetMonitor.java | 192 +++++++++++ 5 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java create mode 100644 android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java create mode 100644 android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8fdbc4a8b..9c7b2fbdb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,9 +3,12 @@ android:versionCode="1" android:versionName="1.0"> - + + + + diff --git a/android/app/src/main/java/org/jitsi/meet/MainApplication.java b/android/app/src/main/java/org/jitsi/meet/MainApplication.java index f8d79c9df..437e0743a 100644 --- a/android/app/src/main/java/org/jitsi/meet/MainApplication.java +++ b/android/app/src/main/java/org/jitsi/meet/MainApplication.java @@ -29,7 +29,8 @@ public class MainApplication extends Application implements ReactApplication { new com.corbt.keepawake.KCKeepAwakePackage(), new com.facebook.react.shell.MainReactPackage(), new com.oblador.vectoricons.VectorIconsPackage(), - new com.oney.WebRTCModule.WebRTCModulePackage() + new com.oney.WebRTCModule.WebRTCModulePackage(), + new org.jitsi.meet.audiomode.AudioModePackage() ); } }; diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java new file mode 100644 index 000000000..4c62371f1 --- /dev/null +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java @@ -0,0 +1,305 @@ +package org.jitsi.meet.audiomode; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Module implementing a simple API to select the appropriate audio device for a conference call. + * + * Audio calls should use AudioModeModule.AUDIO_CALL, which uses the builtin earpiece, + * wired headset or bluetooth headset. The builtin earpiece is the default audio device. + * + * Video calls should should use AudioModeModule.VIDEO_CALL, which uses the builtin + * speaker, earpiece, wired headset or bluetooth headset. The builtin speaker is the default + * audio device. + * + * Before a call has started and after it has ended the AudioModeModule.DEFAULT mode + * should be used. + */ +public class AudioModeModule extends ReactContextBaseJavaModule { + /** + * Constants representing the audio mode. + * - DEFAULT: Used before and after every call. It represents the default audio routing scheme. + * - AUDIO_CALL: Used for audio only calls. It will use the earpiece by default, unless a + * wired or Bluetooth headset is connected. + * - VIDEO_CALL: Used for video calls. It will use the speaker by default, unless a wired or + * Bluetooth headset is connected. + */ + private static final int DEFAULT = 0; + private static final int AUDIO_CALL = 1; + private static final int VIDEO_CALL = 2; + + /** + * React Native module name. + */ + private static final String MODULE_NAME = "AudioMode"; + + /** + * Tag used when logging messages. + */ + static final String TAG = MODULE_NAME; + + /** + * Audio mode currently in use. + */ + private int mode = -1; + + /** + * {@link AudioManager} instance used to interact with the Android audio subsystem. + */ + private final AudioManager audioManager; + + /** + * {@link Handler} for running all operations on the main thread. + */ + private final Handler mainThreadHandler; + + /** + * {@link Runnable} for running update operation on the main thread. + */ + private final Runnable mainThreadRunner; + + /** + * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in old (< M) + * Android versions. + */ + private BluetoothHeadsetMonitor bluetoothHeadsetMonitor; + + /** + * + */ + private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? + AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; + + /** + * Initializes a new module instance. There shall be a single instance of this module throughout + * the lifetime of the application. + * + * @param reactContext the {@link ReactApplicationContext} where this module is created. + */ + public AudioModeModule(ReactApplicationContext reactContext) { + super(reactContext); + + audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); + mainThreadHandler = new Handler(Looper.getMainLooper()); + mainThreadRunner = new Runnable() { + @Override + public void run() { + if (mode != -1) { + updateAudioRoute(mode); + } + } + }; + + // Setup runtime device change detection + setupAudioRouteChangeDetection(); + } + + /** + * Gets the name for this module, to be used in the React Native bridge. + * + * @return a string with the module name. + */ + @Override + public String getName() { + return MODULE_NAME; + } + + /** + * Gets a mapping with the constants this module is exporting. + * + * @return a {@link Map} mapping the constants to be exported with their values. + */ + @Override + public Map getConstants() { + Map constants = new HashMap<>(); + constants.put("DEFAULT", DEFAULT); + constants.put("AUDIO_CALL", AUDIO_CALL); + constants.put("VIDEO_CALL", VIDEO_CALL); + return constants; + } + + /** + * Updates the audio route for the given mode. + * + * @param mode the audio mode to be used when computing the audio route. + * @return true if the audio route was updated successfully, false otherwise. + */ + private boolean updateAudioRoute(int mode) { + Log.d(TAG, "Update audio route for mode: " + mode); + + if (mode == DEFAULT) { + audioManager.setMode(AudioManager.MODE_NORMAL); + audioManager.abandonAudioFocus(null); + audioManager.setSpeakerphoneOn(false); + audioManager.setMicrophoneMute(true); + setBluetoothAudioRoute(false); + + return true; + } + + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + audioManager.setMicrophoneMute(false); + + if (audioManager.requestAudioFocus( + null, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN) + == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { + Log.d(TAG, "Audio focus request failed"); + return false; + } + + boolean useSpeaker = (mode == VIDEO_CALL); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // On Android >= M we use the AudioDeviceCallback API, so turn on Bluetooth SCO + // from the start. + if (audioManager.isBluetoothScoAvailableOffCall()) { + audioManager.startBluetoothSco(); + } + } else { + // On older Android versions we must set the Bluetooth route manually. Also disable + // the speaker in that case. + setBluetoothAudioRoute(bluetoothHeadsetMonitor.isHeadsetAvailable()); + if (bluetoothHeadsetMonitor.isHeadsetAvailable()) { + useSpeaker = false; + } + } + + // XXX: isWiredHeadsetOn is not deprecated when used just for knowing if there is a wired + // headset connected, regardless of audio being routed to it. + audioManager.setSpeakerphoneOn(useSpeaker && + !(audioManager.isWiredHeadsetOn() || audioManager.isBluetoothScoOn())); + + return true; + } + + /** + * Public method to set the current audio mode. + * + * @param mode the desired audio mode. + * @param promise a {@link Promise} which will be resolved if the audio mode could be updated + * successfully, and it will be rejected otherwise. + */ + @ReactMethod + public void setMode(final int mode, final Promise promise) { + if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) { + promise.reject("setMode", "Invalid audio mode " + mode); + return; + } + + Runnable r = new Runnable() { + @Override + public void run() { + if (updateAudioRoute(mode)) { + AudioModeModule.this.mode = mode; + promise.resolve(null); + } else { + promise.reject("setMode", "Failed to set the requested audio mode"); + } + } + }; + mainThreadHandler.post(r); + } + + /** + * Setup the audio route change detection mechanism. We use the + * {@link android.media.AudioDeviceCallback} API on Android >= 23 only. + */ + private void setupAudioRouteChangeDetection() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setupAudioRouteChangeDetectionM(); + } else { + setupAudioRouteChangeDetectionOld(); + } + } + + /** + * Audio route change detection mechanism for Android API >= 23. + */ + @TargetApi(Build.VERSION_CODES.M) + private void setupAudioRouteChangeDetectionM() { + android.media.AudioDeviceCallback audioDeviceCallback = + new android.media.AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + Log.d(TAG, "Audio devices added"); + onAudioDeviceChange(); + } + + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + Log.d(TAG, "Audio devices removed"); + onAudioDeviceChange(); + } + }; + + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); + } + + /** + * Audio route change detection mechanism for Android API < 23. + */ + private void setupAudioRouteChangeDetectionOld() { + ReactContext reactContext = getReactApplicationContext(); + + // Detect changes in wired headset connections + IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG); + BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "Wired headset added / removed"); + onAudioDeviceChange(); + } + }; + reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); + + // Detect Bluetooth device changes + bluetoothHeadsetMonitor = + new BluetoothHeadsetMonitor(this, this.getReactApplicationContext()); + bluetoothHeadsetMonitor.start(); + } + + /** + * Helper method to set the output route to a Bluetooth device. + * @param enabled true if Bluetooth should use used, false otherwise. + */ + private void setBluetoothAudioRoute(boolean enabled) { + if (enabled) { + audioManager.startBluetoothSco(); + audioManager.setBluetoothScoOn(true); + } else { + audioManager.setBluetoothScoOn(false); + audioManager.stopBluetoothSco(); + } + } + + /** + * Helper method to trigger an audio route update when devices change. It makes sure the + * operation is performed on the main thread. + */ + void onAudioDeviceChange() { + mainThreadHandler.post(mainThreadRunner); + } +} diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java new file mode 100644 index 000000000..19dc261a6 --- /dev/null +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java @@ -0,0 +1,41 @@ +package org.jitsi.meet.audiomode; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class AudioModePackage implements ReactPackage { + /** + * {@inheritDoc} + * @return List of native modules to be exposed by React Native. + */ + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new AudioModeModule(reactContext)); + return modules; + } + + /** + * {@inheritDoc} + */ + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java new file mode 100644 index 000000000..bf184a8fa --- /dev/null +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java @@ -0,0 +1,192 @@ +package org.jitsi.meet.audiomode; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.facebook.react.bridge.ReactContext; + +import java.util.List; + +/** + * Helper class to detect and handle Bluetooth device changes. It monitors Bluetooth headsets + * being connected / disconnected and notifies the module about device changes when this occurs. + */ +public class BluetoothHeadsetMonitor { + /** + * {@link AudioModeModule} where this monitor reports. + */ + private final AudioModeModule AudioModeModule; + + /** + * {@link AudioManager} instance used to interact with the Android audio subsystem. + */ + private final AudioManager audioManager; + + /** + * {@link ReactContext} instance where the main module runs. + */ + private final ReactContext reactContext; + + /** + * Reference to the Bluetooth adapter, needed for managing BluetoothProfile.HEADSET + * devices. + */ + private BluetoothAdapter bluetoothAdapter; + + /** + * Reference to a proxy object which allows us to query connected devices. + */ + private BluetoothHeadset bluetoothHeadset; + + /** + * Listener for Bluetooth service profiles, allows us to get the proxy object to + * {@link BluetoothHeadset}. + */ + private BluetoothProfile.ServiceListener bluetoothProfileListener; + + /** + * {@link Handler} for running all operations on the main thread. + */ + private final Handler mainThreadHandler; + + /** + * Helper for running Bluetooth operations on the main thread. + */ + private Runnable bluetoothRunnable; + + /** + * Flag indicating if there are any Bluetooth headset devices currently available. + */ + private boolean headsetAvailable = false; + + public BluetoothHeadsetMonitor(AudioModeModule audioModeModule, ReactContext reactContext) { + this.AudioModeModule = audioModeModule; + this.reactContext = reactContext; + + audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); + bluetoothAdapter = null; + bluetoothHeadset = null; + bluetoothProfileListener = null; + mainThreadHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Start monitoring Bluetooth device activity. + */ + public void start() { + bluetoothRunnable = new Runnable() { + @Override + public void run() { + if (bluetoothHeadset == null) { + headsetAvailable = false; + } else { + List devices = bluetoothHeadset.getConnectedDevices(); + headsetAvailable = !devices.isEmpty(); + } + BluetoothHeadsetMonitor.this.AudioModeModule.onAudioDeviceChange(); + } + }; + + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth"); + return; + } + + if (!audioManager.isBluetoothScoAvailableOffCall()) { + Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available"); + return; + } + + // XXX: The profile listener listens for system services of the given type being available + // to the application. That is, if our Bluetooth adapter has the "headset" profile. + bluetoothProfileListener = new BluetoothProfile.ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile == BluetoothProfile.HEADSET) { + bluetoothHeadset = (BluetoothHeadset) proxy; + updateDevices(); + } + } + + @Override + public void onServiceDisconnected(int profile) { + if (profile == BluetoothProfile.HEADSET) { + bluetoothHeadset = null; + updateDevices(); + } + } + }; + + bluetoothAdapter.getProfileProxy(reactContext, + bluetoothProfileListener, BluetoothProfile.HEADSET); + + IntentFilter bluetoothFilter = new IntentFilter(); + bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); + bluetoothFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { + // XXX: This action will be fired when a Bluetooth headset is connected or + // disconnected to the system. This is not related to audio routing. + final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); + switch (state) { + case BluetoothHeadset.STATE_CONNECTED: + case BluetoothHeadset.STATE_DISCONNECTED: + Log.d(AudioModeModule.TAG, "BT headset connection state changed: " + state); + updateDevices(); + break; + default: + break; + } + } else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { + // XXX: This action will be fired when the connection established with a + // Bluetooth headset (called a SCO connection) changes state. When the SCO + // connection is active we route audio to it. + final int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99); + switch (state) { + case AudioManager.SCO_AUDIO_STATE_CONNECTED: + case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: + Log.d(AudioModeModule.TAG, "BT SCO connection state changed: " + state); + updateDevices(); + break; + default: + break; + } + } + } + }; + reactContext.registerReceiver(bluetoothReceiver, bluetoothFilter); + + // Initial detection + updateDevices(); + } + + /** + * Detect if there are new devices connected / disconnected and fires the + * onAudioDeviceChange callback. + */ + private void updateDevices() { + mainThreadHandler.post(bluetoothRunnable); + } + + /** + * Returns the current headset availability. + * @return true if there is a Bluetooth headset connected, false otherwise. + */ + public boolean isHeadsetAvailable() { + return headsetAvailable; + } +} \ No newline at end of file From ef39baab47954e6f0b1d8b03dd2d72754d6d5a15 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Thu, 26 Jan 2017 20:12:58 -0600 Subject: [PATCH 3/5] Comply w/ coding style - Maximum of 80 characters per line. - Group first and then sort in alphabetical order. - Fields should begin with a lowercase letter. --- android/app/src/main/AndroidManifest.xml | 2 +- .../jitsi/meet/audiomode/AudioModeModule.java | 347 ++++++++++-------- .../meet/audiomode/AudioModePackage.java | 33 +- .../audiomode/BluetoothHeadsetMonitor.java | 130 ++++--- 4 files changed, 284 insertions(+), 228 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9c7b2fbdb..f46293cfd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ android:versionCode="1" android:versionName="1.0"> - + diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java index 4c62371f1..09fa9f866 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java @@ -22,33 +22,43 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - /** - * Module implementing a simple API to select the appropriate audio device for a conference call. + * Module implementing a simple API to select the appropriate audio device for a + * conference call. * - * Audio calls should use AudioModeModule.AUDIO_CALL, which uses the builtin earpiece, - * wired headset or bluetooth headset. The builtin earpiece is the default audio device. + * Audio calls should use AudioModeModule.AUDIO_CALL, which uses the + * builtin earpiece, wired headset or bluetooth headset. The builtin earpiece is + * the default audio device. * - * Video calls should should use AudioModeModule.VIDEO_CALL, which uses the builtin - * speaker, earpiece, wired headset or bluetooth headset. The builtin speaker is the default - * audio device. + * Video calls should should use AudioModeModule.VIDEO_CALL, which uses + * the builtin speaker, earpiece, wired headset or bluetooth headset. The + * builtin speaker is the default audio device. * - * Before a call has started and after it has ended the AudioModeModule.DEFAULT mode - * should be used. + * Before a call has started and after it has ended the + * AudioModeModule.DEFAULT mode should be used. */ public class AudioModeModule extends ReactContextBaseJavaModule { /** * Constants representing the audio mode. - * - DEFAULT: Used before and after every call. It represents the default audio routing scheme. - * - AUDIO_CALL: Used for audio only calls. It will use the earpiece by default, unless a - * wired or Bluetooth headset is connected. - * - VIDEO_CALL: Used for video calls. It will use the speaker by default, unless a wired or - * Bluetooth headset is connected. + * - DEFAULT: Used before and after every call. It represents the default + * audio routing scheme. + * - AUDIO_CALL: Used for audio only calls. It will use the earpiece by + * default, unless a wired or Bluetooth headset is connected. + * - VIDEO_CALL: Used for video calls. It will use the speaker by default, + * unless a wired or Bluetooth headset is connected. */ private static final int DEFAULT = 0; private static final int AUDIO_CALL = 1; private static final int VIDEO_CALL = 2; + /** + * + */ + private static final String ACTION_HEADSET_PLUG + = (android.os.Build.VERSION.SDK_INT >= 21) + ? AudioManager.ACTION_HEADSET_PLUG + : Intent.ACTION_HEADSET_PLUG; + /** * React Native module name. */ @@ -60,15 +70,17 @@ public class AudioModeModule extends ReactContextBaseJavaModule { static final String TAG = MODULE_NAME; /** - * Audio mode currently in use. - */ - private int mode = -1; - - /** - * {@link AudioManager} instance used to interact with the Android audio subsystem. + * {@link AudioManager} instance used to interact with the Android audio + * subsystem. */ private final AudioManager audioManager; + /** + * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in + * old (< M) Android versions. + */ + private BluetoothHeadsetMonitor bluetoothHeadsetMonitor; + /** * {@link Handler} for running all operations on the main thread. */ @@ -80,27 +92,23 @@ public class AudioModeModule extends ReactContextBaseJavaModule { private final Runnable mainThreadRunner; /** - * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in old (< M) - * Android versions. + * Audio mode currently in use. */ - private BluetoothHeadsetMonitor bluetoothHeadsetMonitor; + private int mode = -1; /** + * Initializes a new module instance. There shall be a single instance of + * this module throughout the lifetime of the application. * - */ - private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? - AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; - - /** - * Initializes a new module instance. There shall be a single instance of this module throughout - * the lifetime of the application. - * - * @param reactContext the {@link ReactApplicationContext} where this module is created. + * @param reactContext the {@link ReactApplicationContext} where this module + * is created. */ public AudioModeModule(ReactApplicationContext reactContext) { super(reactContext); - audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); + audioManager + = (AudioManager) + reactContext.getSystemService(Context.AUDIO_SERVICE); mainThreadHandler = new Handler(Looper.getMainLooper()); mainThreadRunner = new Runnable() { @Override @@ -111,10 +119,27 @@ public class AudioModeModule extends ReactContextBaseJavaModule { } }; - // Setup runtime device change detection + // Setup runtime device change detection. setupAudioRouteChangeDetection(); } + /** + * Gets a mapping with the constants this module is exporting. + * + * @return a {@link Map} mapping the constants to be exported with their + * values. + */ + @Override + public Map getConstants() { + Map constants = new HashMap<>(); + + constants.put("AUDIO_CALL", AUDIO_CALL); + constants.put("DEFAULT", DEFAULT); + constants.put("VIDEO_CALL", VIDEO_CALL); + + return constants; + } + /** * Gets the name for this module, to be used in the React Native bridge. * @@ -126,24 +151,126 @@ public class AudioModeModule extends ReactContextBaseJavaModule { } /** - * Gets a mapping with the constants this module is exporting. - * - * @return a {@link Map} mapping the constants to be exported with their values. + * Helper method to trigger an audio route update when devices change. It + * makes sure the operation is performed on the main thread. */ - @Override - public Map getConstants() { - Map constants = new HashMap<>(); - constants.put("DEFAULT", DEFAULT); - constants.put("AUDIO_CALL", AUDIO_CALL); - constants.put("VIDEO_CALL", VIDEO_CALL); - return constants; + void onAudioDeviceChange() { + mainThreadHandler.post(mainThreadRunner); + } + + /** + * Helper method to set the output route to a Bluetooth device. + * + * @param enabled true if Bluetooth should use used, false otherwise. + */ + private void setBluetoothAudioRoute(boolean enabled) { + if (enabled) { + audioManager.startBluetoothSco(); + audioManager.setBluetoothScoOn(true); + } else { + audioManager.setBluetoothScoOn(false); + audioManager.stopBluetoothSco(); + } + } + + /** + * Public method to set the current audio mode. + * + * @param mode the desired audio mode. + * @param promise a {@link Promise} which will be resolved if the audio mode + * could be updated successfully, and it will be rejected otherwise. + */ + @ReactMethod + public void setMode(final int mode, final Promise promise) { + if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) { + promise.reject("setMode", "Invalid audio mode " + mode); + return; + } + + Runnable r = new Runnable() { + @Override + public void run() { + if (updateAudioRoute(mode)) { + AudioModeModule.this.mode = mode; + promise.resolve(null); + } else { + promise.reject( + "setMode", + "Failed to set the requested audio mode"); + } + } + }; + mainThreadHandler.post(r); + } + + /** + * Setup the audio route change detection mechanism. We use the + * {@link android.media.AudioDeviceCallback} API on Android >= 23 only. + */ + private void setupAudioRouteChangeDetection() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setupAudioRouteChangeDetectionM(); + } else { + setupAudioRouteChangeDetectionPreM(); + } + } + + /** + * Audio route change detection mechanism for Android API >= 23. + */ + @TargetApi(Build.VERSION_CODES.M) + private void setupAudioRouteChangeDetectionM() { + android.media.AudioDeviceCallback audioDeviceCallback = + new android.media.AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded( + AudioDeviceInfo[] addedDevices) { + Log.d(TAG, "Audio devices added"); + onAudioDeviceChange(); + } + + @Override + public void onAudioDevicesRemoved( + AudioDeviceInfo[] removedDevices) { + Log.d(TAG, "Audio devices removed"); + onAudioDeviceChange(); + } + }; + + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); + } + + /** + * Audio route change detection mechanism for Android API < 23. + */ + private void setupAudioRouteChangeDetectionPreM() { + ReactContext reactContext = getReactApplicationContext(); + + // Detect changes in wired headset connections. + IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG); + BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "Wired headset added / removed"); + onAudioDeviceChange(); + } + }; + reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); + + // Detect Bluetooth device changes. + bluetoothHeadsetMonitor + = new BluetoothHeadsetMonitor( + this, + getReactApplicationContext()); + bluetoothHeadsetMonitor.start(); } /** * Updates the audio route for the given mode. * * @param mode the audio mode to be used when computing the audio route. - * @return true if the audio route was updated successfully, false otherwise. + * @return true if the audio route was updated successfully, false + * otherwise. */ private boolean updateAudioRoute(int mode) { Log.d(TAG, "Update audio route for mode: " + mode); @@ -173,133 +300,29 @@ public class AudioModeModule extends ReactContextBaseJavaModule { boolean useSpeaker = (mode == VIDEO_CALL); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // On Android >= M we use the AudioDeviceCallback API, so turn on Bluetooth SCO - // from the start. + // On Android >= M we use the AudioDeviceCallback API, so turn on + // Bluetooth SCO from the start. if (audioManager.isBluetoothScoAvailableOffCall()) { audioManager.startBluetoothSco(); } } else { - // On older Android versions we must set the Bluetooth route manually. Also disable - // the speaker in that case. - setBluetoothAudioRoute(bluetoothHeadsetMonitor.isHeadsetAvailable()); + // On older Android versions we must set the Bluetooth route + // manually. Also disable the speaker in that case. + setBluetoothAudioRoute( + bluetoothHeadsetMonitor.isHeadsetAvailable()); if (bluetoothHeadsetMonitor.isHeadsetAvailable()) { useSpeaker = false; } } - // XXX: isWiredHeadsetOn is not deprecated when used just for knowing if there is a wired - // headset connected, regardless of audio being routed to it. - audioManager.setSpeakerphoneOn(useSpeaker && - !(audioManager.isWiredHeadsetOn() || audioManager.isBluetoothScoOn())); + // XXX: isWiredHeadsetOn is not deprecated when used just for knowing if + // there is a wired headset connected, regardless of audio being routed + // to it. + audioManager.setSpeakerphoneOn( + useSpeaker + && !(audioManager.isWiredHeadsetOn() + || audioManager.isBluetoothScoOn())); return true; } - - /** - * Public method to set the current audio mode. - * - * @param mode the desired audio mode. - * @param promise a {@link Promise} which will be resolved if the audio mode could be updated - * successfully, and it will be rejected otherwise. - */ - @ReactMethod - public void setMode(final int mode, final Promise promise) { - if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) { - promise.reject("setMode", "Invalid audio mode " + mode); - return; - } - - Runnable r = new Runnable() { - @Override - public void run() { - if (updateAudioRoute(mode)) { - AudioModeModule.this.mode = mode; - promise.resolve(null); - } else { - promise.reject("setMode", "Failed to set the requested audio mode"); - } - } - }; - mainThreadHandler.post(r); - } - - /** - * Setup the audio route change detection mechanism. We use the - * {@link android.media.AudioDeviceCallback} API on Android >= 23 only. - */ - private void setupAudioRouteChangeDetection() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setupAudioRouteChangeDetectionM(); - } else { - setupAudioRouteChangeDetectionOld(); - } - } - - /** - * Audio route change detection mechanism for Android API >= 23. - */ - @TargetApi(Build.VERSION_CODES.M) - private void setupAudioRouteChangeDetectionM() { - android.media.AudioDeviceCallback audioDeviceCallback = - new android.media.AudioDeviceCallback() { - @Override - public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - Log.d(TAG, "Audio devices added"); - onAudioDeviceChange(); - } - - @Override - public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - Log.d(TAG, "Audio devices removed"); - onAudioDeviceChange(); - } - }; - - audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); - } - - /** - * Audio route change detection mechanism for Android API < 23. - */ - private void setupAudioRouteChangeDetectionOld() { - ReactContext reactContext = getReactApplicationContext(); - - // Detect changes in wired headset connections - IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG); - BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "Wired headset added / removed"); - onAudioDeviceChange(); - } - }; - reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); - - // Detect Bluetooth device changes - bluetoothHeadsetMonitor = - new BluetoothHeadsetMonitor(this, this.getReactApplicationContext()); - bluetoothHeadsetMonitor.start(); - } - - /** - * Helper method to set the output route to a Bluetooth device. - * @param enabled true if Bluetooth should use used, false otherwise. - */ - private void setBluetoothAudioRoute(boolean enabled) { - if (enabled) { - audioManager.startBluetoothSco(); - audioManager.setBluetoothScoOn(true); - } else { - audioManager.setBluetoothScoOn(false); - audioManager.stopBluetoothSco(); - } - } - - /** - * Helper method to trigger an audio route update when devices change. It makes sure the - * operation is performed on the main thread. - */ - void onAudioDeviceChange() { - mainThreadHandler.post(mainThreadRunner); - } } diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java index 19dc261a6..baedd37b3 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java @@ -10,19 +10,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +/** + * Implements {@link ReactPackage} for {@link AudioModeModule}. + */ public class AudioModePackage implements ReactPackage { - /** - * {@inheritDoc} - * @return List of native modules to be exposed by React Native. - */ - @Override - public List createNativeModules( - ReactApplicationContext reactContext) { - List modules = new ArrayList<>(); - modules.add(new AudioModeModule(reactContext)); - return modules; - } - /** * {@inheritDoc} */ @@ -33,9 +24,25 @@ public class AudioModePackage implements ReactPackage { /** * {@inheritDoc} + * + * @return List of native modules to be exposed by React Native. */ @Override - public List createViewManagers(ReactApplicationContext reactContext) { + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new AudioModeModule(reactContext)); + + return modules; + } + + /** + * {@inheritDoc} + */ + @Override + public List createViewManagers( + ReactApplicationContext reactContext) { return Collections.emptyList(); } } diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java index bf184a8fa..099f6d1a9 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java @@ -18,28 +18,25 @@ import com.facebook.react.bridge.ReactContext; import java.util.List; /** - * Helper class to detect and handle Bluetooth device changes. It monitors Bluetooth headsets - * being connected / disconnected and notifies the module about device changes when this occurs. + * Helper class to detect and handle Bluetooth device changes. It monitors + * Bluetooth headsets being connected / disconnected and notifies the module + * about device changes when this occurs. */ public class BluetoothHeadsetMonitor { /** - * {@link AudioModeModule} where this monitor reports. - */ - private final AudioModeModule AudioModeModule; - - /** - * {@link AudioManager} instance used to interact with the Android audio subsystem. + * {@link AudioManager} instance used to interact with the Android audio + * subsystem. */ private final AudioManager audioManager; /** - * {@link ReactContext} instance where the main module runs. + * {@link AudioModeModule} where this monitor reports. */ - private final ReactContext reactContext; + private final AudioModeModule audioModeModule; /** - * Reference to the Bluetooth adapter, needed for managing BluetoothProfile.HEADSET - * devices. + * Reference to the Bluetooth adapter, needed for managing + * BluetoothProfile.HEADSET devices. */ private BluetoothAdapter bluetoothAdapter; @@ -49,37 +46,56 @@ public class BluetoothHeadsetMonitor { private BluetoothHeadset bluetoothHeadset; /** - * Listener for Bluetooth service profiles, allows us to get the proxy object to - * {@link BluetoothHeadset}. + * Listener for Bluetooth service profiles, allows us to get the proxy + * object to {@link BluetoothHeadset}. */ private BluetoothProfile.ServiceListener bluetoothProfileListener; - /** - * {@link Handler} for running all operations on the main thread. - */ - private final Handler mainThreadHandler; - /** * Helper for running Bluetooth operations on the main thread. */ private Runnable bluetoothRunnable; /** - * Flag indicating if there are any Bluetooth headset devices currently available. + * Flag indicating if there are any Bluetooth headset devices currently + * available. */ private boolean headsetAvailable = false; - public BluetoothHeadsetMonitor(AudioModeModule audioModeModule, ReactContext reactContext) { - this.AudioModeModule = audioModeModule; + /** + * {@link Handler} for running all operations on the main thread. + */ + private final Handler mainThreadHandler; + + /** + * {@link ReactContext} instance where the main module runs. + */ + private final ReactContext reactContext; + + public BluetoothHeadsetMonitor( + AudioModeModule audioModeModule, + ReactContext reactContext) { + this.audioModeModule = audioModeModule; this.reactContext = reactContext; - audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); + audioManager + = (AudioManager) + reactContext.getSystemService(Context.AUDIO_SERVICE); bluetoothAdapter = null; bluetoothHeadset = null; bluetoothProfileListener = null; mainThreadHandler = new Handler(Looper.getMainLooper()); } + /** + * Returns the current headset availability. + * + * @return true if there is a Bluetooth headset connected, false otherwise. + */ + public boolean isHeadsetAvailable() { + return headsetAvailable; + } + /** * Start monitoring Bluetooth device activity. */ @@ -90,10 +106,11 @@ public class BluetoothHeadsetMonitor { if (bluetoothHeadset == null) { headsetAvailable = false; } else { - List devices = bluetoothHeadset.getConnectedDevices(); + List devices + = bluetoothHeadset.getConnectedDevices(); headsetAvailable = !devices.isEmpty(); } - BluetoothHeadsetMonitor.this.AudioModeModule.onAudioDeviceChange(); + audioModeModule.onAudioDeviceChange(); } }; @@ -108,11 +125,14 @@ public class BluetoothHeadsetMonitor { return; } - // XXX: The profile listener listens for system services of the given type being available - // to the application. That is, if our Bluetooth adapter has the "headset" profile. + // XXX: The profile listener listens for system services of the given + // type being available to the application. That is, if our Bluetooth + // adapter has the "headset" profile. bluetoothProfileListener = new BluetoothProfile.ServiceListener() { @Override - public void onServiceConnected(int profile, BluetoothProfile proxy) { + public void onServiceConnected( + int profile, + BluetoothProfile proxy) { if (profile == BluetoothProfile.HEADSET) { bluetoothHeadset = (BluetoothHeadset) proxy; updateDevices(); @@ -133,33 +153,47 @@ public class BluetoothHeadsetMonitor { IntentFilter bluetoothFilter = new IntentFilter(); bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); - bluetoothFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + bluetoothFilter.addAction( + BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); - if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { - // XXX: This action will be fired when a Bluetooth headset is connected or - // disconnected to the system. This is not related to audio routing. - final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); + if (action.equals( + BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { + // XXX: This action will be fired when a Bluetooth headset + // is connected or disconnected to the system. This is not + // related to audio routing. + final int state + = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); switch (state) { case BluetoothHeadset.STATE_CONNECTED: case BluetoothHeadset.STATE_DISCONNECTED: - Log.d(AudioModeModule.TAG, "BT headset connection state changed: " + state); + Log.d( + AudioModeModule.TAG, + "BT headset connection state changed: " + + state); updateDevices(); break; default: break; } - } else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { - // XXX: This action will be fired when the connection established with a - // Bluetooth headset (called a SCO connection) changes state. When the SCO - // connection is active we route audio to it. - final int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99); + } else if (action.equals( + AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { + // XXX: This action will be fired when the connection + // established with a Bluetooth headset (called a SCO + // connection) changes state. When the SCO connection is + // active we route audio to it. + final int state + = intent.getIntExtra( + AudioManager.EXTRA_SCO_AUDIO_STATE, + -99); switch (state) { case AudioManager.SCO_AUDIO_STATE_CONNECTED: case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: - Log.d(AudioModeModule.TAG, "BT SCO connection state changed: " + state); + Log.d( + AudioModeModule.TAG, + "BT SCO connection state changed: " + state); updateDevices(); break; default: @@ -170,23 +204,15 @@ public class BluetoothHeadsetMonitor { }; reactContext.registerReceiver(bluetoothReceiver, bluetoothFilter); - // Initial detection + // Initial detection. updateDevices(); } /** - * Detect if there are new devices connected / disconnected and fires the - * onAudioDeviceChange callback. + * Detects if there are new devices connected / disconnected and fires the + * {@link AudioModeModule#onAudioDeviceChange()} callback. */ private void updateDevices() { mainThreadHandler.post(bluetoothRunnable); } - - /** - * Returns the current headset availability. - * @return true if there is a Bluetooth headset connected, false otherwise. - */ - public boolean isHeadsetAvailable() { - return headsetAvailable; - } -} \ No newline at end of file +} From bab94a207d6443f610807c3a196235b49976b939 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Thu, 26 Jan 2017 21:08:50 -0600 Subject: [PATCH 4/5] Remove unnecessary source code --- .../java/org/jitsi/meet/audiomode/AudioModeModule.java | 3 +-- .../jitsi/meet/audiomode/BluetoothHeadsetMonitor.java | 10 ++-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java index 09fa9f866..c2bde519d 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java @@ -19,7 +19,6 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -55,7 +54,7 @@ public class AudioModeModule extends ReactContextBaseJavaModule { * */ private static final String ACTION_HEADSET_PLUG - = (android.os.Build.VERSION.SDK_INT >= 21) + = (Build.VERSION.SDK_INT >= 21) ? AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java index 099f6d1a9..ab0108277 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java @@ -15,8 +15,6 @@ import android.util.Log; import com.facebook.react.bridge.ReactContext; -import java.util.List; - /** * Helper class to detect and handle Bluetooth device changes. It monitors * Bluetooth headsets being connected / disconnected and notifies the module @@ -81,9 +79,6 @@ public class BluetoothHeadsetMonitor { audioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE); - bluetoothAdapter = null; - bluetoothHeadset = null; - bluetoothProfileListener = null; mainThreadHandler = new Handler(Looper.getMainLooper()); } @@ -106,9 +101,8 @@ public class BluetoothHeadsetMonitor { if (bluetoothHeadset == null) { headsetAvailable = false; } else { - List devices - = bluetoothHeadset.getConnectedDevices(); - headsetAvailable = !devices.isEmpty(); + headsetAvailable + = !bluetoothHeadset.getConnectedDevices().isEmpty(); } audioModeModule.onAudioDeviceChange(); } From 18bc99d6b54bb3e99cb76c8f178d1ec3472019ad Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Thu, 26 Jan 2017 23:35:56 -0600 Subject: [PATCH 5/5] Split long methods into multiple shorter ones --- .../jitsi/meet/audiomode/AudioModeModule.java | 33 +-- .../audiomode/BluetoothHeadsetMonitor.java | 245 ++++++++---------- 2 files changed, 125 insertions(+), 153 deletions(-) diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java index c2bde519d..e28f6e085 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java @@ -14,7 +14,6 @@ import android.util.Log; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -83,12 +82,21 @@ public class AudioModeModule extends ReactContextBaseJavaModule { /** * {@link Handler} for running all operations on the main thread. */ - private final Handler mainThreadHandler; + private final Handler mainThreadHandler + = new Handler(Looper.getMainLooper()); /** * {@link Runnable} for running update operation on the main thread. */ - private final Runnable mainThreadRunner; + private final Runnable mainThreadRunner + = new Runnable() { + @Override + public void run() { + if (mode != -1) { + updateAudioRoute(mode); + } + } + }; /** * Audio mode currently in use. @@ -108,15 +116,6 @@ public class AudioModeModule extends ReactContextBaseJavaModule { audioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE); - mainThreadHandler = new Handler(Looper.getMainLooper()); - mainThreadRunner = new Runnable() { - @Override - public void run() { - if (mode != -1) { - updateAudioRoute(mode); - } - } - }; // Setup runtime device change detection. setupAudioRouteChangeDetection(); @@ -243,7 +242,7 @@ public class AudioModeModule extends ReactContextBaseJavaModule { * Audio route change detection mechanism for Android API < 23. */ private void setupAudioRouteChangeDetectionPreM() { - ReactContext reactContext = getReactApplicationContext(); + Context context = getReactApplicationContext(); // Detect changes in wired headset connections. IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG); @@ -254,14 +253,10 @@ public class AudioModeModule extends ReactContextBaseJavaModule { onAudioDeviceChange(); } }; - reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); + context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); // Detect Bluetooth device changes. - bluetoothHeadsetMonitor - = new BluetoothHeadsetMonitor( - this, - getReactApplicationContext()); - bluetoothHeadsetMonitor.start(); + bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(this, context); } /** diff --git a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java index ab0108277..f66d1ab33 100644 --- a/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java +++ b/android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java @@ -13,46 +13,26 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; -import com.facebook.react.bridge.ReactContext; - /** * Helper class to detect and handle Bluetooth device changes. It monitors * Bluetooth headsets being connected / disconnected and notifies the module * about device changes when this occurs. */ public class BluetoothHeadsetMonitor { - /** - * {@link AudioManager} instance used to interact with the Android audio - * subsystem. - */ - private final AudioManager audioManager; - /** * {@link AudioModeModule} where this monitor reports. */ private final AudioModeModule audioModeModule; /** - * Reference to the Bluetooth adapter, needed for managing - * BluetoothProfile.HEADSET devices. + * The {@link Context} in which {@link #audioModeModule} executes. */ - private BluetoothAdapter bluetoothAdapter; + private final Context context; /** * Reference to a proxy object which allows us to query connected devices. */ - private BluetoothHeadset bluetoothHeadset; - - /** - * Listener for Bluetooth service profiles, allows us to get the proxy - * object to {@link BluetoothHeadset}. - */ - private BluetoothProfile.ServiceListener bluetoothProfileListener; - - /** - * Helper for running Bluetooth operations on the main thread. - */ - private Runnable bluetoothRunnable; + private BluetoothHeadset headset; /** * Flag indicating if there are any Bluetooth headset devices currently @@ -63,23 +43,80 @@ public class BluetoothHeadsetMonitor { /** * {@link Handler} for running all operations on the main thread. */ - private final Handler mainThreadHandler; + private final Handler mainThreadHandler + = new Handler(Looper.getMainLooper()); /** - * {@link ReactContext} instance where the main module runs. + * Helper for running Bluetooth operations on the main thread. */ - private final ReactContext reactContext; + private final Runnable updateDevicesRunnable + = new Runnable() { + @Override + public void run() { + headsetAvailable + = (headset != null) + && !headset.getConnectedDevices().isEmpty(); + audioModeModule.onAudioDeviceChange(); + } + }; public BluetoothHeadsetMonitor( AudioModeModule audioModeModule, - ReactContext reactContext) { + Context context) { this.audioModeModule = audioModeModule; - this.reactContext = reactContext; + this.context = context; - audioManager - = (AudioManager) - reactContext.getSystemService(Context.AUDIO_SERVICE); - mainThreadHandler = new Handler(Looper.getMainLooper()); + AudioManager audioManager + = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + if (!audioManager.isBluetoothScoAvailableOffCall()) { + Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available"); + return; + } + + if (getBluetoothHeadsetProfileProxy()) { + registerBluetoothReceiver(); + + // Initial detection. + updateDevices(); + } + } + + private boolean getBluetoothHeadsetProfileProxy() { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + if (adapter == null) { + Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth"); + return false; + } + + // XXX: The profile listener listens for system services of the given + // type being available to the application. That is, if our Bluetooth + // adapter has the "headset" profile. + BluetoothProfile.ServiceListener listener + = new BluetoothProfile.ServiceListener() { + @Override + public void onServiceConnected( + int profile, + BluetoothProfile proxy) { + if (profile == BluetoothProfile.HEADSET) { + headset = (BluetoothHeadset) proxy; + updateDevices(); + } + } + + @Override + public void onServiceDisconnected(int profile) { + // The logic is the same as the logic of onServiceConnected. + onServiceConnected(profile, /* proxy */ null); + } + }; + + return + adapter.getProfileProxy( + context, + listener, + BluetoothProfile.HEADSET); } /** @@ -91,115 +128,55 @@ public class BluetoothHeadsetMonitor { return headsetAvailable; } - /** - * Start monitoring Bluetooth device activity. - */ - public void start() { - bluetoothRunnable = new Runnable() { - @Override - public void run() { - if (bluetoothHeadset == null) { - headsetAvailable = false; - } else { - headsetAvailable - = !bluetoothHeadset.getConnectedDevices().isEmpty(); - } - audioModeModule.onAudioDeviceChange(); - } - }; + private void onBluetoothReceiverReceive(Context context, Intent intent) { + final String action = intent.getAction(); - bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter == null) { - Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth"); - return; + if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { + // XXX: This action will be fired when a Bluetooth headset is + // connected or disconnected to the system. This is not related to + // audio routing. + int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); + + switch (state) { + case BluetoothHeadset.STATE_CONNECTED: + case BluetoothHeadset.STATE_DISCONNECTED: + Log.d( + AudioModeModule.TAG, + "BT headset connection state changed: " + state); + updateDevices(); + break; + } + } else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { + // XXX: This action will be fired when the connection established + // with a Bluetooth headset (called a SCO connection) changes state. + // When the SCO connection is active we route audio to it. + int state + = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99); + + switch (state) { + case AudioManager.SCO_AUDIO_STATE_CONNECTED: + case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: + Log.d( + AudioModeModule.TAG, + "BT SCO connection state changed: " + state); + updateDevices(); + break; + } } + } - if (!audioManager.isBluetoothScoAvailableOffCall()) { - Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available"); - return; - } - - // XXX: The profile listener listens for system services of the given - // type being available to the application. That is, if our Bluetooth - // adapter has the "headset" profile. - bluetoothProfileListener = new BluetoothProfile.ServiceListener() { - @Override - public void onServiceConnected( - int profile, - BluetoothProfile proxy) { - if (profile == BluetoothProfile.HEADSET) { - bluetoothHeadset = (BluetoothHeadset) proxy; - updateDevices(); - } - } - - @Override - public void onServiceDisconnected(int profile) { - if (profile == BluetoothProfile.HEADSET) { - bluetoothHeadset = null; - updateDevices(); - } - } - }; - - bluetoothAdapter.getProfileProxy(reactContext, - bluetoothProfileListener, BluetoothProfile.HEADSET); - - IntentFilter bluetoothFilter = new IntentFilter(); - bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); - bluetoothFilter.addAction( - BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); - BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { + private void registerBluetoothReceiver() { + BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (action.equals( - BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { - // XXX: This action will be fired when a Bluetooth headset - // is connected or disconnected to the system. This is not - // related to audio routing. - final int state - = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); - switch (state) { - case BluetoothHeadset.STATE_CONNECTED: - case BluetoothHeadset.STATE_DISCONNECTED: - Log.d( - AudioModeModule.TAG, - "BT headset connection state changed: " - + state); - updateDevices(); - break; - default: - break; - } - } else if (action.equals( - AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { - // XXX: This action will be fired when the connection - // established with a Bluetooth headset (called a SCO - // connection) changes state. When the SCO connection is - // active we route audio to it. - final int state - = intent.getIntExtra( - AudioManager.EXTRA_SCO_AUDIO_STATE, - -99); - switch (state) { - case AudioManager.SCO_AUDIO_STATE_CONNECTED: - case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: - Log.d( - AudioModeModule.TAG, - "BT SCO connection state changed: " + state); - updateDevices(); - break; - default: - break; - } - } + onBluetoothReceiverReceive(context, intent); } }; - reactContext.registerReceiver(bluetoothReceiver, bluetoothFilter); + IntentFilter filter = new IntentFilter(); - // Initial detection. - updateDevices(); + filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); + filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + context.registerReceiver(receiver, filter); } /** @@ -207,6 +184,6 @@ public class BluetoothHeadsetMonitor { * {@link AudioModeModule#onAudioDeviceChange()} callback. */ private void updateDevices() { - mainThreadHandler.post(bluetoothRunnable); + mainThreadHandler.post(updateDevicesRunnable); } }