Bläddra i källkod

rn,android: refactor audio device handling module

Separate each implementation (3 as of this writing) into each own "handler"
class.

This should make the code easier to understand, maintain and extend.
master
Saúl Ibarra Corretgé 6 år sedan
förälder
incheckning
3c22cd8ef4

+ 164
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioDeviceHandlerConnectionService.java Visa fil

@@ -0,0 +1,164 @@
1
+/*
2
+ * Copyright @ 2017-present 8x8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package org.jitsi.meet.sdk;
18
+
19
+import android.content.Context;
20
+import android.os.Build;
21
+import androidx.annotation.RequiresApi;
22
+
23
+import java.util.HashSet;
24
+import java.util.Set;
25
+
26
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
27
+
28
+
29
+/**
30
+ * {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
31
+ * Android versions >= O when ConnectionService is enabled.
32
+ */
33
+@RequiresApi(Build.VERSION_CODES.O)
34
+class AudioDeviceHandlerConnectionService implements
35
+        AudioModeModule.AudioDeviceHandlerInterface,
36
+        RNConnectionService.CallAudioStateListener {
37
+
38
+    private final static String TAG = AudioDeviceHandlerConnectionService.class.getSimpleName();
39
+
40
+    /**
41
+     * Reference to the main {@code AudioModeModule}.
42
+     */
43
+    private AudioModeModule module;
44
+
45
+    /**
46
+     * Converts any of the "DEVICE_" constants into the corresponding
47
+     * {@link android.telecom.CallAudioState} "ROUTE_" number.
48
+     *
49
+     * @param audioDevice one of the "DEVICE_" constants.
50
+     * @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
51
+     * no match is found.
52
+     */
53
+    private static int audioDeviceToRouteInt(String audioDevice) {
54
+        if (audioDevice == null) {
55
+            return android.telecom.CallAudioState.ROUTE_EARPIECE;
56
+        }
57
+        switch (audioDevice) {
58
+            case AudioModeModule.DEVICE_BLUETOOTH:
59
+                return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
60
+            case AudioModeModule.DEVICE_EARPIECE:
61
+                return android.telecom.CallAudioState.ROUTE_EARPIECE;
62
+            case AudioModeModule.DEVICE_HEADPHONES:
63
+                return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
64
+            case AudioModeModule.DEVICE_SPEAKER:
65
+                return android.telecom.CallAudioState.ROUTE_SPEAKER;
66
+            default:
67
+                JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
68
+                return android.telecom.CallAudioState.ROUTE_EARPIECE;
69
+        }
70
+    }
71
+
72
+    /**
73
+     * Populates given route mask into the "DEVICE_" list.
74
+     *
75
+     * @param supportedRouteMask an integer coming from
76
+     * {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
77
+     * @return a list of device names.
78
+     */
79
+    private static Set<String> routesToDeviceNames(int supportedRouteMask) {
80
+        Set<String> devices = new HashSet<>();
81
+        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
82
+                == android.telecom.CallAudioState.ROUTE_EARPIECE) {
83
+            devices.add(AudioModeModule.DEVICE_EARPIECE);
84
+        }
85
+        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
86
+                == android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
87
+            devices.add(AudioModeModule.DEVICE_BLUETOOTH);
88
+        }
89
+        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
90
+                == android.telecom.CallAudioState.ROUTE_SPEAKER) {
91
+            devices.add(AudioModeModule.DEVICE_SPEAKER);
92
+        }
93
+        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
94
+                == android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
95
+            devices.add(AudioModeModule.DEVICE_HEADPHONES);
96
+        }
97
+        return devices;
98
+    }
99
+
100
+    /**
101
+     * Used to store the most recently reported audio devices.
102
+     * Makes it easier to compare for a change, because the devices are stored
103
+     * as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
104
+     * the {@code availableDevices} on each update.
105
+     */
106
+    private int supportedRouteMask = -1;
107
+
108
+    public AudioDeviceHandlerConnectionService() {
109
+    }
110
+
111
+    @Override
112
+    public void onCallAudioStateChange(final android.telecom.CallAudioState callAudioState) {
113
+        module.runInAudioThread(new Runnable() {
114
+            @Override
115
+            public void run() {
116
+                boolean audioRouteChanged
117
+                    = audioDeviceToRouteInt(module.getSelectedDevice()) != callAudioState.getRoute();
118
+                int newSupportedRoutes = callAudioState.getSupportedRouteMask();
119
+                boolean audioDevicesChanged = supportedRouteMask != newSupportedRoutes;
120
+                if (audioDevicesChanged) {
121
+                    supportedRouteMask = newSupportedRoutes;
122
+                    Set<String> devices = routesToDeviceNames(supportedRouteMask);
123
+                    module.replaceDevices(devices);
124
+                    JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
125
+                }
126
+
127
+                if (audioRouteChanged || audioDevicesChanged) {
128
+                    module.resetSelectedDevice();
129
+                    module.updateAudioRoute();
130
+                }
131
+            }
132
+        });
133
+    }
134
+
135
+    @Override
136
+    public void start(Context context, AudioModeModule audioModeModule) {
137
+        JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
138
+
139
+        module = audioModeModule;
140
+        RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
141
+        if (rcs != null) {
142
+            rcs.setCallAudioStateListener(this);
143
+        }
144
+    }
145
+
146
+    @Override
147
+    public void stop() {
148
+        RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
149
+        if (rcs != null) {
150
+            rcs.setCallAudioStateListener(null);
151
+        }
152
+    }
153
+
154
+    public void setAudioRoute(String audioDevice) {
155
+        int newAudioRoute = audioDeviceToRouteInt(audioDevice);
156
+
157
+        RNConnectionService.setAudioRoute(newAudioRoute);
158
+    }
159
+
160
+    @Override
161
+    public boolean setMode(int mode) {
162
+        return true;
163
+    }
164
+}

+ 231
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioDeviceHandlerGeneric.java Visa fil

