Selaa lähdekoodia

[RN] Implement AudioMode module on Android

This module chooses the most appropriate audio default based on the specified
mode.
j8
Saúl Ibarra Corretgé 8 vuotta sitten
vanhempi
commit
2edaaac7bf

+ 4
- 1
android/app/src/main/AndroidManifest.xml Näytä tiedosto

@@ -3,9 +3,12 @@
3 3
     android:versionCode="1"
4 4
     android:versionName="1.0">
5 5
 
6
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><!-- WebRTC -->
6
+    <!-- XXX: ACCESS_NETWORK_STATE is needed by react-native-webrtc -->
7
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8
+    <uses-permission android:name="android.permission.BLUETOOTH" />
7 9
     <uses-permission android:name="android.permission.CAMERA" />
8 10
     <uses-permission android:name="android.permission.INTERNET" />
11
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
9 12
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
10 13
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
11 14
 

+ 2
- 1
android/app/src/main/java/org/jitsi/meet/MainApplication.java Näytä tiedosto

@@ -29,7 +29,8 @@ public class MainApplication extends Application implements ReactApplication {
29 29
                 new com.corbt.keepawake.KCKeepAwakePackage(),
30 30
                 new com.facebook.react.shell.MainReactPackage(),
31 31
                 new com.oblador.vectoricons.VectorIconsPackage(),
32
-                new com.oney.WebRTCModule.WebRTCModulePackage()
32
+                new com.oney.WebRTCModule.WebRTCModulePackage(),
33
+                new org.jitsi.meet.audiomode.AudioModePackage()
33 34
             );
34 35
         }
35 36
     };

+ 305
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java Näytä tiedosto

