Browse Source

Merge branch 'saghul-android-audiomode'

master
Lyubomir Marinov 8 years ago
parent
commit
c91bffa73c

+ 6
- 3
android/app/src/main/AndroidManifest.xml View File

@@ -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 required by 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
 
@@ -13,8 +16,8 @@
13 16
     <uses-feature android:name="android.hardware.camera.autofocus"/>
14 17
 
15 18
     <uses-sdk
16
-        android:minSdkVersion="16"
17
-        android:targetSdkVersion="22" />
19
+        android:minSdkVersion="19"
20
+        android:targetSdkVersion="23" />
18 21
 
19 22
     <application
20 23
       android:allowBackup="true"

+ 2
- 1
android/app/src/main/java/org/jitsi/meet/MainApplication.java View File

@@ -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
     };

+ 322
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/AudioModeModule.java View File

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

+ 48
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/AudioModePackage.java View File

@@ -0,0 +1,48 @@
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
+/**
14
+ * Implements {@link ReactPackage} for {@link AudioModeModule}.
15
+ */
16
+public class AudioModePackage implements ReactPackage {
17
+    /**
18
+     * {@inheritDoc}
19
+     */
20
+    @Override
21
+    public List<Class<? extends JavaScriptModule>> createJSModules() {
22
+        return Collections.emptyList();
23
+    }
24
+
25
+    /**
26
+     * {@inheritDoc}
27
+     *
28
+     * @return List of native modules to be exposed by React Native.
29
+     */
30
+    @Override
31
+    public List<NativeModule> createNativeModules(
32
+            ReactApplicationContext reactContext) {
33
+        List<NativeModule> modules = new ArrayList<>();
34
+
35
+        modules.add(new AudioModeModule(reactContext));
36
+
37
+        return modules;
38
+    }
39
+
40
+    /**
41
+     * {@inheritDoc}
42
+     */
43
+    @Override
44
+    public List<ViewManager> createViewManagers(
45
+            ReactApplicationContext reactContext) {
46
+        return Collections.emptyList();
47
+    }
48
+}

+ 189
- 0
android/app/src/main/java/org/jitsi/meet/audiomode/BluetoothHeadsetMonitor.java View File

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

Loading…
Cancel
Save