@@ -0,0 +1,231 @@
1
+/*
2
+ * Copyright @ 2017-present 8x8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package org.jitsi.meet.sdk;
18
+
19
+import android.content.Context;
20
+import android.media.AudioDeviceInfo;
21
+import android.media.AudioManager;
22
+import android.os.Build;
23
+import androidx.annotation.RequiresApi;
24
+
25
+import java.util.HashSet;
26
+import java.util.Set;
27
+
28
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
29
+
30
+
31
+/**
32
+ * {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
33
+ * all post-M Android versions. This handler can be used on any Android versions >= M, but by
34
+ * default it's only used on versions < O, since versions >= O use ConnectionService, but it
35
+ * can be disabled.
36
+ */
37
+@RequiresApi(Build.VERSION_CODES.M)
38
+class AudioDeviceHandlerGeneric implements
39
+        AudioModeModule.AudioDeviceHandlerInterface,
40
+        AudioManager.OnAudioFocusChangeListener {
41
+
42
+    private final static String TAG = AudioDeviceHandlerGeneric.class.getSimpleName();
43
+
44
+    /**
45
+     * Reference to the main {@code AudioModeModule}.
46
+     */
47
+    private AudioModeModule module;
48
+
49
+    /**
50
+     * Constant defining a USB headset. Only available on API level >= 26.
51
+     * The value of: AudioDeviceInfo.TYPE_USB_HEADSET
52
+     */
53
+    private static final int TYPE_USB_HEADSET = 22;
54
+
55
+    /**
56
+     * Indicator that we have lost audio focus.
57
+     */
58
+    private boolean audioFocusLost = false;
59
+
60
+    /**
61
+     * {@link AudioManager} instance used to interact with the Android audio
62
+     * subsystem.
63
+     */
64
+    private AudioManager audioManager;
65
+
66
+    /**
67
+     * {@link Runnable} for running audio device detection the main thread.
68
+     * This is only used on Android >= M.
69
+     */
70
+    private final Runnable onAudioDeviceChangeRunner = new Runnable() {
71
+        @Override
72
+        public void run() {
73
+            Set<String> devices = new HashSet<>();
74
+            AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
75
+
76
+            for (AudioDeviceInfo info: deviceInfos) {
77
+                switch (info.getType()) {
78
+                    case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
79
+                        devices.add(AudioModeModule.DEVICE_BLUETOOTH);
80
+                        break;
81
+                    case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
82
+                        devices.add(AudioModeModule.DEVICE_EARPIECE);
83
+                        break;
84
+                    case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
85
+                        devices.add(AudioModeModule.DEVICE_SPEAKER);
86
+                        break;
87
+                    case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
88
+                    case AudioDeviceInfo.TYPE_WIRED_HEADSET:
89
+                    case TYPE_USB_HEADSET:
90
+                        devices.add(AudioModeModule.DEVICE_HEADPHONES);
91
+                        break;
92
+                }
93
+            }
94
+
95
+            module.replaceDevices(devices);
96
+
97
+            JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
98
+
99
+            module.updateAudioRoute();
100
+        }
101
+    };
102
+
103
+    private final android.media.AudioDeviceCallback audioDeviceCallback =
104
+        new android.media.AudioDeviceCallback() {
105
+            @Override
106
+            public void onAudioDevicesAdded(
107
+                AudioDeviceInfo[] addedDevices) {
108
+                JitsiMeetLogger.d(TAG + " Audio devices added");
109
+                onAudioDeviceChange();
110
+            }
111
+
112
+            @Override
113
+            public void onAudioDevicesRemoved(
114
+                AudioDeviceInfo[] removedDevices) {
115
+                JitsiMeetLogger.d(TAG + " Audio devices removed");
116
+                onAudioDeviceChange();
117
+            }
118
+        };
119
+
120
+    public AudioDeviceHandlerGeneric() {
121
+    }
122
+
123
+    /**
124
+     * Helper method to trigger an audio route update when devices change. It
125
+     * makes sure the operation is performed on the audio thread.
126
+     */
127
+    private void onAudioDeviceChange() {
128
+        module.runInAudioThread(onAudioDeviceChangeRunner);
129
+    }
130
+
131
+    /**
132
+     * {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
133
+     * when the audio focus of the system is updated.
134
+     *
135
+     * @param focusChange - The type of focus change.
136
+     */
137
+    @Override
138
+    public void onAudioFocusChange(final int focusChange) {
139
+        module.runInAudioThread(new Runnable() {
140
+            @Override
141
+            public void run() {
142
+                switch (focusChange) {
143
+                    case AudioManager.AUDIOFOCUS_GAIN: {
144
+                        JitsiMeetLogger.d(TAG + " Audio focus gained");
145
+                        // Some other application potentially stole our audio focus
146
+                        // temporarily. Restore our mode.
147
+                        if (audioFocusLost) {
148
+                            module.updateAudioRoute();
149
+                        }
150
+                        audioFocusLost = false;
151
+                        break;
152
+                    }
153
+                    case AudioManager.AUDIOFOCUS_LOSS:
154
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
155
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
156
+                        JitsiMeetLogger.d(TAG + " Audio focus lost");
157
+                        audioFocusLost = true;
158
+                        break;
159
+                    }
160
+                }
161
+            }
162
+        });
163
+    }
164
+
165
+    /**
166
+     * Helper method to set the output route to a Bluetooth device.
167
+     *
168
+     * @param enabled true if Bluetooth should use used, false otherwise.
169
+     */
170
+    private void setBluetoothAudioRoute(boolean enabled) {
171
+        if (enabled) {
172
+            audioManager.startBluetoothSco();
173
+            audioManager.setBluetoothScoOn(true);
174
+        } else {
175
+            audioManager.setBluetoothScoOn(false);
176
+            audioManager.stopBluetoothSco();
177
+        }
178
+    }
179
+
180
+    @Override
181
+    public void start(Context context, AudioModeModule audioModeModule) {
182
+        JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
183
+
184
+        module = audioModeModule;
185
+        audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
186
+
187
+        // Setup runtime device change detection.
188
+        audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
189
+
190
+        // Do an initial detection.
191
+        onAudioDeviceChange();
192
+    }
193
+
194
+    @Override
195
+    public void stop() {
196
+        audioManager.unregisterAudioDeviceCallback(audioDeviceCallback);
197
+    }
198
+
199
+    @Override
200
+    public void setAudioRoute(String device) {
201
+        // Turn bluetooth on / off
202
+        setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
203
+
204
+        // Turn speaker on / off
205
+        audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
206
+    }
207
+
208
+    @Override
209
+    public boolean setMode(int mode) {
210
+        if (mode == AudioModeModule.DEFAULT) {
211
+            audioFocusLost = false;
212
+            audioManager.setMode(AudioManager.MODE_NORMAL);
213
+            audioManager.abandonAudioFocus(this);
214
+            audioManager.setSpeakerphoneOn(false);
215
+            setBluetoothAudioRoute(false);
216
+
217
+            return true;
218
+        }
219
+
220
+        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
221
+        audioManager.setMicrophoneMute(false);
222
+
223
+        if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
224
+            == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
225
+            JitsiMeetLogger.w(TAG + " Audio focus request failed");
226
+            return false;
227
+        }
228
+
229
+        return true;
230
+    }
231
+}

+ 229
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioDeviceHandlerLegacy.java Visa fil

@@ -0,0 +1,229 @@
1
+/*
2
+ * Copyright @ 2017-present 8x8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package org.jitsi.meet.sdk;
18
+
19
+import android.content.BroadcastReceiver;
20
+import android.content.Context;
21
+import android.content.Intent;
22
+import android.content.IntentFilter;
23
+import android.content.pm.PackageManager;
24
+import android.media.AudioManager;
25
+
26
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
27
+
28
+
29
+/**
30
+ * {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
31
+ * legacy (pre-M) Android versions.
32
+ */
33
+class AudioDeviceHandlerLegacy implements
34
+        AudioModeModule.AudioDeviceHandlerInterface,
35
+        AudioManager.OnAudioFocusChangeListener,
36
+        BluetoothHeadsetMonitor.Listener {
37
+
38
+    private final static String TAG = AudioDeviceHandlerLegacy.class.getSimpleName();
39
+
40
+    /**
41
+     * Reference to the main {@code AudioModeModule}.
42
+     */
43
+    private AudioModeModule module;
44
+
45
+    /**
46
+     * Indicator that we have lost audio focus.
47
+     */
48
+    private boolean audioFocusLost = false;
49
+
50
+    /**
51
+     * {@link AudioManager} instance used to interact with the Android audio
52
+     * subsystem.
53
+     */
54
+    private AudioManager audioManager;
55
+
56
+    /**
57
+     * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
58
+     * old (< M) Android versions.
59
+     */
60
+    private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
61
+
62
+    public AudioDeviceHandlerLegacy() {
63
+    }
64
+
65
+    /**
66
+     * Helper method to trigger an audio route update when Bluetooth devices are
67
+     * connected / disconnected.
68
+     */
69
+    @Override
70
+    public void onBluetoothDeviceChange(final boolean deviceAvailable) {
71
+        module.runInAudioThread(new Runnable() {
72
+            @Override
73
+            public void run() {
74
+                if (deviceAvailable) {
75
+                    module.addDevice(AudioModeModule.DEVICE_BLUETOOTH);
76
+                } else {
77
+                    module.removeDevice(AudioModeModule.DEVICE_BLUETOOTH);
78
+                }
79
+
80
+                module.updateAudioRoute();
81
+            }
82
+        });
83
+    }
84
+
85
+    /**
86
+     * Helper method to trigger an audio route update when a headset is plugged
87
+     * or unplugged.
88
+     */
89
+    private void onHeadsetDeviceChange() {
90
+        module.runInAudioThread(new Runnable() {
91
+            @Override
92
+            public void run() {
93
+                // XXX: isWiredHeadsetOn is not deprecated when used just for
94
+                // knowing if there is a wired headset connected, regardless of
95
+                // audio being routed to it.
96
+                //noinspection deprecation
97
+                if (audioManager.isWiredHeadsetOn()) {
98
+                    module.addDevice(AudioModeModule.DEVICE_HEADPHONES);
99
+                } else {
100
+                    module.removeDevice(AudioModeModule.DEVICE_HEADPHONES);
101
+                }
102
+
103
+                module.updateAudioRoute();
104
+            }
105
+        });
106
+    }
107
+
108
+    /**
109
+     * {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
110
+     * when the audio focus of the system is updated.
111
+     *
112
+     * @param focusChange - The type of focus change.
113
+     */
114
+    @Override
115
+    public void onAudioFocusChange(final int focusChange) {
116
+        module.runInAudioThread(new Runnable() {
117
+            @Override
118
+            public void run() {
119
+                switch (focusChange) {
120
+                    case AudioManager.AUDIOFOCUS_GAIN: {
121
+                        JitsiMeetLogger.d(TAG + " Audio focus gained");
122
+                        // Some other application potentially stole our audio focus
123
+                        // temporarily. Restore our mode.
124
+                        if (audioFocusLost) {
125
+                            module.updateAudioRoute();
126
+                        }
127
+                        audioFocusLost = false;
128
+                        break;
129
+                    }
130
+                    case AudioManager.AUDIOFOCUS_LOSS:
131
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
132
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
133
+                        JitsiMeetLogger.d(TAG + " Audio focus lost");
134
+                        audioFocusLost = true;
135
+                        break;
136
+                    }
137
+                }
138
+            }
139
+        });
140
+    }
141
+
142
+    /**
143
+     * Helper method to set the output route to a Bluetooth device.
144
+     *
145
+     * @param enabled true if Bluetooth should use used, false otherwise.
146
+     */
147
+    private void setBluetoothAudioRoute(boolean enabled) {
148
+        if (enabled) {
149
+            audioManager.startBluetoothSco();
150
+            audioManager.setBluetoothScoOn(true);
151
+        } else {
152
+            audioManager.setBluetoothScoOn(false);
153
+            audioManager.stopBluetoothSco();
154
+        }
155
+    }
156
+
157
+    @Override
158
+    public void start(Context context, AudioModeModule audioModeModule) {
159
+        JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
160
+
161
+        module = audioModeModule;
162
+        audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
163
+
164
+        // Setup runtime device change detection.
165
+        //
166
+
167
+        // Detect changes in wired headset connections.
168
+        IntentFilter wiredHeadSetFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG);
169
+        BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
170
+            @Override
171
+            public void onReceive(Context context, Intent intent) {
172
+                JitsiMeetLogger.d(TAG + " Wired headset added / removed");
173
+                onHeadsetDeviceChange();
174
+            }
175
+        };
176
+        context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
177
+
178
+        // Detect Bluetooth device changes.
179
+        bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(context, this);
180
+
181
+        // On Android < M, detect if we have an earpiece.
182
+        PackageManager pm = context.getPackageManager();
183
+        if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
184
+            module.addDevice(AudioModeModule.DEVICE_EARPIECE);
185
+        }
186
+
187
+        // Always assume there is a speaker.
188
+        module.addDevice(AudioModeModule.DEVICE_SPEAKER);
189
+    }
190
+
191
+    @Override
192
+    public void stop() {
193
+        bluetoothHeadsetMonitor.stop();
194
+        bluetoothHeadsetMonitor = null;
195
+    }
196
+
197
+    @Override
198
+    public void setAudioRoute(String device) {
199
+        // Turn bluetooth on / off
200
+        setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
201
+
202
+        // Turn speaker on / off
203
+        audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
204
+    }
205
+
206
+    @Override
207
+    public boolean setMode(int mode) {
208
+        if (mode == AudioModeModule.DEFAULT) {
209
+            audioFocusLost = false;
210
+            audioManager.setMode(AudioManager.MODE_NORMAL);
211
+            audioManager.abandonAudioFocus(this);
212
+            audioManager.setSpeakerphoneOn(false);
213
+            setBluetoothAudioRoute(false);
214
+
215
+            return true;
216
+        }
217
+
218
+        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
219
+        audioManager.setMicrophoneMute(false);
220
+
221
+        if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
222
+                == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
223
+            JitsiMeetLogger.w(TAG + " Audio focus request failed");
224
+            return false;
225
+        }
226
+
227
+        return true;
228
+    }
229
+}

+ 130
- 428
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java Visa fil

@@ -1,5 +1,5 @@
1 1
 /*
2
- * Copyright @ 2017-present Atlassian Pty Ltd
2
+ * Copyright @ 2017-present 8x8, Inc.
3 3
  *
4 4
  * Licensed under the Apache License, Version 2.0 (the "License");
5 5
  * you may not use this file except in compliance with the License.
@@ -16,16 +16,8 @@
16 16
 
17 17
 package org.jitsi.meet.sdk;
18 18
 
19
-import android.annotation.TargetApi;
20
-import android.content.BroadcastReceiver;
21 19
 import android.content.Context;
22
-import android.content.Intent;
23
-import android.content.IntentFilter;
24
-import android.content.pm.PackageManager;
25
-import android.media.AudioDeviceInfo;
26
-import android.media.AudioManager;
27 20
 import android.os.Build;
28
-import androidx.annotation.RequiresApi;
29 21
 
30 22
 import com.facebook.react.bridge.Arguments;
31 23
 import com.facebook.react.bridge.Promise;
@@ -61,9 +53,7 @@ import java.util.concurrent.Executors;
61 53
  * {@code AudioModeModule.DEFAULT} mode should be used.
62 54
  */
63 55
 @ReactModule(name = AudioModeModule.NAME)
