ソースを参照

android: add ConnectionService

* feat(Android): implement ConnectionService

Adds basic integration with Android's ConnectionService by implementing
the outgoing call scenario.

* ref(callkit): rename _SET_CALLKIT_SUBSCRIPTIONS

* ref(callkit): move feature to call-integration directory

* feat(ConnectionService): synchronize video state

* ref(AudioMode): use ConnectionService on API >= 26

Not ready yet - few details left mentioned in the FIXMEs

* feat(ConnectionService): add debug logs

Adds logs to trace the calls.

* fix(ConnectionService): leaking ConnectionImpl instances

Turns out there is no callback fired back from the JavaScript side after
the disconnect or abort event is sent from the native. The connection
must be marked as disconnected and removed immediately.

* feat(ConnectionService): handle onCreateOutgoingConnectionFailed

* ref(ConnectionService): merge classes and move to the sdk package

* feat(CallIntegration): show Alert if outgoing call fails

* fix(ConnectionService): alternatively get call UUID from the account

Some Android flavours (or versions ?) do copy over extras to
the onCreateOutgoingConnectionFailed callback. But the call UUID is also
set as the PhoneAccount's label, so eventually it should be available
there.

* ref(ConnectionService): use call UUID as PhoneAccount ID.

The extra is not reliable on some custom Android flavours. It also makes
sense to use unique id for the account instead of the URL given that
it's created on the per call basis.

* fix(ConnectionService): abort the call when hold is requested

Turns out Android P can sometimes request HOLD even though there's no
HOLD capability added to the connection (what!?), so just abort the call
in that case.

* fix(ConnectionService): unregister account on call failure

Unregister the PhoneAccount onCreateOutgoingConnectionFailed. That's
before the ConnectionImpl instance is created which is normally
responsible for doing that.

* fix(AudioModeModule): make package private and run on the audio thread

* address other review comments
j8
Paweł Domas 6年前
コミット
f8294fb312

+ 7
- 0
android/sdk/src/main/AndroidManifest.xml ファイルの表示

6
   <uses-permission android:name="android.permission.BLUETOOTH" />
6
   <uses-permission android:name="android.permission.BLUETOOTH" />
7
   <uses-permission android:name="android.permission.CAMERA" />
7
   <uses-permission android:name="android.permission.CAMERA" />
8
   <uses-permission android:name="android.permission.INTERNET" />
8
   <uses-permission android:name="android.permission.INTERNET" />
9
+  <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
9
   <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
10
   <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
10
   <uses-permission android:name="android.permission.RECORD_AUDIO" />
11
   <uses-permission android:name="android.permission.RECORD_AUDIO" />
11
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
12
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
26
       android:supportsRtl="true">
27
       android:supportsRtl="true">
27
     <activity
28
     <activity
28
         android:name="com.facebook.react.devsupport.DevSettingsActivity" />
29
         android:name="com.facebook.react.devsupport.DevSettingsActivity" />
30
+    <service android:name="org.jitsi.meet.sdk.ConnectionService"
31
+        android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
32
+      <intent-filter>
33
+        <action android:name="android.telecom.ConnectionService" />
34
+      </intent-filter>
35
+    </service>
29
   </application>
36
   </application>
30
 </manifest>
37
 </manifest>

+ 170
- 31
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java ファイルの表示

25
 import android.media.AudioDeviceInfo;
25
 import android.media.AudioDeviceInfo;
26
 import android.media.AudioManager;
26
 import android.media.AudioManager;
27
 import android.os.Build;
27
 import android.os.Build;
28
+import android.support.annotation.RequiresApi;
29
+import android.telecom.CallAudioState;
28
 import android.util.Log;
30
 import android.util.Log;
29
 
31
 
30
 import com.facebook.react.bridge.Arguments;
32
 import com.facebook.react.bridge.Arguments;
100
      */
102
      */
101
     static final String TAG = MODULE_NAME;
103
     static final String TAG = MODULE_NAME;
102
 
104
 
105
+    /**
106
+     * Converts any of the "DEVICE_" constants into the corresponding
107
+     * {@link CallAudioState} "ROUTE_" number.
108
+     *
109
+     * @param audioDevice one of the "DEVICE_" constants.
110
+     * @return a route number {@link CallAudioState#ROUTE_EARPIECE} if no match
111
+     * is found.
112
+     */
113
+    @RequiresApi(api = Build.VERSION_CODES.M)
114
+    private static int audioDeviceToRouteInt(String audioDevice) {
115
+        if (audioDevice == null) {
116
+            return CallAudioState.ROUTE_EARPIECE;
117
+        }
118
+        switch (audioDevice) {
119
+            case DEVICE_BLUETOOTH:
120
+                return CallAudioState.ROUTE_BLUETOOTH;
121
+            case DEVICE_EARPIECE:
122
+                return CallAudioState.ROUTE_EARPIECE;
123
+            case DEVICE_HEADPHONES:
124
+                return CallAudioState.ROUTE_WIRED_HEADSET;
125
+            case DEVICE_SPEAKER:
126
+                return CallAudioState.ROUTE_SPEAKER;
127
+            default:
128
+                Log.e(TAG, "Unsupported device name: " + audioDevice);
129
+                return CallAudioState.ROUTE_EARPIECE;
130
+        }
131
+    }
132
+
133
+    /**
134
+     * Populates given route mask into the "DEVICE_" list.
135
+     *
136
+     * @param supportedRouteMask an integer coming from
137
+     * {@link CallAudioState#getSupportedRouteMask()}.
138
+     * @return a list of device names.
139
+     */
140
+    private static Set<String> routesToDeviceNames(int supportedRouteMask) {
141
+        Set<String> devices = new HashSet<>();
142
+        if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE)
143
+                == CallAudioState.ROUTE_EARPIECE) {
144
+            devices.add(DEVICE_EARPIECE);
145
+        }
146
+        if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
147
+                == CallAudioState.ROUTE_BLUETOOTH) {
148
+            devices.add(DEVICE_BLUETOOTH);
149
+        }
150
+        if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER)
151
+                == CallAudioState.ROUTE_SPEAKER) {
152
+            devices.add(DEVICE_SPEAKER);
153
+        }
154
+        if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
155
+                == CallAudioState.ROUTE_WIRED_HEADSET) {
156
+            devices.add(DEVICE_HEADPHONES);
157
+        }
158
+        return devices;
159
+    }
160
+
161
+    /**
162
+     * Whether or not the ConnectionService is used for selecting audio devices.
163
+     */
164
+    private static boolean useConnectionService() {
165
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
166
+    }
167
+
103
     /**
168
     /**
104
      * Indicator that we have lost audio focus.
169
      * Indicator that we have lost audio focus.
105
      */
170
      */
204
      */
269
      */
205
     private String selectedDevice;
270
     private String selectedDevice;
206
 
271
 