@@ -0,0 +1,305 @@
1
+package org.jitsi.meet.audiomode;
2
+
3
+import android.annotation.TargetApi;
4
+import android.content.BroadcastReceiver;
5
+import android.content.Context;
6
+import android.content.Intent;
7
+import android.content.IntentFilter;
8
+import android.media.AudioDeviceInfo;
9
+import android.media.AudioManager;
10
+import android.os.Build;
11
+import android.os.Handler;
12
+import android.os.Looper;
13
+import android.util.Log;
14
+
15
+import com.facebook.react.bridge.Promise;
16
+import com.facebook.react.bridge.ReactApplicationContext;
17
+import com.facebook.react.bridge.ReactContext;
18
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
19
+import com.facebook.react.bridge.ReactMethod;
20
+
21
+import java.util.HashMap;
22
+import java.util.List;
23
+import java.util.Map;
24
+
25
+
26
+/**
27
+ * Module implementing a simple API to select the appropriate audio device for a conference call.
28
+ *
29
+ * Audio calls should use <tt>AudioModeModule.AUDIO_CALL</tt>, which uses the builtin earpiece,
30
+ * wired headset or bluetooth headset. The builtin earpiece is the default audio device.
31
+ *
32
+ * Video calls should should use <tt>AudioModeModule.VIDEO_CALL</tt>, which uses the builtin
33
+ * speaker, earpiece, wired headset or bluetooth headset. The builtin speaker is the default
34
+ * audio device.
35
+ *
36
+ * Before a call has started and after it has ended the <tt>AudioModeModule.DEFAULT</tt> mode
37
+ * should be used.
38
+ */
39
+public class AudioModeModule extends ReactContextBaseJavaModule {
40
+    /**
41
+     * Constants representing the audio mode.
42
+     * - DEFAULT: Used before and after every call.  It represents the default audio routing scheme.
43
+     * - AUDIO_CALL: Used for audio only calls.  It will use the earpiece by default, unless a
44
+     *   wired or Bluetooth headset is connected.
45
+     * - VIDEO_CALL: Used for video calls.  It will use the speaker by default, unless a wired or
46
+     *   Bluetooth headset is connected.
47
+     */
48
+    private static final int DEFAULT    = 0;
49
+    private static final int AUDIO_CALL = 1;
50
+    private static final int VIDEO_CALL = 2;
51
+
52
+    /**
53
+     * React Native module name.
54
+     */
55
+    private static final String MODULE_NAME = "AudioMode";
56
+
57
+    /**
58
+     * Tag used when logging messages.
59
+     */
60
+    static final String TAG = MODULE_NAME;
61
+
62
+    /**
63
+     * Audio mode currently in use.
64
+     */
65
+    private int mode = -1;
66
+
67
+    /**
68
+     * {@link AudioManager} instance used to interact with the Android audio subsystem.
69
+     */
70
+    private final AudioManager audioManager;
71
+
72
+    /**
73
+     * {@link Handler} for running all operations on the main thread.
74
+     */
75
+    private final Handler mainThreadHandler;
76
+
77
+    /**
78
+     * {@link Runnable} for running update operation on the main thread.
79
+     */
80
+    private final Runnable mainThreadRunner;
81
+
82
+    /**
83
+     * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in old (< M)
84
+     * Android versions.
85
+     */
86
+    private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
87
+
88
+    /**
89
+     *
90
+     */
91
+    private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ?
92
+            AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG;
93
+
94
+    /**
95
+     * Initializes a new module instance. There shall be a single instance of this module throughout
96
+     * the lifetime of the application.
97
+     *
98
+     * @param reactContext the {@link ReactApplicationContext} where this module is created.
99
+     */
100
+    public AudioModeModule(ReactApplicationContext reactContext) {
101
+        super(reactContext);
102
+
103
+        audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE));
104
+        mainThreadHandler = new Handler(Looper.getMainLooper());
105
+        mainThreadRunner = new Runnable() {
106
+            @Override
107
+            public void run() {
108
+                if (mode != -1) {
109
+                    updateAudioRoute(mode);
110
+                }
111
+            }
112
+        };
113
+
114
+        // Setup runtime device change detection
115
+        setupAudioRouteChangeDetection();
116
+    }
117
+
118
+    /**
119
+     * Gets the name for this module, to be used in the React Native bridge.
120
+     *
121
+     * @return a string with the module name.
122
+     */
123
+    @Override
124
+    public String getName() {
125
+        return MODULE_NAME;
126
+    }
127
+
128
+    /**
129
+     * Gets a mapping with the constants this module is exporting.
130
+     *
131
+     * @return a {@link Map} mapping the constants to be exported with their values.
132
+     */
133
+    @Override
134
+    public Map<String, Object> getConstants() {
135
+        Map<String, Object> constants = new HashMap<>();
136
+        constants.put("DEFAULT", DEFAULT);
137
+        constants.put("AUDIO_CALL", AUDIO_CALL);
138
+        constants.put("VIDEO_CALL", VIDEO_CALL);
139
+        return constants;
140
+    }
141
+
142
+    /**
143
+     * Updates the audio route for the given mode.
144
+     *
145
+     * @param mode the audio mode to be used when computing the audio route.
146
+     * @return true if the audio route was updated successfully, false otherwise.
147
+     */
148
+    private boolean updateAudioRoute(int mode) {
149
+        Log.d(TAG, "Update audio route for mode: " + mode);
150
+
151
+        if (mode == DEFAULT) {
152
+            audioManager.setMode(AudioManager.MODE_NORMAL);
153
+            audioManager.abandonAudioFocus(null);
154
+            audioManager.setSpeakerphoneOn(false);
155
+            audioManager.setMicrophoneMute(true);
156
+            setBluetoothAudioRoute(false);
157
+
158
+            return true;
159
+        }
160
+
161
+        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
162
+        audioManager.setMicrophoneMute(false);
163
+
164
+        if (audioManager.requestAudioFocus(
165
+                    null,
166
+                    AudioManager.STREAM_VOICE_CALL,
167
+                    AudioManager.AUDIOFOCUS_GAIN)
168
+                == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
169
+            Log.d(TAG, "Audio focus request failed");
170
+            return false;
171
+        }
172
+
173
+        boolean useSpeaker = (mode == VIDEO_CALL);
174
+
175
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
176
+            // On Android >= M we use the AudioDeviceCallback API, so turn on Bluetooth SCO
177
+            // from the start.
178
+            if (audioManager.isBluetoothScoAvailableOffCall()) {
179
+                audioManager.startBluetoothSco();
180
+            }
181
+        } else {
182
+            // On older Android versions we must set the Bluetooth route manually. Also disable
183
+            // the speaker in that case.
184
+            setBluetoothAudioRoute(bluetoothHeadsetMonitor.isHeadsetAvailable());
185
+            if (bluetoothHeadsetMonitor.isHeadsetAvailable()) {
186
+                useSpeaker = false;
187
+            }
188
+        }
189
+
190
+        // XXX: isWiredHeadsetOn is not deprecated when used just for knowing if there is a wired
191
+        // headset connected, regardless of audio being routed to it.
192
+        audioManager.setSpeakerphoneOn(useSpeaker &&
193
+                !(audioManager.isWiredHeadsetOn() || audioManager.isBluetoothScoOn()));
194
+
195
+        return true;
196
+    }
197
+
198
+    /**
199
+     * Public method to set the current audio mode.
200
+     *
201
+     * @param mode the desired audio mode.
202
+     * @param promise a {@link Promise} which will be resolved if the audio mode could be updated
203
+     *                successfully, and it will be rejected otherwise.
204
+     */
205
+    @ReactMethod
206
+    public void setMode(final int mode, final Promise promise) {
207
+        if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) {
208
+            promise.reject("setMode", "Invalid audio mode " + mode);
209
+            return;
210
+        }
211
+
212
+        Runnable r = new Runnable() {
213
+            @Override
214
+            public void run() {
215
+                if (updateAudioRoute(mode)) {
216
+                    AudioModeModule.this.mode = mode;
217
+                    promise.resolve(null);
218
+                } else {
219
+                    promise.reject("setMode", "Failed to set the requested audio mode");
220
+                }
221
+            }
222
+        };
223
+        mainThreadHandler.post(r);
224
+    }
225
+
226
+    /**
227
+     * Setup the audio route change detection mechanism. We use the
228
+     * {@link android.media.AudioDeviceCallback} API on Android >= 23 only.
229
+     */
230
+    private void setupAudioRouteChangeDetection() {
231
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
232
+            setupAudioRouteChangeDetectionM();
233
+        } else {
234
+            setupAudioRouteChangeDetectionOld();
235
+        }
236
+    }
237
+
238
+    /**
239
+     * Audio route change detection mechanism for Android API >= 23.
240
+     */
241
+    @TargetApi(Build.VERSION_CODES.M)
242
+    private void setupAudioRouteChangeDetectionM() {
243
+        android.media.AudioDeviceCallback audioDeviceCallback =
244
+                new android.media.AudioDeviceCallback() {
245
+                    @Override
246
+                    public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
247
+                        Log.d(TAG, "Audio devices added");
248
+                        onAudioDeviceChange();
249
+                    }
250
+
251
+                    @Override
252
+                    public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
253
+                        Log.d(TAG, "Audio devices removed");
254
+                        onAudioDeviceChange();
255
+                    }
256
+                };
257
+
258
+        audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
259
+    }
260
+
261
+    /**
262
+     * Audio route change detection mechanism for Android API < 23.
263
+     */
264
+    private void setupAudioRouteChangeDetectionOld() {
265
+        ReactContext reactContext = getReactApplicationContext();
266
+
267
+        // Detect changes in wired headset connections
268
+        IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
269
+        BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
270
+            @Override
271
+            public void onReceive(Context context, Intent intent) {
272
+                Log.d(TAG, "Wired headset added / removed");
273
+                onAudioDeviceChange();
274
+            }
275
+        };
276
+        reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
277
+
278
+        // Detect Bluetooth device changes
279
+        bluetoothHeadsetMonitor =
280
+                new BluetoothHeadsetMonitor(this, this.getReactApplicationContext());
281
+        bluetoothHeadsetMonitor.start();
282
+    }
283
+
284
+    /**
285
+     * Helper method to set the output route to a Bluetooth device.
286
+     * @param enabled true if Bluetooth should use used, false otherwise.
287
+     */
288
+    private void setBluetoothAudioRoute(boolean enabled) {
289
+        if (enabled) {
290
+            audioManager.startBluetoothSco();
291
+            audioManager.setBluetoothScoOn(true);
292
+        } else {
293
+            audioManager.setBluetoothScoOn(false);
294
+            audioManager.stopBluetoothSco();
295
+        }
296
+    }
297
+
298
+    /**
299
+     * Helper method to trigger an audio route update when devices change.  It makes sure the
300
+     * operation is performed on the main thread.
301
+     */
302
+    void onAudioDeviceChange() {
303
+        mainThreadHandler.post(mainThreadRunner);
304
+    }
305
+}