64
-class AudioModeModule extends ReactContextBaseJavaModule
65
-    implements AudioManager.OnAudioFocusChangeListener {
66
-
56
+class AudioModeModule extends ReactContextBaseJavaModule {
67 57
     public static final String NAME = "AudioMode";
68 58
 
69 59
     /**
@@ -75,24 +65,9 @@ class AudioModeModule extends ReactContextBaseJavaModule
75 65
      * - VIDEO_CALL: Used for video calls. It will use the speaker by default,
76 66
      *   unless a wired or Bluetooth headset is connected.
77 67
      */
78
-    private static final int DEFAULT    = 0;
79
-    private static final int AUDIO_CALL = 1;
80
-    private static final int VIDEO_CALL = 2;
81
-
82
-    /**
83
-     * Constant defining the action for plugging in a headset. This is used on
84
-     * our device detection system for API < 23.
85
-     */
86
-    private static final String ACTION_HEADSET_PLUG
87
-        = (Build.VERSION.SDK_INT >= 21)
88
-            ? AudioManager.ACTION_HEADSET_PLUG
89
-            : Intent.ACTION_HEADSET_PLUG;
90
-
91
-    /**
92
-     * Constant defining a USB headset. Only available on API level >= 26.
93
-     * The value of: AudioDeviceInfo.TYPE_USB_HEADSET
94
-     */
95
-    private static final int TYPE_USB_HEADSET = 22;
68
+    static final int DEFAULT    = 0;
69
+    static final int AUDIO_CALL = 1;
70
+    static final int VIDEO_CALL = 2;
96 71
 
97 72
     /**
98 73
      * The {@code Log} tag {@code AudioModeModule} is to log messages with.
@@ -100,136 +75,23 @@ class AudioModeModule extends ReactContextBaseJavaModule
100 75
     static final String TAG = NAME;
101 76
 
102 77
     /**
103
-     * Converts any of the "DEVICE_" constants into the corresponding
104
-     * {@link android.telecom.CallAudioState} "ROUTE_" number.
105
-     *
106
-     * @param audioDevice one of the "DEVICE_" constants.
107
-     * @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
108
-     * no match is found.
78
+     * Whether or not the ConnectionService is used for selecting audio devices.
109 79
      */
110
-    @RequiresApi(api = Build.VERSION_CODES.M)
111
-    private static int audioDeviceToRouteInt(String audioDevice) {
112
-        if (audioDevice == null) {
113
-            return android.telecom.CallAudioState.ROUTE_EARPIECE;
114
-        }
115
-        switch (audioDevice) {
116
-            case DEVICE_BLUETOOTH:
117
-                return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
118
-            case DEVICE_EARPIECE:
119
-                return android.telecom.CallAudioState.ROUTE_EARPIECE;
120
-            case DEVICE_HEADPHONES:
121
-                return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
122
-            case DEVICE_SPEAKER:
123
-                return android.telecom.CallAudioState.ROUTE_SPEAKER;
124
-            default:
125
-                JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
126
-                return android.telecom.CallAudioState.ROUTE_EARPIECE;
127
-        }
128
-    }
129 80
 
130
-    /**
131
-     * Populates given route mask into the "DEVICE_" list.
132
-     *
133
-     * @param supportedRouteMask an integer coming from
134
-     * {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
135
-     * @return a list of device names.
136
-     */
137
-    @RequiresApi(api = Build.VERSION_CODES.M)
138
-    private static Set<String> routesToDeviceNames(int supportedRouteMask) {
139
-        Set<String> devices = new HashSet<>();
140
-        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
141
-                == android.telecom.CallAudioState.ROUTE_EARPIECE) {
142
-            devices.add(DEVICE_EARPIECE);
143
-        }
144
-        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
145
-                == android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
146
-            devices.add(DEVICE_BLUETOOTH);
147
-        }
148
-        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
149
-                == android.telecom.CallAudioState.ROUTE_SPEAKER) {
150
-            devices.add(DEVICE_SPEAKER);
151
-        }
152
-        if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
153
-                == android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
154
-            devices.add(DEVICE_HEADPHONES);
155
-        }
156
-        return devices;
157
-    }
81
+    private static final boolean supportsConnectionService = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
82
+    private static boolean useConnectionService_ = supportsConnectionService;
158 83
 
159
-    /**
160
-     * Whether or not the ConnectionService is used for selecting audio devices.
161
-     */
162 84
     static boolean useConnectionService() {
163
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
85
+        return supportsConnectionService && useConnectionService_;
164 86
     }
165 87
 
166
-    /**
167
-     * Indicator that we have lost audio focus.
168
-     */
169
-    private boolean audioFocusLost = false;
170
-
171
-    /**
172
-     * {@link AudioManager} instance used to interact with the Android audio
173
-     * subsystem.
174
-     */
175
-    private final AudioManager audioManager;
176
-
177
-    /**
178
-     * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
179
-     * old (< M) Android versions.
180
-     */
181
-    private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
88
+    private AudioDeviceHandlerInterface audioDeviceHandler;
182 89
 
183 90
     /**
184 91
      * {@link ExecutorService} for running all audio operations on a dedicated
185 92
      * thread.
186 93
      */
187
-    private static final ExecutorService executor
188
-        = Executors.newSingleThreadExecutor();
189
-
190
-    /**
191
-     * {@link Runnable} for running audio device detection the main thread.
192
-     * This is only used on Android >= M.
193
-     */
194
-    private final Runnable onAudioDeviceChangeRunner = new Runnable() {
195
-        @TargetApi(Build.VERSION_CODES.M)
196
-        @Override
197
-        public void run() {
198
-            Set<String> devices = new HashSet<>();
199
-            AudioDeviceInfo[] deviceInfos
200
-                = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
201
-
202
-            for (AudioDeviceInfo info: deviceInfos) {
203
-                switch (info.getType()) {
204
-                case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
205
-                    devices.add(DEVICE_BLUETOOTH);
206
-                    break;
207
-                case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
208
-                    devices.add(DEVICE_EARPIECE);
209
-                    break;
210
-                case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
211
-                    devices.add(DEVICE_SPEAKER);
212
-                    break;
213
-                case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
214
-                case AudioDeviceInfo.TYPE_WIRED_HEADSET:
215
-                case TYPE_USB_HEADSET:
216
-                    devices.add(DEVICE_HEADPHONES);
217
-                    break;
218
-                }
219
-            }
220
-
221
-            availableDevices = devices;
222
-            JitsiMeetLogger.i(TAG + " Available audio devices: " +
223
-                availableDevices.toString());
224
-
225
-            // Reset user selection
226
-            userSelectedDevice = null;
227
-
228
-            if (mode != -1) {
229
-                updateAudioRoute(mode);
230
-            }
231
-        }
232
-    };
94
+    private static final ExecutorService executor = Executors.newSingleThreadExecutor();
233 95
 
234 96
     /**
235 97
      * Audio mode currently in use.
@@ -239,10 +101,10 @@ class AudioModeModule extends ReactContextBaseJavaModule
239 101
     /**
240 102
      * Audio device types.
241 103
      */