272
+    /**
273
+     * Used on API >= 26 to store the most recently reported audio devices.
274
+     * Makes it easier to compare for a change, because the devices are stored
275
+     * as a mask in the {@link CallAudioState}. The mask is populated into
276
+     * the {@link #availableDevices} on each update.
277
+     */
278
+    @RequiresApi(api = Build.VERSION_CODES.O)
279
+    private int supportedRouteMask;
280
+
207
     /**
281
     /**
208
      * User selected device. When null the default is used depending on the
282
      * User selected device. When null the default is used depending on the
209
      * mode.
283
      * mode.
224
             = (AudioManager)
298
             = (AudioManager)
225
                 reactContext.getSystemService(Context.AUDIO_SERVICE);
299
                 reactContext.getSystemService(Context.AUDIO_SERVICE);
226
 
300
 
227
-        // Setup runtime device change detection.
228
-        setupAudioRouteChangeDetection();
301
+        // Starting Oreo the ConnectionImpl from ConnectionService us used to
302
+        // detect the available devices.
303
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
304
+            // Setup runtime device change detection.
305
+            setupAudioRouteChangeDetection();
306
+
307
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
308
+                // Do an initial detection on Android >= M.
309
+                runInAudioThread(onAudioDeviceChangeRunner);
310
+            } else {
311
+                // On Android < M, detect if we have an earpiece.
312
+                PackageManager pm = reactContext.getPackageManager();
313
+                if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
314
+                    availableDevices.add(DEVICE_EARPIECE);
315
+                }
229
 
316
 
230
-        // Do an initial detection on Android >= M.
231
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
232
-            runInAudioThread(onAudioDeviceChangeRunner);
233
-        } else {
234
-            // On Android < M, detect if we have an earpiece.
235
-            PackageManager pm = reactContext.getPackageManager();
236
-            if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
237
-                availableDevices.add(DEVICE_EARPIECE);
317
+                // Always assume there is a speaker.
318
+                availableDevices.add(DEVICE_SPEAKER);
238
             }
319
             }
239
-
240
-            // Always assume there is a speaker.
241
-            availableDevices.add(DEVICE_SPEAKER);
242
         }
320
         }
243
     }
321
     }
244
 
322
 
354
         });
432
         });
355
     }
433
     }
356
 
434
 
435
+    @RequiresApi(api = Build.VERSION_CODES.O)
436
+    void onCallAudioStateChange(final CallAudioState callAudioState) {
437
+        runInAudioThread(new Runnable() {
438
+            @Override
439
+            public void run() {
440
+                int newSupportedRoutes = callAudioState.getSupportedRouteMask();
441
+                boolean audioDevicesChanged
442
+                        = supportedRouteMask != newSupportedRoutes;
443
+                if (audioDevicesChanged) {
444
+                    supportedRouteMask = newSupportedRoutes;
445
+                    availableDevices = routesToDeviceNames(supportedRouteMask);
446
+                    Log.d(TAG,
447
+                          "Available audio devices: "
448
+                                  + availableDevices.toString());
449
+                }
450
+
451
+                boolean audioRouteChanged
452
+                    = audioDeviceToRouteInt(selectedDevice)
453
+                            != callAudioState.getRoute();
454
+
455
+                if (audioRouteChanged || audioDevicesChanged) {
456
+                    // Reset user selection
457
+                    userSelectedDevice = null;
458
+
459
+                    if (mode != -1) {
460
+                        updateAudioRoute(mode);
461
+                    }
462
+                }
463
+            }
464
+        });
465
+    }
466
+
357
     /**
467
     /**
358
      * {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
468
      * {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
359
      * when the audio focus of the system is updated.
469
      * when the audio focus of the system is updated.
417
         });
527
         });
418
     }
528
     }
419
 
529
 
530
+    /**
531
+     * The API >= 26 way of adjusting the audio route.
532
+     *
533
+     * @param audioDevice one of the "DEVICE_" names to set as the audio route.
534
+     */
535
+    @RequiresApi(api = Build.VERSION_CODES.O)
536
+    private void setAudioRoute(String audioDevice) {
537
+        int newAudioRoute = audioDeviceToRouteInt(audioDevice);
538
+
539
+        RNConnectionService.setAudioRoute(newAudioRoute);
540
+    }
541
+
542
+    /**
543
+     * The API < 26 way of adjusting the audio route.
544
+     *
545
+     * @param audioDevice one of the "DEVICE_" names to set as the audio route.
546
+     */
547
+    private void setAudioRoutePreO(String audioDevice) {
548
+        // Turn bluetooth on / off
549
+        setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
550
+
551
+        // Turn speaker on / off
552
+        audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
553
+    }
554
+
420
     /**
555
     /**
421
      * Helper method to set the output route to a Bluetooth device.
556
      * Helper method to set the output route to a Bluetooth device.
422
      *
557
      *
475
 
610
 
476
     /**
611
     /**
477
      * Setup the audio route change detection mechanism. We use the
612
      * Setup the audio route change detection mechanism. We use the
478
-     * {@link android.media.AudioDeviceCallback} API on Android >= 23 only.
613
+     * {@link android.media.AudioDeviceCallback} on 23 >= Android API < 26.
479
      */
614
      */
480
     private void setupAudioRouteChangeDetection() {
615
     private void setupAudioRouteChangeDetection() {
481
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
616
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
486
     }
621
     }
487
 
622
 
488
     /**
623
     /**
489
-     * Audio route change detection mechanism for Android API >= 23.
624
+     * Audio route change detection mechanism for 23 >= Android API < 26.
490
      */
625
      */
491
     @TargetApi(Build.VERSION_CODES.M)
626
     @TargetApi(Build.VERSION_CODES.M)
492
     private void setupAudioRouteChangeDetectionM() {
627
     private void setupAudioRouteChangeDetectionM() {
542
         Log.d(TAG, "Update audio route for mode: " + mode);
677
         Log.d(TAG, "Update audio route for mode: " + mode);
543
 
678
 
544
         if (mode == DEFAULT) {
679
         if (mode == DEFAULT) {
545
-            audioFocusLost = false;
546
-            audioManager.setMode(AudioManager.MODE_NORMAL);
547
-            audioManager.abandonAudioFocus(this);
548
-            audioManager.setSpeakerphoneOn(false);
549
-            setBluetoothAudioRoute(false);
680
+            if (!useConnectionService()) {
681
+                audioFocusLost = false;
682
+                audioManager.setMode(AudioManager.MODE_NORMAL);
683
+                audioManager.abandonAudioFocus(this);
684
+                audioManager.setSpeakerphoneOn(false);
685
+                setBluetoothAudioRoute(false);
686
+            }
550
             selectedDevice = null;
687
             selectedDevice = null;
551
             userSelectedDevice = null;
688
             userSelectedDevice = null;
552
 
689
 
553
             return true;
690
             return true;
554
         }
691
         }
555
 
692
 
556
-        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
557
-        audioManager.setMicrophoneMute(false);
693
+        if (!useConnectionService()) {
694
+            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
695
+            audioManager.setMicrophoneMute(false);
558
 
696
 
559
-        if (audioManager.requestAudioFocus(
697
+            if (audioManager.requestAudioFocus(
560
                     this,
698
                     this,
561
                     AudioManager.STREAM_VOICE_CALL,
699
                     AudioManager.STREAM_VOICE_CALL,
562
                     AudioManager.AUDIOFOCUS_GAIN)
700
                     AudioManager.AUDIOFOCUS_GAIN)
563
-                == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
564
-            Log.d(TAG, "Audio focus request failed");
565
-            return false;
701
+                    == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
702
+                Log.d(TAG, "Audio focus request failed");
703
+                return false;
704
+            }
566
         }
705
         }
567
 
706
 
568
         boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
707
         boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
596
         selectedDevice = audioDevice;
735
         selectedDevice = audioDevice;
597
         Log.d(TAG, "Selected audio device: " + audioDevice);
736
         Log.d(TAG, "Selected audio device: " + audioDevice);
598
 
737
 
599
-        // Turn bluetooth on / off
600
-        setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
601
-
602
-        // Turn speaker on / off
603
-        audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
738
+        if (useConnectionService()) {
739
+            setAudioRoute(audioDevice);
740
+        } else {
741
+            setAudioRoutePreO(audioDevice);
742
+        }
604
 
743
 
605
         return true;
744
         return true;
606
     }
745
     }