+ 41
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java Näytä tiedosto

@@ -0,0 +1,41 @@
1
+package org.jitsi.meet.audiomode;
2
+
3
+import com.facebook.react.ReactPackage;
4
+import com.facebook.react.bridge.JavaScriptModule;
5
+import com.facebook.react.bridge.NativeModule;
6
+import com.facebook.react.bridge.ReactApplicationContext;
7
+import com.facebook.react.uimanager.ViewManager;
8
+
9
+import java.util.ArrayList;
10
+import java.util.Collections;
11
+import java.util.List;
12
+
13
+public class AudioModePackage implements ReactPackage {
14
+    /**
15
+     * {@inheritDoc}
16
+     * @return List of native modules to be exposed by React Native.
17
+     */
18
+    @Override
19
+    public List<NativeModule> createNativeModules(
20
+                                ReactApplicationContext reactContext) {
21
+        List<NativeModule> modules = new ArrayList<>();
22
+        modules.add(new AudioModeModule(reactContext));
23
+        return modules;
24
+    }
25
+
26
+    /**
27
+     * {@inheritDoc}
28
+     */
29
+    @Override
30
+    public List<Class<? extends JavaScriptModule>> createJSModules() {
31
+        return Collections.emptyList();
32
+    }
33
+
34
+    /**
35
+     * {@inheritDoc}
36
+     */
37
+    @Override
38
+    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
39
+        return Collections.emptyList();
40
+    }
41
+}