242
-    private static final String DEVICE_BLUETOOTH  = "BLUETOOTH";
243
-    private static final String DEVICE_EARPIECE   = "EARPIECE";
244
-    private static final String DEVICE_HEADPHONES = "HEADPHONES";
245
-    private static final String DEVICE_SPEAKER    = "SPEAKER";
104
+    static final String DEVICE_BLUETOOTH  = "BLUETOOTH";
105
+    static final String DEVICE_EARPIECE   = "EARPIECE";
106
+    static final String DEVICE_HEADPHONES = "HEADPHONES";
107
+    static final String DEVICE_SPEAKER    = "SPEAKER";
246 108
 
247 109
     /**
248 110
      * Device change event.
@@ -259,15 +121,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
259 121
      */
260 122
     private String selectedDevice;
261 123
 
262
-    /**
263
-     * Used on API >= 26 to store the most recently reported audio devices.
264
-     * Makes it easier to compare for a change, because the devices are stored
265
-     * as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
266
-     * the {@link #availableDevices} on each update.
267
-     */
268
-    @RequiresApi(api = Build.VERSION_CODES.O)
269
-    private int supportedRouteMask;
270
-
271 124
     /**
272 125
      * User selected device. When null the default is used depending on the
273 126
      * mode.
@@ -284,30 +137,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
284 137
     public AudioModeModule(ReactApplicationContext reactContext) {
285 138
         super(reactContext);
286 139
 
287
-        audioManager
288
-            = (AudioManager)
289
-                reactContext.getSystemService(Context.AUDIO_SERVICE);
290
-
291
-        // Starting Oreo the ConnectionImpl from ConnectionService is used to
292
-        // detect the available devices.
293
-        if (!useConnectionService()) {
294
-            // Setup runtime device change detection.
295
-            setupAudioRouteChangeDetection();
296
-
297
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
298
-                // Do an initial detection on Android >= M.
299
-                onAudioDeviceChange();
300
-            } else {
301
-                // On Android < M, detect if we have an earpiece.
302
-                PackageManager pm = reactContext.getPackageManager();
303
-                if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
304
-                    availableDevices.add(DEVICE_EARPIECE);
305
-                }
306
-
307
-                // Always assume there is a speaker.
308
-                availableDevices.add(DEVICE_SPEAKER);
309
-            }
310
-        }
140
+        setAudioDeviceHandler();
311 141
     }
312 142
 
313 143
     /**
@@ -363,133 +193,27 @@ class AudioModeModule extends ReactContextBaseJavaModule
363 193
         return NAME;
364 194
     }
365 195
 
366
-    /**
367
-     * Helper method to trigger an audio route update when devices change. It
368
-     * makes sure the operation is performed on the main thread.
369
-     *
370
-     * Only used on Android >= M.
371
-     */
372
-    void onAudioDeviceChange() {
373
-        runInAudioThread(onAudioDeviceChangeRunner);
374
-    }
375
-
376
-    /**
377
-     * Helper method to trigger an audio route update when Bluetooth devices are
378
-     * connected / disconnected.
379
-     *
380
-     * Only used on Android < M. Runs on the main thread.
381
-     */
382
-    void onBluetoothDeviceChange() {
383
-        if (bluetoothHeadsetMonitor != null && bluetoothHeadsetMonitor.isHeadsetAvailable()) {
384
-            availableDevices.add(DEVICE_BLUETOOTH);
385
-        } else {
386
-            availableDevices.remove(DEVICE_BLUETOOTH);
196
+    private void setAudioDeviceHandler() {
197
+        if (audioDeviceHandler != null) {
198
+            audioDeviceHandler.stop();
387 199
         }
388 200
 
389
-        if (mode != -1) {
390
-            updateAudioRoute(mode);
391
-        }
392
-    }
393
-
394
-    /**
395
-     * Helper method to trigger an audio route update when a headset is plugged
396
-     * or unplugged.
397
-     *
398
-     * Only used on Android < M.
399
-     */
400
-    void onHeadsetDeviceChange() {
401
-        runInAudioThread(new Runnable() {
402
-            @Override
403
-            public void run() {
404
-                // XXX: isWiredHeadsetOn is not deprecated when used just for
405
-                // knowing if there is a wired headset connected, regardless of
406
-                // audio being routed to it.
407
-                //noinspection deprecation
408
-                if (audioManager.isWiredHeadsetOn()) {
409
-                    availableDevices.add(DEVICE_HEADPHONES);
410
-                } else {
411
-                    availableDevices.remove(DEVICE_HEADPHONES);
412
-                }
413
-
414
-                if (mode != -1) {
415
-                    updateAudioRoute(mode);
416
-                }
417
-            }
418
-        });
419
-    }
420
-
421
-    @RequiresApi(api = Build.VERSION_CODES.O)
422
-    void onCallAudioStateChange(Object callAudioState_) {
423
-        final android.telecom.CallAudioState callAudioState
424
-            = (android.telecom.CallAudioState)callAudioState_;
425
-        runInAudioThread(new Runnable() {
426
-            @Override
427
-            public void run() {
428
-                int newSupportedRoutes = callAudioState.getSupportedRouteMask();
429
-                boolean audioDevicesChanged
430
-                        = supportedRouteMask != newSupportedRoutes;
431
-                if (audioDevicesChanged) {
432
-                    supportedRouteMask = newSupportedRoutes;
433
-                    availableDevices = routesToDeviceNames(supportedRouteMask);
434
-                    JitsiMeetLogger.i(TAG + " Available audio devices: "
435
-                                  + availableDevices.toString());
436
-                }
437
-
438
-                boolean audioRouteChanged
439
-                    = audioDeviceToRouteInt(selectedDevice)
440
-                            != callAudioState.getRoute();
441
-
442
-                if (audioRouteChanged || audioDevicesChanged) {
443
-                    // Reset user selection
444
-                    userSelectedDevice = null;
445
-
446
-                    // If the OS changes the Audio Route or Devices we could have lost
447
-                    // the selected audio device
448
-                    selectedDevice = null;
449
-
450
-                    if (mode != -1) {
451
-                        updateAudioRoute(mode);
452
-                    }
453
-                }
454
-            }
455
-        });
456
-    }
457
-
458
-    /**
459
-     * {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
460
-     * when the audio focus of the system is updated.
461
-     *
462
-     * @param focusChange - The type of focus change.
463
-     */
464
-    @Override
465
-    public void onAudioFocusChange(int focusChange) {
466
-        switch (focusChange) {
467
-        case AudioManager.AUDIOFOCUS_GAIN: {
468
-            JitsiMeetLogger.d(TAG + " Audio focus gained");
469
-            // Some other application potentially stole our audio focus
470
-            // temporarily. Restore our mode.
471
-            if (audioFocusLost) {
472
-                updateAudioRoute(mode);
473
-            }
474
-            audioFocusLost = false;
475
-            break;
476
-        }
477
-        case AudioManager.AUDIOFOCUS_LOSS:
478
-        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
479
-        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
480
-            JitsiMeetLogger.d(TAG + " Audio focus lost");
481
-            audioFocusLost = true;
482
-            break;
201
+        if (useConnectionService()) {
202
+            audioDeviceHandler = new AudioDeviceHandlerConnectionService();
203
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
204
+            audioDeviceHandler = new AudioDeviceHandlerGeneric();
205
+        } else {
206
+            audioDeviceHandler = new AudioDeviceHandlerLegacy();
483 207
         }
484 208
 
485
-        }
209
+        audioDeviceHandler.start(getReactApplicationContext(), this);
486 210
     }
487 211
 
488 212
     /**
489 213
      * Helper function to run operations on a dedicated thread.
490 214
      * @param runnable
491 215
      */
492
-    public void runInAudioThread(Runnable runnable) {
216
+    void runInAudioThread(Runnable runnable) {
493 217
         executor.execute(runnable);
494 218
     }
495 219
 
@@ -518,46 +242,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
518 242
         });
519 243
     }