+ 435
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ConnectionService.java ファイルの表示

1
+package org.jitsi.meet.sdk;
2
+
3
+import android.content.ComponentName;
4
+import android.content.Context;
5
+import android.net.Uri;
6
+import android.os.Build;
7
+import android.os.Bundle;
8
+import android.support.annotation.RequiresApi;
9
+import android.telecom.CallAudioState;
10
+import android.telecom.Connection;
11
+import android.telecom.ConnectionRequest;
12
+import android.telecom.DisconnectCause;
13
+import android.telecom.PhoneAccount;
14
+import android.telecom.PhoneAccountHandle;
15
+import android.telecom.TelecomManager;
16
+import android.telecom.VideoProfile;
17
+import android.util.Log;
18
+
19
+import com.facebook.react.bridge.Promise;
20
+import com.facebook.react.bridge.ReadableMap;
21
+import com.facebook.react.bridge.WritableNativeMap;
22
+
23
+import java.util.ArrayList;
24
+import java.util.HashMap;
25
+import java.util.List;
26
+import java.util.Map;
27
+import java.util.Objects;
28
+
29
+/**
30
+ * Jitsi Meet implementation of {@link ConnectionService}. At the time of this
31
+ * writing it implements only the outgoing call scenario.
32
+ *
33
+ * NOTE the class needs to be public, but is not part of the SDK API and should
34
+ * never be used directly.
35
+ *
36
+ * @author Pawel Domas
37
+ */
38
+@RequiresApi(api = Build.VERSION_CODES.O)
39
+public class ConnectionService extends android.telecom.ConnectionService {
40
+
41
+    /**
42
+     * Tag used for logging.
43
+     */
44
+    static final String TAG = "JitsiConnectionService";
45
+
46
+    /**
47
+     * The extra added to the {@link ConnectionImpl} and
48
+     * {@link ConnectionRequest} which stores the {@link PhoneAccountHandle}
49
+     * created for the call.
50
+     */
51
+    static final String EXTRA_PHONE_ACCOUNT_HANDLE
52
+        = "org.jitsi.meet.sdk.connection_service.PHONE_ACCOUNT_HANDLE";
53
+
54
+    /**
55
+     * Connections mapped by call UUID.
56
+     */
57
+    static private final Map<String, ConnectionImpl> connections
58
+            = new HashMap<>();
59
+
60
+    /**
61
+     * The start call Promises mapped by call UUID.
62
+     */
63
+    static private final HashMap<String, Promise> startCallPromises
64
+            = new HashMap<>();
65
+
66
+    /**
67
+     * Adds {@link ConnectionImpl} to the list.
68
+     *
69
+     * @param connection - {@link ConnectionImpl}
70
+     */
71
+    static void addConnection(ConnectionImpl connection) {
72
+        connections.put(connection.getCallUUID(), connection);
73
+    }
74
+
75
+    /**
76
+     * Returns all {@link ConnectionImpl} instances held in this list.
77
+     *
78
+     * @return a list of {@link ConnectionImpl}.
79
+     */
80
+    static List<ConnectionImpl> getConnections() {
81
+        return new ArrayList<>(connections.values());
82
+    }
83
+
84
+    /**
85
+     * Registers a start call promise.
86
+     *
87
+     * @param uuid - the call UUID to which the start call promise belongs to.
88
+     * @param promise - the Promise instance to be stored for later use.
89
+     */
90
+    static void registerStartCallPromise(String uuid, Promise promise) {
91
+        startCallPromises.put(uuid, promise);
92
+    }
93
+
94
+    /**
95
+     * Removes {@link ConnectionImpl} from the list.
96
+     *
97
+     * @param connection - {@link ConnectionImpl}
98
+     */
99
+    static void removeConnection(ConnectionImpl connection) {
100
+        connections.remove(connection.getCallUUID());
101
+    }
102
+
103
+    /**
104
+     * Used to adjusts the connection's state to
105
+     * {@link android.telecom.Connection#STATE_ACTIVE}.
106
+     *
107
+     * @param callUUID the call UUID which identifies the connection.
108
+     */
109
+    static void setConnectionActive(String callUUID) {
110
+        ConnectionImpl connection = connections.get(callUUID);
111
+
112
+        if (connection != null) {
113
+            connection.setActive();
114
+        } else {
115
+            Log.e(TAG, String.format(
116
+                    "setConnectionActive - no connection for UUID: %s",
117
+                    callUUID));
118
+        }
119
+    }
120
+
121
+    /**
122
+     * Used to adjusts the connection's state to
123
+     * {@link android.telecom.Connection#STATE_DISCONNECTED}.
124
+     *
125
+     * @param callUUID the call UUID which identifies the connection.
126
+     * @param cause disconnection reason.
127
+     */
128
+    static void setConnectionDisconnected(String callUUID, DisconnectCause cause) {
129
+        ConnectionImpl connection = connections.get(callUUID);
130
+
131
+        if (connection != null) {
132
+            // Note that the connection is not removed from the list here, but
133
+            // in ConnectionImpl's state changed callback. It's a safer
134
+            // approach, because in case the app would crash on the JavaScript
135
+            // side the calls would be cleaned up by the system they would still
136
+            // be removed from the ConnectionList.
137
+            connection.setDisconnected(cause);
138
+            connection.destroy();
139
+        } else {
140
+            Log.e(TAG, "endCall no connection for UUID: " + callUUID);
141
+        }
142
+    }
143
+
144
+    /**
145
+     * Unregisters a start call promise. Must be called after the Promise is
146
+     * rejected or resolved.
147
+     *
148
+     * @param uuid the call UUID which identifies the call to which the promise
149
+     *        belongs to.
150
+     * @return the unregistered Promise instance or <tt>null</tt> if there
151
+     *         wasn't any for the given call UUID.
152
+     */
153
+    static Promise unregisterStartCallPromise(String uuid) {
154
+        return startCallPromises.remove(uuid);
155
+    }
156
+
157
+    /**
158
+     * Used to adjusts the call's state.
159
+     *
160
+     * @param callUUID the call UUID which identifies the connection.
161
+     * @param callState a map which carries the properties to be modified. See
162
+     *        "KEY_*" constants in {@link ConnectionImpl} for the list of keys.
163
+     */
164
+    static void updateCall(String callUUID, ReadableMap callState) {
165
+        ConnectionImpl connection = connections.get(callUUID);
166
+
167
+        if (connection != null) {
168
+            if (callState.hasKey(ConnectionImpl.KEY_HAS_VIDEO)) {
169
+                boolean hasVideo
170
+                        = callState.getBoolean(ConnectionImpl.KEY_HAS_VIDEO);
171
+
172
+                Log.d(TAG, String.format(
173
+                        "updateCall: %s hasVideo: %s", callUUID, hasVideo));
174
+                connection.setVideoState(
175
+                        hasVideo
176
+                                ? VideoProfile.STATE_BIDIRECTIONAL
177
+                                : VideoProfile.STATE_AUDIO_ONLY);
178
+            }
179
+        } else {
180
+            Log.e(TAG, "updateCall no connection for UUID: " + callUUID);
181
+        }
182
+    }
183
+
184
+    @Override
185
+    public Connection onCreateOutgoingConnection(
186
+            PhoneAccountHandle accountHandle, ConnectionRequest request) {
187
+        ConnectionImpl connection = new ConnectionImpl();
188
+
189
+        connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
190
+        connection.setAddress(
191
+            request.getAddress(),
192
+            TelecomManager.PRESENTATION_ALLOWED);
193
+        connection.setExtras(request.getExtras());
194
+        // NOTE there's a time gap between the placeCall and this callback when
195
+        // things could get out of sync, but they are put back in sync once
196
+        // the startCall Promise is resolved below. That's because on
197
+        // the JavaScript side there's a logic to sync up in .then() callback.
198
+        connection.setVideoState(request.getVideoState());
199
+
200
+        Bundle moreExtras = new Bundle();
201
+
202
+        moreExtras.putParcelable(
203
+            EXTRA_PHONE_ACCOUNT_HANDLE,
204
+            Objects.requireNonNull(request.getAccountHandle(), "accountHandle"));
205
+        connection.putExtras(moreExtras);
206
+
207
+        addConnection(connection);
208
+
209
+        Promise startCallPromise
210
+            = unregisterStartCallPromise(connection.getCallUUID());
211
+
212
+        if (startCallPromise != null) {
213
+            Log.d(TAG,
214
+                  "onCreateOutgoingConnection " + connection.getCallUUID());
215
+            startCallPromise.resolve(null);
216
+        } else {
217
+            Log.e(TAG, String.format(
218
+                "onCreateOutgoingConnection: no start call Promise for %s",
219
+                connection.getCallUUID()));
220
+        }
221
+
222
+        return connection;
223
+    }
224
+
225
+    @Override
226
+    public Connection onCreateIncomingConnection(
227
+            PhoneAccountHandle accountHandle, ConnectionRequest request) {
228
+        throw new RuntimeException("Not implemented");
229
+    }
230
+
231
+    @Override
232
+    public void onCreateIncomingConnectionFailed(
233
+            PhoneAccountHandle accountHandle, ConnectionRequest request) {
234
+        throw new RuntimeException("Not implemented");
235
+    }
236
+
237
+    @Override
238
+    public void onCreateOutgoingConnectionFailed(
239
+            PhoneAccountHandle accountHandle, ConnectionRequest request) {
240
+        PhoneAccountHandle theAccountHandle = request.getAccountHandle();
241
+        String callUUID = theAccountHandle.getId();
242
+
243
+        Log.e(TAG, "onCreateOutgoingConnectionFailed " + callUUID);
244
+
245
+        if (callUUID != null) {
246
+            Promise startCallPromise = unregisterStartCallPromise(callUUID);
247
+
248
+            if (startCallPromise != null) {
249
+                startCallPromise.reject(
250
+                        "CREATE_OUTGOING_CALL_FAILED",
251
+                        "The request has been denied by the system");
252
+            } else {
253
+                Log.e(TAG, String.format(
254
+                        "startCallFailed - no start call Promise for UUID: %s",
255
+                        callUUID));
256
+            }
257
+        } else {
258
+            Log.e(TAG, "onCreateOutgoingConnectionFailed - no call UUID");
259
+        }
260
+
261
+        unregisterPhoneAccount(theAccountHandle);
262
+    }
263
+
264
+    private void unregisterPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
265
+        TelecomManager telecom = getSystemService(TelecomManager.class);
266
+        if (telecom != null) {
267
+            if (phoneAccountHandle != null) {
268
+                telecom.unregisterPhoneAccount(phoneAccountHandle);
269
+            } else {
270
+                Log.e(TAG, "unregisterPhoneAccount - account handle is null");
271
+            }
272
+        } else {
273
+            Log.e(TAG, "unregisterPhoneAccount - telecom is null");
274
+        }
275
+    }
276
+
277
+    /**
278
+     * Registers new {@link PhoneAccountHandle}.
279
+     *
280
+     * @param context the current Android context.
281
+     * @param address the phone account's address. At the time of this writing
282
+     *        it's the call handle passed from the Java Script side.
283
+     * @param callUUID the call's UUID for which the account is to be created.
284
+     *        It will be used as the account's id.
285
+     * @return {@link PhoneAccountHandle} described by the given arguments.
286
+     */
287
+    static PhoneAccountHandle registerPhoneAccount(
288
+            Context context, Uri address, String callUUID) {
289
+        PhoneAccountHandle phoneAccountHandle
290
+            = new PhoneAccountHandle(
291
+                    new ComponentName(context, ConnectionService.class),
292
+                    callUUID);
293
+
294
+        PhoneAccount.Builder builder
295
+            = PhoneAccount.builder(phoneAccountHandle, address.toString())
296
+                .setAddress(address)
297
+                .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
298
+                        PhoneAccount.CAPABILITY_VIDEO_CALLING |
299
+                        PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
300
+                .addSupportedUriScheme(PhoneAccount.SCHEME_SIP);
301
+
302
+        PhoneAccount account = builder.build();
303
+
304
+        TelecomManager telecomManager
305
+            = context.getSystemService(TelecomManager.class);
306
+        telecomManager.registerPhoneAccount(account);
307
+
308
+        return phoneAccountHandle;
309
+    }
310
+
311
+    /**
312
+     * Connection implementation for Jitsi Meet's {@link ConnectionService}.
313
+     *
314
+     * @author Pawel Domas
315
+     */
316
+    class ConnectionImpl extends Connection {
317
+
318
+        /**
319
+         * The constant which defines the key for the "has video" property.
320
+         * The key is used in the map which carries the call's state passed as
321
+         * the argument of the {@link RNConnectionService#updateCall} method.
322
+         */
323
+        static final String KEY_HAS_VIDEO = "hasVideo";
324
+
325
+        /**
326
+         * Called when system wants to disconnect the call.
327
+         *
328
+         * {@inheritDoc}
329
+         */
330
+        @Override
331
+        public void onDisconnect() {
332
+            Log.d(TAG, "onDisconnect " + getCallUUID());
333
+            WritableNativeMap data = new WritableNativeMap();
334
+            data.putString("callUUID", getCallUUID());
335
+            ReactContextUtils.emitEvent(
336
+                    null,
337
+                    "org.jitsi.meet:features/connection_service#disconnect",
338
+                    data);
339
+            // The JavaScript side will not go back to the native with
340
+            // 'endCall', so the Connection must be removed immediately.
341
+            setConnectionDisconnected(
342
+                    getCallUUID(),
343
+                    new DisconnectCause(DisconnectCause.LOCAL));
344
+        }
345
+
346
+        /**
347
+         * Called when system wants to abort the call.
348
+         *
349
+         * {@inheritDoc}
350
+         */
351
+        @Override
352
+        public void onAbort() {
353
+            Log.d(TAG, "onAbort " + getCallUUID());
354
+            WritableNativeMap data = new WritableNativeMap();
355
+            data.putString("callUUID", getCallUUID());
356
+            ReactContextUtils.emitEvent(
357
+                    null,
358
+                    "org.jitsi.meet:features/connection_service#abort",
359
+                    data);
360
+            // The JavaScript side will not go back to the native with
361
+            // 'endCall', so the Connection must be removed immediately.
362
+            setConnectionDisconnected(
363
+                    getCallUUID(),
364
+                    new DisconnectCause(DisconnectCause.CANCELED));
365
+        }
366
+
367
+        @Override
368
+        public void onHold() {
369
+            // What ?! Android will still call this method even if we do not add
370
+            // the HOLD capability, so do the same thing as on abort.
371
+            // TODO implement HOLD
372
+            Log.d(TAG, String.format(
373
+                  "onHold %s - HOLD is not supported, aborting the call...",
374
+                  getCallUUID()));
375
+            this.onAbort();
376
+        }
377
+
378
+        /**
379
+         * Called when there's change to the call audio state. Either by
380
+         * the system after the connection is initialized or in response to
381
+         * {@link #setAudioRoute(int)}.
382
+         *
383
+         * @param state the new {@link CallAudioState}
384
+         */
385
+        @Override
386
+        public void onCallAudioStateChanged(CallAudioState state) {
387
+            Log.d(TAG, "onCallAudioStateChanged: " + state);
388
+            AudioModeModule audioModeModule
389
+                    = ReactInstanceManagerHolder
390
+                    .getNativeModule(AudioModeModule.class);
391
+            if (audioModeModule != null) {
392
+                audioModeModule.onCallAudioStateChange(state);
393
+            }
394
+        }
395
+
396
+        /**
397
+         * Unregisters the account when the call is disconnected.
398
+         *
399
+         * @param state - the new connection's state.
400
+         */
401
+        @Override
402
+        public void onStateChanged(int state) {
403
+            Log.d(TAG,
404
+                  String.format("onStateChanged: %s %s",
405
+                                Connection.stateToString(state),
406
+                                getCallUUID()));
407
+
408
+            if (state == STATE_DISCONNECTED) {
409
+                removeConnection(this);
410
+                unregisterPhoneAccount(getPhoneAccountHandle());
411
+            }
412
+        }
413
+
414
+        /**
415
+         * Retrieves the UUID of the call associated with this connection.
416
+         *
417
+         * @return call UUID
418
+         */
419
+        String getCallUUID() {
420
+            return getPhoneAccountHandle().getId();
421
+        }
422
+
423
+        private PhoneAccountHandle getPhoneAccountHandle() {
424
+            return getExtras().getParcelable(
425
+                    ConnectionService.EXTRA_PHONE_ACCOUNT_HANDLE);
426
+        }
427
+
428
+        @Override
429
+        public String toString() {
430
+            return String.format(
431
+                    "ConnectionImpl[adress=%s, uuid=%s]@%d",
432
+                    getAddress(), getCallUUID(), hashCode());
433
+        }
434
+    }
435
+}