+ 192
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java Näytä tiedosto

@@ -0,0 +1,192 @@
1
+package org.jitsi.meet.audiomode;
2
+
3
+import android.bluetooth.BluetoothAdapter;
4
+import android.bluetooth.BluetoothDevice;
5
+import android.bluetooth.BluetoothHeadset;
6
+import android.bluetooth.BluetoothProfile;
7
+import android.content.BroadcastReceiver;
8
+import android.content.Context;
9
+import android.content.Intent;
10
+import android.content.IntentFilter;
11
+import android.media.AudioManager;
12
+import android.os.Handler;
13
+import android.os.Looper;
14
+import android.util.Log;
15
+
16
+import com.facebook.react.bridge.ReactContext;
17
+
18
+import java.util.List;
19
+
20
+/**
21
+ * Helper class to detect and handle Bluetooth device changes.  It monitors Bluetooth headsets
22
+ * being connected / disconnected and notifies the module about device changes when this occurs.
23
+ */
24
+public class BluetoothHeadsetMonitor {
25
+    /**
26
+     * {@link AudioModeModule} where this monitor reports.
27
+     */
28
+    private final AudioModeModule AudioModeModule;
29
+
30
+    /**
31
+     * {@link AudioManager} instance used to interact with the Android audio subsystem.
32
+     */
33
+    private final AudioManager audioManager;
34
+
35
+    /**
36
+     * {@link ReactContext} instance where the main module runs.
37
+     */
38
+    private final ReactContext reactContext;
39
+
40
+    /**
41
+     * Reference to the Bluetooth adapter, needed for managing <tt>BluetoothProfile.HEADSET</tt>
42
+     * devices.
43
+     */
44
+    private BluetoothAdapter bluetoothAdapter;
45
+
46
+    /**
47
+     * Reference to a proxy object which allows us to query connected devices.
48
+     */
49
+    private BluetoothHeadset bluetoothHeadset;
50
+
51
+    /**
52
+     * Listener for Bluetooth service profiles, allows us to get the proxy object to
53
+     * {@link BluetoothHeadset}.
54
+     */
55
+    private BluetoothProfile.ServiceListener bluetoothProfileListener;
56
+
57
+    /**
58
+     * {@link Handler} for running all operations on the main thread.
59
+     */
60
+    private final Handler mainThreadHandler;
61
+
62
+    /**
63
+     * Helper for running Bluetooth operations on the main thread.
64
+     */
65
+    private Runnable bluetoothRunnable;
66
+
67
+    /**
68
+     * Flag indicating if there are any Bluetooth headset devices currently available.
69
+     */
70
+    private boolean headsetAvailable = false;
71
+
72
+    public BluetoothHeadsetMonitor(AudioModeModule audioModeModule, ReactContext reactContext) {
73
+        this.AudioModeModule = audioModeModule;
74
+        this.reactContext = reactContext;
75
+
76
+        audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE));
77
+        bluetoothAdapter = null;
78
+        bluetoothHeadset = null;
79
+        bluetoothProfileListener = null;
80
+        mainThreadHandler = new Handler(Looper.getMainLooper());
81
+    }
82
+
83
+    /**
84
+     * Start monitoring Bluetooth device activity.
85
+     */
86
+    public void start() {
87
+        bluetoothRunnable = new Runnable() {
88
+            @Override
89
+            public void run() {
90
+                if (bluetoothHeadset == null) {
91
+                    headsetAvailable = false;
92
+                } else {
93
+                    List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
94
+                    headsetAvailable = !devices.isEmpty();
95
+                }
96
+                BluetoothHeadsetMonitor.this.AudioModeModule.onAudioDeviceChange();
97
+            }
98
+        };
99
+
100
+        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
101
+        if (bluetoothAdapter == null) {
102
+            Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth");
103
+            return;
104
+        }
105
+
106
+        if (!audioManager.isBluetoothScoAvailableOffCall()) {
107
+            Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available");
108
+            return;
109
+        }
110
+
111
+        // XXX: The profile listener listens for system services of the given type being available
112
+        // to the application.  That is, if our Bluetooth adapter has the "headset" profile.
113
+        bluetoothProfileListener = new BluetoothProfile.ServiceListener() {
114
+            @Override
115
+            public void onServiceConnected(int profile, BluetoothProfile proxy) {
116
+                if (profile == BluetoothProfile.HEADSET) {
117
+                    bluetoothHeadset = (BluetoothHeadset) proxy;
118
+                    updateDevices();
119
+                }
120
+            }
121
+
122
+            @Override
123
+            public void onServiceDisconnected(int profile) {
124
+                if (profile == BluetoothProfile.HEADSET) {
125
+                    bluetoothHeadset = null;
126
+                    updateDevices();
127
+                }
128
+            }
129
+        };
130
+
131
+        bluetoothAdapter.getProfileProxy(reactContext,
132
+                bluetoothProfileListener, BluetoothProfile.HEADSET);
133
+
134
+        IntentFilter bluetoothFilter = new IntentFilter();
135
+        bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
136
+        bluetoothFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
137
+        BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
138
+            @Override
139
+            public void onReceive(Context context, Intent intent) {
140
+                final String action = intent.getAction();
141
+                if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
142
+                    // XXX: This action will be fired when a Bluetooth headset is connected or
143
+                    // disconnected to the system.  This is not related to audio routing.
144
+                    final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99);
145
+                    switch (state) {
146
+                    case BluetoothHeadset.STATE_CONNECTED:
147
+                    case BluetoothHeadset.STATE_DISCONNECTED:
148
+                        Log.d(AudioModeModule.TAG, "BT headset connection state changed: " + state);
149
+                        updateDevices();
150
+                        break;
151
+                    default:
152
+                        break;
153
+                    }
154
+                } else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) {
155
+                    // XXX: This action will be fired when the connection established with a
156
+                    // Bluetooth headset (called a SCO connection) changes state.  When the SCO
157
+                    // connection is active we route audio to it.
158
+                    final int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
159
+                    switch (state) {
160
+                    case AudioManager.SCO_AUDIO_STATE_CONNECTED:
161
+                    case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
162
+                        Log.d(AudioModeModule.TAG, "BT SCO connection state changed: " + state);
163
+                        updateDevices();
164
+                        break;
165
+                    default:
166
+                        break;
167
+                    }
168
+                }
169
+            }
170
+        };
171
+        reactContext.registerReceiver(bluetoothReceiver, bluetoothFilter);
172
+
173
+        // Initial detection
174
+        updateDevices();
175
+    }
176
+
177
+    /**
178
+     * Detect if there are new devices connected / disconnected and fires the
179
+     * <tt>onAudioDeviceChange</tt> callback.
180
+     */
181
+    private void updateDevices() {
182
+        mainThreadHandler.post(bluetoothRunnable);
183
+    }
184
+
185
+    /**
186
+     * Returns the current headset availability.
187
+     * @return true if there is a Bluetooth headset connected, false otherwise.
188
+     */
189
+    public boolean isHeadsetAvailable() {
190
+        return headsetAvailable;
191
+    }
192
+}

Loading…
Peruuta
Tallenna