520 244
 
521
-    /**
522
-     * The API >= 26 way of adjusting the audio route.
523
-     *
524
-     * @param audioDevice one of the "DEVICE_" names to set as the audio route.
525
-     */
526
-    @RequiresApi(api = Build.VERSION_CODES.O)
527
-    private void setAudioRoute(String audioDevice) {
528
-        int newAudioRoute = audioDeviceToRouteInt(audioDevice);
529
-
530
-        RNConnectionService.setAudioRoute(newAudioRoute);
531
-    }
532
-
533
-    /**
534
-     * The API < 26 way of adjusting the audio route.
535
-     *
536
-     * @param audioDevice one of the "DEVICE_" names to set as the audio route.
537
-     */
538
-    private void setAudioRoutePreO(String audioDevice) {
539
-        // Turn bluetooth on / off
540
-        setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
541
-
542
-        // Turn speaker on / off
543
-        audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
544
-    }
545
-
546
-    /**
547
-     * Helper method to set the output route to a Bluetooth device.
548
-     *
549
-     * @param enabled true if Bluetooth should use used, false otherwise.
550
-     */
551
-    private void setBluetoothAudioRoute(boolean enabled) {
552
-        if (enabled) {
553
-            audioManager.startBluetoothSco();
554
-            audioManager.setBluetoothScoOn(true);
555
-        } else {
556
-            audioManager.setBluetoothScoOn(false);
557
-            audioManager.stopBluetoothSco();
558
-        }
559
-    }
560
-
561 245
     /**
562 246
      * Public method to set the current audio mode.
563 247
      *
@@ -587,70 +271,22 @@ class AudioModeModule extends ReactContextBaseJavaModule
587 271
                     AudioModeModule.this.mode = mode;
588 272
                     promise.resolve(null);
589 273
                 } else {
590
-                    promise.reject(
591
-                            "setMode",
592
-                            "Failed to set audio mode to " + mode);
274
+                    promise.reject("setMode", "Failed to set audio mode to " + mode);
593 275
                 }
594 276
             }
595 277
         });
596 278
     }
597 279
 
598 280
     /**
599
-     * Setup the audio route change detection mechanism. We use the
600
-     * {@link android.media.AudioDeviceCallback} on 23 >= Android API < 26.
601
-     */
602
-    private void setupAudioRouteChangeDetection() {
603
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
604
-            setupAudioRouteChangeDetectionM();
605
-        } else {
606
-            setupAudioRouteChangeDetectionPreM();
607
-        }
608
-    }
609
-
610
-    /**
611
-     * Audio route change detection mechanism for 23 >= Android API < 26.
612
-     */
613
-    @TargetApi(Build.VERSION_CODES.M)
614
-    private void setupAudioRouteChangeDetectionM() {
615
-        android.media.AudioDeviceCallback audioDeviceCallback =
616
-                new android.media.AudioDeviceCallback() {
617
-                    @Override
618
-                    public void onAudioDevicesAdded(
619
-                            AudioDeviceInfo[] addedDevices) {
620
-                        JitsiMeetLogger.d(TAG + " Audio devices added");
621
-                        onAudioDeviceChange();
622
-                    }
623
-
624
-                    @Override
625
-                    public void onAudioDevicesRemoved(
626
-                            AudioDeviceInfo[] removedDevices) {
627
-                        JitsiMeetLogger.d(TAG + " Audio devices removed");
628
-                        onAudioDeviceChange();
629
-                    }
630
-                };
631
-
632
-        audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
633
-    }
634
-
635
-    /**
636
-     * Audio route change detection mechanism for Android API < 23.
281
+     * Sets whether ConnectionService should be used (if available) for setting the audio mode
282
+     * or not.
283
+     *
284
+     * @param use Boolean indicator of where it should be used or not.
637 285
      */
638
-    private void setupAudioRouteChangeDetectionPreM() {
639
-        Context context = getReactApplicationContext();
640
-
641
-        // Detect changes in wired headset connections.
642
-        IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
643
-        BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
644
-            @Override
645
-            public void onReceive(Context context, Intent intent) {
646
-                JitsiMeetLogger.d(TAG + " Wired headset added / removed");
647
-                onHeadsetDeviceChange();
648
-            }
649
-        };
650
-        context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
651
-
652
-        // Detect Bluetooth device changes.
653
-        bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(this, context);
286
+    @ReactMethod
287
+    public void setUseConnectionService(boolean use) {
288
+        useConnectionService_ = use;
289
+        setAudioDeviceHandler();
654 290
     }
655 291
 