+ 165
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java ファイルの表示

1
+package org.jitsi.meet.sdk;
2
+
3
+import android.annotation.SuppressLint;
4
+import android.content.Context;
5
+import android.net.Uri;
6
+import android.os.Build;
7
+import android.os.Bundle;
8
+import android.support.annotation.RequiresApi;
9
+import android.telecom.DisconnectCause;
10
+import android.telecom.PhoneAccount;
11
+import android.telecom.PhoneAccountHandle;
12
+import android.telecom.TelecomManager;
13
+import android.telecom.VideoProfile;
14
+import android.util.Log;
15
+
16
+import com.facebook.react.bridge.Promise;
17
+import com.facebook.react.bridge.ReactApplicationContext;
18
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
19
+import com.facebook.react.bridge.ReactMethod;
20
+import com.facebook.react.bridge.ReadableMap;
21
+
22
+/**
23
+ * The react-native side of Jitsi Meet's {@link ConnectionService}. Exposes
24
+ * the Java Script API.
25
+ *
26
+ * @author Pawel Domas
27
+ */
28
+@RequiresApi(api = Build.VERSION_CODES.O)
29
+class RNConnectionService
30
+    extends ReactContextBaseJavaModule {
31
+
32
+    private final static String TAG = ConnectionService.TAG;
33
+
34
+    /**
35
+     * Sets the audio route on all existing {@link android.telecom.Connection}s
36
+     *
37
+     * @param audioRoute the new audio route to be set. See
38
+     * {@link android.telecom.CallAudioState} constants prefixed with "ROUTE_".
39
+     */
40
+    @RequiresApi(api = Build.VERSION_CODES.O)
41
+    static void setAudioRoute(int audioRoute) {
42
+        for (ConnectionService.ConnectionImpl c
43
+                : ConnectionService.getConnections()) {
44
+            c.setAudioRoute(audioRoute);
45
+        }
46
+    }
47
+
48
+    RNConnectionService(ReactApplicationContext reactContext) {
49
+        super(reactContext);
50
+    }
51
+
52
+    /**
53
+     * Starts a new outgoing call.
54
+     *
55
+     * @param callUUID - unique call identifier assigned by Jitsi Meet to
56
+     *        a conference call.
57
+     * @param handle - a call handle which by default is Jitsi Meet room's URL.
58
+     * @param hasVideo - whether or not user starts with the video turned on.
59
+     * @param promise - the Promise instance passed by the React-native bridge,
60
+     *        so that this method returns a Promise on the JS side.
61
+     *
62
+     * NOTE regarding the "missingPermission" suppress - SecurityException will
63
+     * be handled as part of the Exception try catch block and the Promise will
64
+     * be rejected.
65
+     */
66
+    @SuppressLint("MissingPermission")
67
+    @ReactMethod
68
+    public void startCall(
69
+            String callUUID,
70
+            String handle,
71
+            boolean hasVideo,
72
+            Promise promise) {
73
+        Log.d(TAG,
74
+              String.format("startCall UUID=%s, h=%s, v=%s",
75
+                            callUUID,
76
+                            handle,
77
+                            hasVideo));
78
+
79
+        ReactApplicationContext ctx = getReactApplicationContext();
80
+
81
+        Uri address = Uri.fromParts(PhoneAccount.SCHEME_SIP, handle, null);
82
+        PhoneAccountHandle accountHandle
83
+            = ConnectionService.registerPhoneAccount(
84
+                    getReactApplicationContext(), address, callUUID);
85
+
86
+        Bundle extras = new Bundle();
87
+        extras.putParcelable(
88
+                TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
89
+                accountHandle);
90
+        extras.putInt(
91
+            TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
92
+            hasVideo
93
+                ? VideoProfile.STATE_BIDIRECTIONAL
94
+                : VideoProfile.STATE_AUDIO_ONLY);
95
+
96
+        ConnectionService.registerStartCallPromise(callUUID, promise);
97
+
98
+        try {
99
+            TelecomManager tm
100
+                = (TelecomManager) ctx.getSystemService(
101
+                        Context.TELECOM_SERVICE);
102
+
103
+            tm.placeCall(address, extras);
104
+        } catch (Exception e) {
105
+            ConnectionService.unregisterStartCallPromise(callUUID);
106
+            promise.reject(e);
107
+        }
108
+    }
109
+
110
+    /**
111
+     * Called by the JS side of things to mark the call as failed.
112
+     *
113
+     * @param callUUID - the call's UUID.
114
+     */
115
+    @ReactMethod
116
+    public void reportCallFailed(String callUUID) {
117
+        Log.d(TAG, "reportCallFailed " + callUUID);
118
+        ConnectionService.setConnectionDisconnected(
119
+                callUUID,
120
+                new DisconnectCause(DisconnectCause.ERROR));
121
+    }
122
+
123
+    /**
124
+     * Called by the JS side of things to mark the call as disconnected.
125
+     *
126
+     * @param callUUID - the call's UUID.
127
+     */
128
+    @ReactMethod
129
+    public void endCall(String callUUID) {
130
+        Log.d(TAG, "endCall " + callUUID);
131
+        ConnectionService.setConnectionDisconnected(
132
+                callUUID,
133
+                new DisconnectCause(DisconnectCause.LOCAL));
134
+    }
135
+
136
+    /**
137
+     * Called by the JS side of things to mark the call as active.
138
+     *
139
+     * @param callUUID - the call's UUID.
140
+     */
141
+    @ReactMethod
142
+    public void reportConnectedOutgoingCall(String callUUID) {
143
+        Log.d(TAG, "reportConnectedOutgoingCall " + callUUID);
144
+        ConnectionService.setConnectionActive(callUUID);
145
+    }
146
+
147
+    @Override
148
+    public String getName() {
149
+        return "ConnectionService";
150
+    }
151
+
152
+    /**
153
+     * Called by the JS side to update the call's state.
154
+     *
155
+     * @param callUUID - the call's UUID.
156
+     * @param callState - the map which carries infor about the current call's
157
+     * state. See static fields in {@link ConnectionService.ConnectionImpl}
158
+     * prefixed with "KEY_" for the values supported by the Android
159
+     * implementation.
160
+     */
161
+    @ReactMethod
162
+    public void updateCall(String callUUID, ReadableMap callState) {
163
+        ConnectionService.updateCall(callUUID, callState);
164
+    }
165
+}

+ 27
- 14
android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java ファイルの表示

25
 import com.facebook.react.bridge.ReactApplicationContext;
25
 import com.facebook.react.bridge.ReactApplicationContext;
26
 import com.facebook.react.common.LifecycleState;
26
 import com.facebook.react.common.LifecycleState;
27
 
27
 
28
+import java.util.ArrayList;
28
 import java.util.Arrays;
29
 import java.util.Arrays;
29
 import java.util.List;
30
 import java.util.List;
30
 
31
 
31
 class ReactInstanceManagerHolder {
32
 class ReactInstanceManagerHolder {
32
     /**
33
     /**
34
+     * FIXME (from linter): Do not place Android context classes in static
35
+     * fields (static reference to ReactInstanceManager which has field
36
+     * mApplicationContext pointing to Context); this is a memory leak (and
37
+     * also breaks Instant Run).
38
+     *
33
      * React Native bridge. The instance manager allows embedding applications
39
      * React Native bridge. The instance manager allows embedding applications
34
      * to create multiple root views off the same JavaScript bundle.
40
      * to create multiple root views off the same JavaScript bundle.
35
      */
41
      */
37
 
43
 
38
     private static List<NativeModule> createNativeModules(
44
     private static List<NativeModule> createNativeModules(
39
             ReactApplicationContext reactContext) {
45
             ReactApplicationContext reactContext) {
40
-        return Arrays.<NativeModule>asList(
41
-            new AndroidSettingsModule(reactContext),
42
-            new AppInfoModule(reactContext),
43
-            new AudioModeModule(reactContext),
44
-            new ExternalAPIModule(reactContext),
45
-            new LocaleDetector(reactContext),
46
-            new PictureInPictureModule(reactContext),
47
-            new ProximityModule(reactContext),
48
-            new WiFiStatsModule(reactContext),
49
-            new org.jitsi.meet.sdk.dropbox.Dropbox(reactContext),
50
-            new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
51
-            new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
52
-        );
46
+        List<NativeModule> nativeModules
47
+            = new ArrayList<>(Arrays.<NativeModule>asList(
48
+                new AndroidSettingsModule(reactContext),
49
+                new AppInfoModule(reactContext),
50
+                new AudioModeModule(reactContext),
51
+                new ExternalAPIModule(reactContext),
52
+                new LocaleDetector(reactContext),
53
+                new PictureInPictureModule(reactContext),
54
+                new ProximityModule(reactContext),
55
+                new WiFiStatsModule(reactContext),
56
+                new org.jitsi.meet.sdk.dropbox.Dropbox(reactContext),
57
+                new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
58
+                new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));
59
+
60
+        if (android.os.Build.VERSION.SDK_INT
61
+                >= android.os.Build.VERSION_CODES.O) {
62
+            nativeModules.add(new RNConnectionService(reactContext));
63
+        }
64
+
65
+        return nativeModules;
53
     }
66
     }
54
 
67
 
55
     /**
68
     /**
58
      * @param eventName {@code String} containing the event name.
71
      * @param eventName {@code String} containing the event name.
59
      * @param data {@code Object} optional ancillary data for the event.
72
      * @param data {@code Object} optional ancillary data for the event.
60
      */
73
      */
61
-    public static boolean emitEvent(
74
+    static boolean emitEvent(
62
             String eventName,
75
             String eventName,
63
             @Nullable Object data) {
76
             @Nullable Object data) {
64
         ReactInstanceManager reactInstanceManager
77
         ReactInstanceManager reactInstanceManager

+ 1
- 1
ios/sdk/sdk.xcodeproj/project.pbxproj ファイルの表示

73
 		0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
73
 		0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
74
 		0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = "<group>"; };
74
 		0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = "<group>"; };
75
 		0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = "<group>"; };
75
 		0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = "<group>"; };