656 292
     /**
@@ -663,14 +299,11 @@ class AudioModeModule extends ReactContextBaseJavaModule
663 299
     private boolean updateAudioRoute(int mode) {
664 300
         JitsiMeetLogger.i(TAG + " Update audio route for mode: " + mode);
665 301
 
302
+        if (!audioDeviceHandler.setMode(mode)) {
303
+            return false;
304
+        }
305
+
666 306
         if (mode == DEFAULT) {
667
-            if (!useConnectionService()) {
668
-                audioFocusLost = false;
669
-                audioManager.setMode(AudioManager.MODE_NORMAL);
670
-                audioManager.abandonAudioFocus(this);
671
-                audioManager.setSpeakerphoneOn(false);
672
-                setBluetoothAudioRoute(false);
673
-            }
674 307
             selectedDevice = null;
675 308
             userSelectedDevice = null;
676 309
 
@@ -678,20 +311,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
678 311
             return true;
679 312
         }
680 313
 
681
-        if (!useConnectionService()) {
682
-            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
683
-            audioManager.setMicrophoneMute(false);
684
-
685
-            if (audioManager.requestAudioFocus(
686
-                    this,
687
-                    AudioManager.STREAM_VOICE_CALL,
688
-                    AudioManager.AUDIOFOCUS_GAIN)
689
-                    == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
690
-                JitsiMeetLogger.w(TAG + " Audio focus request failed");
691
-                return false;
692
-            }
693
-        }
694
-
695 314
         boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
696 315
         boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
697 316
 
@@ -706,8 +325,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
706 325
         }
707 326
 
708 327
         // Consider the user's selection
709
-        if (userSelectedDevice != null
710
-                && availableDevices.contains(userSelectedDevice)) {
328
+        if (userSelectedDevice != null && availableDevices.contains(userSelectedDevice)) {
711 329
             audioDevice = userSelectedDevice;
712 330
         }
713 331
 
@@ -720,13 +338,97 @@ class AudioModeModule extends ReactContextBaseJavaModule
720 338
         selectedDevice = audioDevice;
721 339
         JitsiMeetLogger.i(TAG + " Selected audio device: " + audioDevice);
722 340
 
723
-        if (useConnectionService()) {
724
-            setAudioRoute(audioDevice);
725
-        } else {
726
-            setAudioRoutePreO(audioDevice);
727
-        }
341
+        audioDeviceHandler.setAudioRoute(audioDevice);
728 342
 
729 343
         notifyDevicesChanged();
730 344
         return true;
731 345
     }
346
+
347
+    /**
348
+     * Gets the currently selected audio device.
349
+     *
350
+     * @return The selected audio device.
351
+     */
352
+    String getSelectedDevice() {
353
+        return selectedDevice;
354
+    }
355
+
356
+    /**
357
+     * Resets the current device selection.
358
+     */
359
+    void resetSelectedDevice() {
360
+        selectedDevice = null;
361
+        userSelectedDevice = null;
362
+    }
363
+
364
+    /**
365
+     * Adds a new device to the list of available devices.
366
+     *
367
+     * @param device The new device.
368
+     */
369
+    void addDevice(String device) {
370
+        availableDevices.add(device);
371
+        resetSelectedDevice();
372
+    }
373
+
374
+    /**
375
+     * Removes a device from the list of available devices.
376
+     *
377
+     * @param device The old device to the removed.
378
+     */
379
+    void removeDevice(String device) {
380
+        availableDevices.remove(device);
381
+        resetSelectedDevice();
382
+    }
383
+
384
+    /**
385
+     * Replaces the current list of available devices with a new one.
386
+     *
387
+     * @param devices The new devices list.
388
+     */
389
+    void replaceDevices(Set<String> devices) {
390
+        availableDevices = devices;
391
+        resetSelectedDevice();
392
+    }
393
+
394
+    /**
395
+     * Re-sets the current audio route. Needed when devices changes have happened.
396
+     */
397
+    void updateAudioRoute() {
398
+        if (mode != -1) {
399
+            updateAudioRoute(mode);
400
+        }
401
+    }
402
+
403
+    /**
404
+     * Interface for the modules implementing the actual audio device management.
405
+     */
406
+    interface AudioDeviceHandlerInterface {
407
+        /**
408
+         * Start detecting audio device changes.
409
+         * @param context Android {@link Context} where detection should take place.
410
+         * @param audioModeModule Reference to the main {@link AudioModeModule}.
411
+         */
412
+        void start(Context context, AudioModeModule audioModeModule);
413
+
414
+        /**
415
+         * Stop audio device detection.
416
+         */
417
+        void stop();
418
+
419
+        /**
420
+         * Set the appropriate route for the given audio device.
421
+         *
422
+         * @param device Audio device for which the route must be set.
423
+         */
424
+        void setAudioRoute(String device);
425
+
426
+        /**
427
+         * Set the given audio mode.
428
+         *
429
+         * @param mode The new audio mode to be used.
430
+         * @return Whether the operation was successful or not.
431
+         */
432
+        boolean setMode(int mode);
433
+    }
732 434
 }

+ 63
- 66
android/sdk/src/main/java/org/jitsi/meet/sdk/BluetoothHeadsetMonitor.java Visa fil

@@ -1,5 +1,5 @@
1 1
 /*
2
- * Copyright @ 2017-present Atlassian Pty Ltd
2
+ * Copyright @ 2017-present 8x8, Inc.
3 3
  *
4 4
  * Licensed under the Apache License, Version 2.0 (the "License");
5 5
  * you may not use this file except in compliance with the License.
@@ -33,15 +33,17 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
33 33
  * about device changes when this occurs.
34 34
  */