76
-		0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = CallKitIcon.png; path = ../../react/features/mobile/callkit/CallKitIcon.png; sourceTree = "<group>"; };
76
+		0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = CallKitIcon.png; path = ../../react/features/mobile/call-integration/CallKitIcon.png; sourceTree = "<group>"; };
77
 		0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = "<group>"; };
77
 		0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = "<group>"; };
78
 		0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = "<group>"; };
78
 		0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = "<group>"; };
79
 		0BCA495E1EC4B6C600B793EE /* Proximity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Proximity.m; sourceTree = "<group>"; };
79
 		0BCA495E1EC4B6C600B793EE /* Proximity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Proximity.m; sourceTree = "<group>"; };

+ 1
- 1
react/features/app/components/App.native.js ファイルの表示

15
 import '../../google-api';
15
 import '../../google-api';
16
 import '../../mobile/audio-mode';
16
 import '../../mobile/audio-mode';
17
 import '../../mobile/background';
17
 import '../../mobile/background';
18
-import '../../mobile/callkit';
18
+import '../../mobile/call-integration';
19
 import '../../mobile/external-api';
19
 import '../../mobile/external-api';
20
 import '../../mobile/full-screen';
20
 import '../../mobile/full-screen';
21
 import '../../mobile/permissions';
21
 import '../../mobile/permissions';

react/features/mobile/callkit/CallKit.js → react/features/mobile/call-integration/CallKit.js ファイルの表示

1
 import { NativeModules, NativeEventEmitter } from 'react-native';
1
 import { NativeModules, NativeEventEmitter } from 'react-native';
2
 
2
 
3
+import { getName } from '../../app';
4
+
3
 /**
5
 /**
4
  * Thin wrapper around Apple's CallKit functionality.
6
  * Thin wrapper around Apple's CallKit functionality.
5
  *
7
  *
32
 
34
 
33
     CallKit = {
35
     CallKit = {
34
         ...CallKit,
36
         ...CallKit,
35
-        addListener: eventEmitter.addListener.bind(eventEmitter)
37
+        addListener: eventEmitter.addListener.bind(eventEmitter),
38
+        registerSubscriptions(context, delegate) {
39
+            CallKit.setProviderConfiguration({
40
+                iconTemplateImageName: 'CallKitIcon',
41
+                localizedName: getName()
42
+            });
43
+
44
+            return [
45
+                CallKit.addListener(
46
+                    'performEndCallAction',
47
+                    delegate._onPerformEndCallAction,
48
+                    context),
49
+                CallKit.addListener(
50
+                    'performSetMutedCallAction',
51
+                    delegate._onPerformSetMutedCallAction,
52
+                    context),
53
+
54
+                // According to CallKit's documentation, when the system resets
55
+                // we should terminate all calls. Hence, providerDidReset is
56
+                // the same to us as performEndCallAction.
57
+                CallKit.addListener(
58
+                    'providerDidReset',
59
+                    delegate._onPerformEndCallAction,
60
+                    context)
61
+            ];
62
+        }
36
     };
63
     };
37
 }
64
 }
38
 
65
 

react/features/mobile/callkit/CallKitIcon.png → react/features/mobile/call-integration/CallKitIcon.png ファイルの表示


+ 33
- 0
react/features/mobile/call-integration/ConnectionService.js ファイルの表示

1
+import { NativeEventEmitter, NativeModules } from 'react-native';
2
+
3
+let ConnectionService = NativeModules.ConnectionService;
4
+
5
+// XXX Rather than wrapping ConnectionService in a new class and forwarding
6
+// the many methods of the latter to the former, add the one additional
7
+// method that we need to ConnectionService.
8
+if (ConnectionService) {
9
+    const eventEmitter = new NativeEventEmitter(ConnectionService);
10
+
11
+    ConnectionService = {
12
+        ...ConnectionService,
13
+        addListener: eventEmitter.addListener.bind(eventEmitter),
14
+        registerSubscriptions(context, delegate) {
15
+            return [
16
+                ConnectionService.addListener(
17
+                    'org.jitsi.meet:features/connection_service#disconnect',
18
+                    delegate._onPerformEndCallAction,
19
+                    context),
20
+                ConnectionService.addListener(
21
+                    'org.jitsi.meet:features/connection_service#abort',
22
+                    delegate._onPerformEndCallAction,
23
+                    context)
24
+            ];
25
+        },
26
+        setMuted() {
27
+            // Currently no-op, but remember to remove when implemented on
28
+            // the native side
29
+        }
30
+    };
31
+}
32
+
33
+export default ConnectionService;

+ 13
- 0
react/features/mobile/call-integration/actionTypes.js ファイルの表示

1
+/**
2
+ * The type of redux action to set CallKit's and ConnectionService's event
3
+ * subscriptions.
4
+ *
5
+ * {
6
+ *     type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
7
+ *     subscriptions: Array|undefined
8
+ * }
9
+ *
10
+ * @protected
11
+ */
12
+export const _SET_CALL_INTEGRATION_SUBSCRIPTIONS
13
+    = Symbol('_SET_CALL_INTEGRATION_SUBSCRIPTIONS');

react/features/mobile/callkit/index.js → react/features/mobile/call-integration/index.js ファイルの表示


react/features/mobile/callkit/middleware.js → react/features/mobile/call-integration/middleware.js ファイルの表示

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { Alert } from 'react-native';
3
 import uuid from 'uuid';
4
 import uuid from 'uuid';
4
 
5
 
5
 import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
6
 import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
6
-import { appNavigate, getName } from '../../app';
7
+import { appNavigate } from '../../app';
7
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
8
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
8
 import {
9
 import {
9
     CONFERENCE_FAILED,
10
     CONFERENCE_FAILED,
27
     isLocalTrackMuted
28
     isLocalTrackMuted
28
 } from '../../base/tracks';
29
 } from '../../base/tracks';
29
 
30
 
30
-import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
31
+import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
32
+
31
 import CallKit from './CallKit';
33
 import CallKit from './CallKit';
34
+import ConnectionService from './ConnectionService';
35
+
36
+const CallIntegration = CallKit || ConnectionService;
32
 
37
 
33
 /**
38
 /**
34
  * Middleware that captures system actions and hooks up CallKit.
39
  * Middleware that captures system actions and hooks up CallKit.
36
  * @param {Store} store - The redux store.
41
  * @param {Store} store - The redux store.
37
  * @returns {Function}
42
  * @returns {Function}
38
  */
43
  */