35 35
 class BluetoothHeadsetMonitor {
36
+    private final static String TAG = BluetoothHeadsetMonitor.class.getSimpleName();
37
+
36 38
     /**
37
-     * {@link AudioModeModule} where this monitor reports.
39
+     * The {@link Context} in which this module executes.
38 40
      */
39
-    private final AudioModeModule audioModeModule;
41
+    private final Context context;
40 42
 
41 43
     /**
42
-     * The {@link Context} in which {@link #audioModeModule} executes.
44
+     * Reference to the {@link BluetoothAdapter} object, used to access Bluetooth functionality.
43 45
      */
44
-    private final Context context;
46
+    private BluetoothAdapter adapter;
45 47
 
46 48
     /**
47 49
      * Reference to a proxy object which allows us to query connected devices.
@@ -49,52 +51,25 @@ class BluetoothHeadsetMonitor {
49 51
     private BluetoothHeadset headset;
50 52
 
51 53
     /**
52
-     * Flag indicating if there are any Bluetooth headset devices currently
53
-     * available.
54
+     * receiver registered for receiving Bluetooth connection state changes.
54 55
      */
55
-    private boolean headsetAvailable = false;
56
+    private BroadcastReceiver receiver;
56 57
 
57 58
     /**
58
-     * Helper for running Bluetooth operations on the main thread.
59
+     * Listener for receiving Bluetooth device change events.
59 60
      */
60
-    private final Runnable updateDevicesRunnable
61
-        = new Runnable() {
62
-            @Override
63
-            public void run() {
64
-                headsetAvailable
65
-                    = (headset != null)
66
-                        && !headset.getConnectedDevices().isEmpty();
67
-                audioModeModule.onBluetoothDeviceChange();
68
-            }
69
-        };
61
+    private Listener listener;
70 62
 
71
-    public BluetoothHeadsetMonitor(
72
-            AudioModeModule audioModeModule,
73
-            Context context) {
74
-        this.audioModeModule = audioModeModule;
63
+    public BluetoothHeadsetMonitor(Context context, Listener listener) {
75 64
         this.context = context;
76
-
77
-        AudioManager audioManager
78
-            = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
79
-
80
-        if (!audioManager.isBluetoothScoAvailableOffCall()) {
81
-            JitsiMeetLogger.w(AudioModeModule.TAG + " Bluetooth SCO is not available");
82
-            return;
83
-        }
84
-
85
-        if (getBluetoothHeadsetProfileProxy()) {
86
-            registerBluetoothReceiver();
87
-
88
-            // Initial detection.
89
-            updateDevices();
90
-        }
65
+        this.listener = listener;
91 66
     }
92 67
 
93 68
     private boolean getBluetoothHeadsetProfileProxy() {
94
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
69
+        adapter = BluetoothAdapter.getDefaultAdapter();
95 70
 
96 71
         if (adapter == null) {
97
-            JitsiMeetLogger.w(AudioModeModule.TAG + " Device doesn't support Bluetooth");
72
+            JitsiMeetLogger.w(TAG + " Device doesn't support Bluetooth");
98 73
             return false;
99 74
         }
100 75
 
@@ -104,9 +79,7 @@ class BluetoothHeadsetMonitor {
104 79
         BluetoothProfile.ServiceListener listener
105 80
             = new BluetoothProfile.ServiceListener() {
106 81
                 @Override
107
-                public void onServiceConnected(
108
-                        int profile,
109
-                        BluetoothProfile proxy) {
82
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
110 83
                     if (profile == BluetoothProfile.HEADSET) {
111 84
                         headset = (BluetoothHeadset) proxy;
112 85
                         updateDevices();
@@ -120,21 +93,7 @@ class BluetoothHeadsetMonitor {
120 93
                 }
121 94
             };
122 95
 
123
-        return
124
-            adapter.getProfileProxy(
125
-                    context,
126
-                    listener,
127
-                    BluetoothProfile.HEADSET);
128
-    }
129
-
130
-    /**
131
-     * Returns the current headset availability.
132
-     *
133
-     * @return {@code true} if there is a Bluetooth headset connected;
134
-     * {@code false}, otherwise.
135
-     */
136
-    public boolean isHeadsetAvailable() {
137
-        return headsetAvailable;
96
+        return adapter.getProfileProxy(context, listener, BluetoothProfile.HEADSET);
138 97
     }
139 98
 
140 99
     private void onBluetoothReceiverReceive(Context context, Intent intent) {
@@ -149,7 +108,7 @@ class BluetoothHeadsetMonitor {
149 108
             switch (state) {
150 109
             case BluetoothHeadset.STATE_CONNECTED:
151 110
             case BluetoothHeadset.STATE_DISCONNECTED:
152
-                JitsiMeetLogger.d(AudioModeModule.TAG + " BT headset connection state changed: " + state);
111
+                JitsiMeetLogger.d(TAG + " BT headset connection state changed: " + state);
153 112
                 updateDevices();
154 113
                 break;
155 114
             }
@@ -157,13 +116,12 @@ class BluetoothHeadsetMonitor {
157 116
             // XXX: This action will be fired when the connection established
158 117
             // with a Bluetooth headset (called a SCO connection) changes state.
159 118
             // When the SCO connection is active we route audio to it.
160
-            int state
161
-                = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
119
+            int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
162 120
 
163 121
             switch (state) {
164 122
             case AudioManager.SCO_AUDIO_STATE_CONNECTED:
165 123
             case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
166
-                JitsiMeetLogger.d(AudioModeModule.TAG + " BT SCO connection state changed: " + state);
124
+                JitsiMeetLogger.d(TAG + " BT SCO connection state changed: " + state);
167 125
                 updateDevices();
168 126
                 break;
169 127
             }
@@ -171,24 +129,63 @@ class BluetoothHeadsetMonitor {
171 129
     }
172 130
 
173 131
     private void registerBluetoothReceiver() {
174
-        BroadcastReceiver receiver = new BroadcastReceiver() {
132
+        receiver = new BroadcastReceiver() {
175 133
             @Override
176 134
             public void onReceive(Context context, Intent intent) {
177 135
                 onBluetoothReceiverReceive(context, intent);
178 136
             }
179 137
         };
180
-        IntentFilter filter = new IntentFilter();
181 138
 
139
+        IntentFilter filter = new IntentFilter();
182 140
         filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
183 141
         filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
142
+
184 143
         context.registerReceiver(receiver, filter);
185 144
     }
186 145
 
187 146
     /**
188 147
      * Detects if there are new devices connected / disconnected and fires the
189
-     * {@link AudioModeModule#onAudioDeviceChange()} callback.
148
+     * {@link Listener} registered event.
190 149
      */
191 150
     private void updateDevices() {
192
-        audioModeModule.runInAudioThread(updateDevicesRunnable);
151
+        boolean headsetAvailable = (headset != null) && !headset.getConnectedDevices().isEmpty();
152
+        listener.onBluetoothDeviceChange(headsetAvailable);
153
+    }
154
+
155
+    public void start() {
156
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
157
+
158
+        if (!audioManager.isBluetoothScoAvailableOffCall()) {
159
+            JitsiMeetLogger.w(TAG + " Bluetooth SCO is not available");
160
+            return;
161
+        }
162
+
163
+        if (!getBluetoothHeadsetProfileProxy()) {
164
+            JitsiMeetLogger.w(TAG + " Couldn't get BT profile proxy");
165
+            return;
166
+        }
167
+
168
+        registerBluetoothReceiver();
169
+
170
+        // Initial detection.
171
+        updateDevices();
172
+    }
173
+
174
+    public void stop() {
175
+        if (receiver != null) {
176
+            context.unregisterReceiver(receiver);
177
+        }
178
+
179
+        if (adapter != null && headset != null) {
180
+            adapter.closeProfileProxy(BluetoothProfile.HEADSET, headset);
181
+        }
182
+
183
+        receiver = null;
184
+        headset = null;
185
+        adapter = null;
186
+    }
187
+
188
+    interface Listener {
189
+        void onBluetoothDeviceChange(boolean deviceAvailable);
193 190
     }
194 191
 }

+ 3
- 5
android/sdk/src/main/java/org/jitsi/meet/sdk/ConnectionService.java Visa fil

@@ -403,11 +403,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
403 403
         @Override
404 404
         public void onCallAudioStateChanged(CallAudioState state) {
405 405
             JitsiMeetLogger.d(TAG + " onCallAudioStateChanged: " + state);
406
-            AudioModeModule audioModeModule
407
-                    = ReactInstanceManagerHolder
408
-                    .getNativeModule(AudioModeModule.class);
409
-            if (audioModeModule != null) {
410
-                audioModeModule.onCallAudioStateChange(state);
406
+            RNConnectionService module = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
407
+            if (module != null) {
408
+                module.onCallAudioStateChange(state);
411 409
             }
412 410
         }
413 411
 

+ 31
- 2
android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java Visa fil

@@ -29,13 +29,18 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
29 29
  */
30 30
 @RequiresApi(api = Build.VERSION_CODES.O)
31 31
 @ReactModule(name = RNConnectionService.NAME)
32
-class RNConnectionService
33
-    extends ReactContextBaseJavaModule {
32
+class RNConnectionService extends ReactContextBaseJavaModule {
34 33
 
35 34
     public static final String NAME = "ConnectionService";
36 35
 
37 36
     private static final String TAG = ConnectionService.TAG;
38 37
 
38
+    /**
39
+     * Handler for dealing with call state changes. We are acting as a proxy between ConnectionService
40
+     * and other modules such as {@link AudioModeModule}.
41
+     */
42
+    private CallAudioStateListener callAudioStateListener;
43
+
39 44
     /**
40 45
      * Sets the audio route on all existing {@link android.telecom.Connection}s
41 46
      *
@@ -168,4 +173,28 @@ class RNConnectionService
168 173
     public void updateCall(String callUUID, ReadableMap callState) {
169 174
         ConnectionService.updateCall(callUUID, callState);
170 175
     }
176
+
177
+    public CallAudioStateListener getCallAudioStateListener() {
178
+        return callAudioStateListener;
179
+    }
180
+
181
+    public void setCallAudioStateListener(CallAudioStateListener callAudioStateListener) {
182
+        this.callAudioStateListener = callAudioStateListener;
183
+    }
184
+
185
+    /**
186
+     * Handler for call state changes. {@code ConnectionServiceImpl} will call this handler when the
187
+     * call audio state changes.
188
+     *
189
+     * @param callAudioState The current call's audio state.
190
+     */
191
+    void onCallAudioStateChange(android.telecom.CallAudioState callAudioState) {
192
+        if (callAudioStateListener != null) {
193
+            callAudioStateListener.onCallAudioStateChange(callAudioState);
194
+        }
195
+    }
196
+
197
+    interface CallAudioStateListener {
198
+        void onCallAudioStateChange(android.telecom.CallAudioState callAudioState);
199
+    }
171 200
 }

Laddar…
Avbryt
Spara