39
-CallKit && MiddlewareRegistry.register(store => next => action => {
44
+CallIntegration && MiddlewareRegistry.register(store => next => action => {
40
     switch (action.type) {
45
     switch (action.type) {
41
-    case _SET_CALLKIT_SUBSCRIPTIONS:
46
+    case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
42
         return _setCallKitSubscriptions(store, next, action);
47
         return _setCallKitSubscriptions(store, next, action);
43
 
48
 
44
     case APP_WILL_MOUNT:
49
     case APP_WILL_MOUNT:
46
 
51
 
47
     case APP_WILL_UNMOUNT:
52
     case APP_WILL_UNMOUNT:
48
         store.dispatch({
53
         store.dispatch({
49
-            type: _SET_CALLKIT_SUBSCRIPTIONS,
54
+            type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
50
             subscriptions: undefined
55
             subscriptions: undefined
51
         });
56
         });
52
         break;
57
         break;
91
 function _appWillMount({ dispatch, getState }, next, action) {
96
 function _appWillMount({ dispatch, getState }, next, action) {
92
     const result = next(action);
97
     const result = next(action);
93
 
98
 
94
-    CallKit.setProviderConfiguration({
95
-        iconTemplateImageName: 'CallKitIcon',
96
-        localizedName: getName()
97
-    });
98
-
99
     const context = {
99
     const context = {
100
         dispatch,
100
         dispatch,
101
         getState
101
         getState
102
     };
102
     };
103
-    const subscriptions = [
104
-        CallKit.addListener(
105
-            'performEndCallAction',
106
-            _onPerformEndCallAction,
107
-            context),
108
-        CallKit.addListener(
109
-            'performSetMutedCallAction',
110
-            _onPerformSetMutedCallAction,
111
-            context),
112
-
113
-        // According to CallKit's documentation, when the system resets we
114
-        // should terminate all calls. Hence, providerDidReset is the same to us
115
-        // as performEndCallAction.
116
-        CallKit.addListener(
117
-            'providerDidReset',
118
-            _onPerformEndCallAction,
119
-            context)
120
-    ];
121
-
122
-    dispatch({
123
-        type: _SET_CALLKIT_SUBSCRIPTIONS,
103
+
104
+    const delegate = {
105
+        _onPerformSetMutedCallAction,
106
+        _onPerformEndCallAction
107
+    };
108
+
109
+    const subscriptions
110
+        = CallIntegration.registerSubscriptions(context, delegate);
111
+
112
+    subscriptions && dispatch({
113
+        type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
124
         subscriptions
114
         subscriptions
125
     });
115
     });
126
 
116
 
150
         const { callUUID } = action.conference;
140
         const { callUUID } = action.conference;
151
 
141
 
152
         if (callUUID) {
142
         if (callUUID) {
153
-            CallKit.reportCallFailed(callUUID);
143
+            CallIntegration.reportCallFailed(callUUID);
154
         }
144
         }
155
     }
145
     }
156
 
146
 
176
     const { callUUID } = action.conference;
166
     const { callUUID } = action.conference;
177
 
167
 
178
     if (callUUID) {
168
     if (callUUID) {
179
-        CallKit.reportConnectedOutgoingCall(callUUID);
169
+        CallIntegration.reportConnectedOutgoingCall(callUUID);
180
     }
170
     }
181
 
171
 
182
     return result;
172
     return result;
201
     const { callUUID } = action.conference;
191
     const { callUUID } = action.conference;
202
 
192
 
203
     if (callUUID) {
193
     if (callUUID) {
204
-        CallKit.endCall(callUUID);
194
+        CallIntegration.endCall(callUUID);
205
     }
195
     }
206
 
196
 
207
     return result;
197
     return result;
220
  * @private
210
  * @private
221
  * @returns {*} The value returned by {@code next(action)}.
211
  * @returns {*} The value returned by {@code next(action)}.
222
  */
212
  */
223
-function _conferenceWillJoin({ getState }, next, action) {
213
+function _conferenceWillJoin({ dispatch, getState }, next, action) {
224
     const result = next(action);
214
     const result = next(action);
225
 
215
 
226
     const { conference } = action;
216
     const { conference } = action;
234
     // it upper cased.
224
     // it upper cased.
235
     conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
225
     conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
236
 
226
 
237
-    CallKit.startCall(conference.callUUID, handle, hasVideo)
227
+    CallIntegration.startCall(conference.callUUID, handle, hasVideo)
238
         .then(() => {
228
         .then(() => {
239
             const { callee } = state['features/base/jwt'];
229
             const { callee } = state['features/base/jwt'];
240
             const displayName
230
             const displayName
247
                     state['features/base/tracks'],
237
                     state['features/base/tracks'],
248
                     MEDIA_TYPE.AUDIO);
238
                     MEDIA_TYPE.AUDIO);
249
 
239
 
250
-            // eslint-disable-next-line object-property-newline
251
-            CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
252
-            CallKit.setMuted(conference.callUUID, muted);
240
+            CallIntegration.updateCall(
241
+                conference.callUUID,
242
+                {
243
+                    displayName,
244
+                    hasVideo
245
+                });
246
+            CallIntegration.setMuted(conference.callUUID, muted);
247
+        })
248
+        .catch(error => {
249
+            // Currently this error code is emitted only by Android.
250
+            if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
251
+                // We're not tracking the call anymore - it doesn't exist on
252
+                // the native side.
253
+                delete conference.callUUID;
254
+                dispatch(appNavigate(undefined));
255
+                Alert.alert(
256
+                    'Call aborted',
257
+                    'There\'s already another call in progress.'
258
+                        + ' Please end it first and try again.',
259
+                    [
260
+                        { text: 'OK' }
261
+                    ],
262
+                    { cancelable: false });
263
+            }
253
         });
264
         });
254
 
265
 
255
     return result;
266
     return result;
288
 
299
 
289
     if (conference && conference.callUUID === callUUID) {
300
     if (conference && conference.callUUID === callUUID) {
290
         muted = Boolean(muted); // eslint-disable-line no-param-reassign
301
         muted = Boolean(muted); // eslint-disable-line no-param-reassign
291
-        sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
302
+        sendAnalytics(
303
+            createTrackMutedEvent('audio', 'call-integration', muted));
292
         dispatch(setAudioMuted(muted, /* ensureTrack */ true));
304
         dispatch(setAudioMuted(muted, /* ensureTrack */ true));
293
     }
305
     }
294
 }
306
 }
319
     const conference = getCurrentConference(state);
331
     const conference = getCurrentConference(state);
320
 
332
 
321
     if (conference && conference.callUUID) {
333
     if (conference && conference.callUUID) {
322
-        CallKit.updateCall(
334
+        CallIntegration.updateCall(
323
             conference.callUUID,
335
             conference.callUUID,
324
             { hasVideo: !action.audioOnly });
336
             { hasVideo: !action.audioOnly });
325
     }
337
     }
329
 
341
 
330
 /**
342
 /**
331
  * Notifies the feature callkit that the action
343
  * Notifies the feature callkit that the action
332
- * {@link _SET_CALLKIT_SUBSCRIPTIONS} is being dispatched within a specific
333
- * redux {@code store}.
344
+ * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
345
+ * a specific redux {@code store}.
334
  *
346
  *
335
  * @param {Store} store - The redux store in which the specified {@code action}
347
  * @param {Store} store - The redux store in which the specified {@code action}
336
  * is being dispatched.
348
  * is being dispatched.
337
  * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
349
  * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
338
  * specified {@code action} in the specified {@code store}.
350
  * specified {@code action} in the specified {@code store}.
339
- * @param {Action} action - The redux action {@code _SET_CALLKIT_SUBSCRIPTIONS}
340
- * which is being dispatched in the specified {@code store}.
351
+ * @param {Action} action - The redux action
352
+ * {@code _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
353
+ * the specified {@code store}.
341
  * @private
354
  * @private
342
  * @returns {*} The value returned by {@code next(action)}.
355
  * @returns {*} The value returned by {@code next(action)}.
343
  */
356
  */
344
 function _setCallKitSubscriptions({ getState }, next, action) {
357
 function _setCallKitSubscriptions({ getState }, next, action) {
345
-    const { subscriptions } = getState()['features/callkit'];
358
+    const { subscriptions } = getState()['features/call-integration'];
346
 
359
 
347
     if (subscriptions) {
360
     if (subscriptions) {
348
         for (const subscription of subscriptions) {
361
         for (const subscription of subscriptions) {
377
             const tracks = state['features/base/tracks'];
390
             const tracks = state['features/base/tracks'];
378
             const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
391
             const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
379
 
392
 
380
-            CallKit.setMuted(conference.callUUID, muted);
393
+            CallIntegration.setMuted(conference.callUUID, muted);
381
             break;
394
             break;
382
         }
395
         }
383
         case 'video': {
396
         case 'video': {
384
-            CallKit.updateCall(
397
+            CallIntegration.updateCall(
385
                 conference.callUUID,
398
                 conference.callUUID,
386
                 { hasVideo: !isVideoMutedByAudioOnly(state) });
399
                 { hasVideo: !isVideoMutedByAudioOnly(state) });
387
             break;
400
             break;

react/features/mobile/callkit/reducer.js → react/features/mobile/call-integration/reducer.js ファイルの表示

1
 import { assign, ReducerRegistry } from '../../base/redux';
1
 import { assign, ReducerRegistry } from '../../base/redux';
2
 
2
 
3
-import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
3
+import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
4
 import CallKit from './CallKit';
4
 import CallKit from './CallKit';
5
+import ConnectionService from './ConnectionService';
5
 
6
 
6
-CallKit && ReducerRegistry.register(
7
-    'features/callkit',
7
+(CallKit || ConnectionService) && ReducerRegistry.register(
8
+    'features/call-integration',
8
     (state = {}, action) => {
9
     (state = {}, action) => {
9
         switch (action.type) {
10
         switch (action.type) {
10
-        case _SET_CALLKIT_SUBSCRIPTIONS:
11
+        case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
11
             return assign(state, 'subscriptions', action.subscriptions);
12
             return assign(state, 'subscriptions', action.subscriptions);
12
         }
13
         }
13
 
14
 

+ 0
- 11
react/features/mobile/callkit/actionTypes.js ファイルの表示

1
-/**
2
- * The type of redux action to set CallKit's event subscriptions.
3
- *
4
- * {
5
- *     type: _SET_CALLKIT_SUBSCRIPTIONS,
6
- *     subscriptions: Array|undefined
7
- * }
8
- *
9
- * @protected
10
- */
11
-export const _SET_CALLKIT_SUBSCRIPTIONS = Symbol('_SET_CALLKIT_SUBSCRIPTIONS');

読み込み中…
キャンセル
保存