@capgo/capacitor-twilio-voice 7.3.2 → 7.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/android/src/main/java/ee/forgr/capacitor_twilio_voice/CapacitorTwilioVoicePlugin.java +541 -44
- package/dist/docs.json +89 -4
- package/dist/esm/definitions.d.ts +17 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorTwilioVoicePlugin/CapacitorTwilioVoicePlugin.swift +212 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
A Capacitor plugin for integrating Twilio Voice calling functionality into iOS and Android applications.
|
|
10
10
|
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
The most complete doc is available here: https://capgo.app/docs/plugins/twilio-voice/
|
|
14
|
+
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
13
17
|
```bash
|
package/android/src/main/java/ee/forgr/capacitor_twilio_voice/CapacitorTwilioVoicePlugin.java
CHANGED
|
@@ -2,6 +2,9 @@ package ee.forgr.capacitor_twilio_voice;
|
|
|
2
2
|
|
|
3
3
|
import android.Manifest;
|
|
4
4
|
import android.app.Activity;
|
|
5
|
+
import android.app.AlertDialog;
|
|
6
|
+
import android.app.KeyguardManager;
|
|
7
|
+
import android.app.KeyguardManager;
|
|
5
8
|
import android.app.NotificationChannel;
|
|
6
9
|
import android.app.NotificationManager;
|
|
7
10
|
import android.app.PendingIntent;
|
|
@@ -21,13 +24,18 @@ import android.os.Build;
|
|
|
21
24
|
import android.os.IBinder;
|
|
22
25
|
import android.os.VibrationEffect;
|
|
23
26
|
import android.os.Vibrator;
|
|
27
|
+
import android.provider.Settings;
|
|
24
28
|
import android.util.Base64;
|
|
25
29
|
import android.util.Log;
|
|
30
|
+
import android.widget.Toast;
|
|
31
|
+
import androidx.activity.result.ActivityResultLauncher;
|
|
32
|
+
import androidx.activity.result.contract.ActivityResultContracts;
|
|
26
33
|
import androidx.annotation.NonNull;
|
|
27
34
|
import androidx.annotation.Nullable;
|
|
28
35
|
import androidx.core.app.ActivityCompat;
|
|
29
36
|
import androidx.core.app.NotificationCompat;
|
|
30
37
|
import androidx.core.app.NotificationManagerCompat;
|
|
38
|
+
import androidx.core.content.ContextCompat;
|
|
31
39
|
import com.getcapacitor.JSArray;
|
|
32
40
|
import com.getcapacitor.JSObject;
|
|
33
41
|
import com.getcapacitor.Plugin;
|
|
@@ -67,11 +75,13 @@ import org.json.JSONObject;
|
|
|
67
75
|
)
|
|
68
76
|
public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
69
77
|
|
|
70
|
-
private final String PLUGIN_VERSION = "7.
|
|
78
|
+
private final String PLUGIN_VERSION = "7.4.2";
|
|
71
79
|
|
|
72
80
|
private static final String TAG = "CapacitorTwilioVoice";
|
|
73
81
|
private static final String PREF_ACCESS_TOKEN = "twilio_access_token";
|
|
74
82
|
private static final String PREF_FCM_TOKEN = "twilio_fcm_token";
|
|
83
|
+
private static final String PREFS_NAME = "capacitor_twilio_voice_prefs";
|
|
84
|
+
private static final String PREF_MIC_PERMISSION_REQUESTED = "mic_permission_requested";
|
|
75
85
|
|
|
76
86
|
public static CapacitorTwilioVoicePlugin instance;
|
|
77
87
|
|
|
@@ -103,6 +113,21 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
103
113
|
private static final int REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT = 2001;
|
|
104
114
|
private String pendingCallSidForPermission;
|
|
105
115
|
|
|
116
|
+
private enum PendingPermissionAction {
|
|
117
|
+
NONE,
|
|
118
|
+
OUTGOING_CALL,
|
|
119
|
+
ACCEPT_CALL
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private PendingPermissionAction pendingPermissionAction = PendingPermissionAction.NONE;
|
|
123
|
+
private PluginCall pendingOutgoingCall;
|
|
124
|
+
private String pendingOutgoingTo;
|
|
125
|
+
private PluginCall pendingPermissionCall;
|
|
126
|
+
private long permissionRequestTimestamp = 0L;
|
|
127
|
+
private int permissionAttemptCount = 0;
|
|
128
|
+
private boolean awaitingSettingsResult = false;
|
|
129
|
+
private ActivityResultLauncher<String[]> micPermissionLauncher;
|
|
130
|
+
|
|
106
131
|
// Voice Call Service
|
|
107
132
|
private VoiceCallService voiceCallService;
|
|
108
133
|
private boolean isServiceBound = false;
|
|
@@ -150,6 +175,7 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
150
175
|
data.put("error", error.getMessage());
|
|
151
176
|
}
|
|
152
177
|
notifyListeners("callDisconnected", data);
|
|
178
|
+
moveAppToBackgroundIfLocked();
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
@Override
|
|
@@ -240,6 +266,11 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
240
266
|
bindToVoiceCallService();
|
|
241
267
|
|
|
242
268
|
Log.d(TAG, "CapacitorTwilioVoice plugin loaded");
|
|
269
|
+
|
|
270
|
+
micPermissionLauncher = getBridge().registerForActivityResult(
|
|
271
|
+
new ActivityResultContracts.RequestMultiplePermissions(),
|
|
272
|
+
(permissions) -> handleMicPermissionResult(permissions)
|
|
273
|
+
);
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
private void bindToVoiceCallService() {
|
|
@@ -356,6 +387,44 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
356
387
|
Log.d(TAG, "CapacitorTwilioVoice plugin destroyed");
|
|
357
388
|
}
|
|
358
389
|
|
|
390
|
+
@Override
|
|
391
|
+
protected void handleOnResume() {
|
|
392
|
+
super.handleOnResume();
|
|
393
|
+
Log.d(
|
|
394
|
+
TAG,
|
|
395
|
+
"handleOnResume: hasPermission=" +
|
|
396
|
+
hasMicrophonePermission() +
|
|
397
|
+
", awaitingSettings=" +
|
|
398
|
+
awaitingSettingsResult +
|
|
399
|
+
", pendingCall=" +
|
|
400
|
+
(pendingPermissionCall != null) +
|
|
401
|
+
", pendingAction=" +
|
|
402
|
+
pendingPermissionAction
|
|
403
|
+
);
|
|
404
|
+
if (hasMicrophonePermission()) {
|
|
405
|
+
if (pendingPermissionAction != PendingPermissionAction.NONE || pendingPermissionCall != null) {
|
|
406
|
+
awaitingSettingsResult = false;
|
|
407
|
+
Log.d(TAG, "handleOnResume: permission granted, resuming pending flow");
|
|
408
|
+
handleMicrophonePermissionGranted();
|
|
409
|
+
}
|
|
410
|
+
} else if (awaitingSettingsResult && pendingPermissionCall != null) {
|
|
411
|
+
awaitingSettingsResult = false;
|
|
412
|
+
Log.d(TAG, "handleOnResume: permission still denied after returning from settings");
|
|
413
|
+
JSObject ret = new JSObject();
|
|
414
|
+
ret.put("granted", false);
|
|
415
|
+
pendingPermissionCall.setKeepAlive(false);
|
|
416
|
+
pendingPermissionCall.resolve(ret);
|
|
417
|
+
pendingPermissionCall = null;
|
|
418
|
+
} else if (awaitingSettingsResult && pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
419
|
+
awaitingSettingsResult = false;
|
|
420
|
+
Log.d(TAG, "handleOnResume: settings return without permission for accept flow");
|
|
421
|
+
handlePermissionFailure();
|
|
422
|
+
} else if (pendingPermissionCall != null) {
|
|
423
|
+
Log.d(TAG, "handleOnResume: permission denied from dialog, invoking fallback handling");
|
|
424
|
+
handleMicrophonePermissionDenied();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
359
428
|
public void setInjectedContext(Context injectedContext) {
|
|
360
429
|
this.injectedContext = injectedContext;
|
|
361
430
|
}
|
|
@@ -388,33 +457,19 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
388
457
|
}
|
|
389
458
|
|
|
390
459
|
private void ensureMicPermissionThenAccept(String callSid) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (granted) {
|
|
460
|
+
Log.d(TAG, "ensureMicPermissionThenAccept: callSid=" + callSid);
|
|
461
|
+
if (hasMicrophonePermission()) {
|
|
462
|
+
Log.d(TAG, "ensureMicPermissionThenAccept: permission granted, proceeding");
|
|
395
463
|
proceedAcceptCall(callSid);
|
|
396
464
|
return;
|
|
397
465
|
}
|
|
398
466
|
|
|
399
|
-
// Remember callSid and request via Activity runtime permission API
|
|
400
467
|
pendingCallSidForPermission = callSid;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT
|
|
407
|
-
);
|
|
408
|
-
} else if (mainActivityClass != null) {
|
|
409
|
-
// No current activity; bring app to foreground where permission can be requested
|
|
410
|
-
Intent launchIntent = new Intent(context, mainActivityClass);
|
|
411
|
-
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
412
|
-
launchIntent.putExtra("AUTO_ACCEPT_CALL", true);
|
|
413
|
-
launchIntent.putExtra(EXTRA_CALL_SID, callSid);
|
|
414
|
-
context.startActivity(launchIntent);
|
|
415
|
-
} else {
|
|
416
|
-
Log.w(TAG, "No activity available to request RECORD_AUDIO permission");
|
|
417
|
-
}
|
|
468
|
+
pendingPermissionAction = PendingPermissionAction.ACCEPT_CALL;
|
|
469
|
+
permissionAttemptCount = 0;
|
|
470
|
+
awaitingSettingsResult = false;
|
|
471
|
+
Log.d(TAG, "ensureMicPermissionThenAccept: requesting permission before accepting call");
|
|
472
|
+
requestMicrophonePermission();
|
|
418
473
|
}
|
|
419
474
|
|
|
420
475
|
// Helper to actually start the service once permission is granted
|
|
@@ -435,6 +490,12 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
435
490
|
Log.d(TAG, "Call acceptance started via service (permission granted)");
|
|
436
491
|
} catch (Exception e) {
|
|
437
492
|
Log.e(TAG, "Error accepting call via service", e);
|
|
493
|
+
} finally {
|
|
494
|
+
pendingCallSidForPermission = null;
|
|
495
|
+
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
496
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
497
|
+
}
|
|
498
|
+
permissionAttemptCount = 0;
|
|
438
499
|
}
|
|
439
500
|
}
|
|
440
501
|
|
|
@@ -683,11 +744,32 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
683
744
|
return;
|
|
684
745
|
}
|
|
685
746
|
|
|
747
|
+
if (pendingOutgoingCall != null) {
|
|
748
|
+
pendingOutgoingCall.setKeepAlive(false);
|
|
749
|
+
pendingOutgoingCall.reject("Another call is awaiting microphone permission.");
|
|
750
|
+
clearOutgoingPermissionState();
|
|
751
|
+
}
|
|
752
|
+
|
|
686
753
|
String to = call.getString("to");
|
|
687
754
|
if (to == null) {
|
|
688
755
|
to = ""; // Empty string for echo test
|
|
689
756
|
}
|
|
690
757
|
|
|
758
|
+
if (hasMicrophonePermission()) {
|
|
759
|
+
startOutgoingCall(call, to);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
pendingOutgoingCall = call;
|
|
764
|
+
pendingOutgoingTo = to;
|
|
765
|
+
pendingPermissionAction = PendingPermissionAction.OUTGOING_CALL;
|
|
766
|
+
permissionAttemptCount = 0;
|
|
767
|
+
call.setKeepAlive(true);
|
|
768
|
+
requestMicrophonePermission();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private void startOutgoingCall(PluginCall call, String to) {
|
|
772
|
+
Log.d(TAG, "startOutgoingCall: to=" + to);
|
|
691
773
|
// Start call via the foreground service
|
|
692
774
|
Intent serviceIntent = new Intent(getSafeContext(), VoiceCallService.class);
|
|
693
775
|
serviceIntent.setAction(VoiceCallService.ACTION_START_CALL);
|
|
@@ -700,11 +782,366 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
700
782
|
JSObject ret = new JSObject();
|
|
701
783
|
ret.put("success", true);
|
|
702
784
|
ret.put("callSid", "pending"); // Will be updated when service connects
|
|
785
|
+
call.setKeepAlive(false);
|
|
703
786
|
call.resolve(ret);
|
|
704
787
|
} catch (Exception e) {
|
|
788
|
+
call.setKeepAlive(false);
|
|
705
789
|
Log.e(TAG, "Error starting call service", e);
|
|
706
790
|
call.reject("Failed to start call: " + e.getMessage());
|
|
791
|
+
} finally {
|
|
792
|
+
clearOutgoingPermissionState();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private boolean hasMicrophonePermission() {
|
|
797
|
+
return ContextCompat.checkSelfPermission(getSafeContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private SharedPreferences getPrefs() {
|
|
801
|
+
return getSafeContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private void requestMicrophonePermission() {
|
|
805
|
+
Activity activity = getActivity();
|
|
806
|
+
permissionRequestTimestamp = System.currentTimeMillis();
|
|
807
|
+
permissionAttemptCount++;
|
|
808
|
+
|
|
809
|
+
if (activity != null) {
|
|
810
|
+
activity.runOnUiThread(() -> {
|
|
811
|
+
Log.d(TAG, "requestMicrophonePermission: requesting RECORD_AUDIO (attempt " + permissionAttemptCount + ")");
|
|
812
|
+
if (micPermissionLauncher != null) {
|
|
813
|
+
micPermissionLauncher.launch(new String[] { Manifest.permission.RECORD_AUDIO });
|
|
814
|
+
} else {
|
|
815
|
+
ActivityCompat.requestPermissions(
|
|
816
|
+
activity,
|
|
817
|
+
new String[] { Manifest.permission.RECORD_AUDIO },
|
|
818
|
+
REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (mainActivityClass != null) {
|
|
826
|
+
Context context = getSafeContext();
|
|
827
|
+
Intent launchIntent = new Intent(context, mainActivityClass);
|
|
828
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
829
|
+
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL && pendingCallSidForPermission != null) {
|
|
830
|
+
launchIntent.putExtra("AUTO_ACCEPT_CALL", true);
|
|
831
|
+
launchIntent.putExtra(EXTRA_CALL_SID, pendingCallSidForPermission);
|
|
832
|
+
}
|
|
833
|
+
Log.d(TAG, "requestMicrophonePermission: launching activity to request permission");
|
|
834
|
+
context.startActivity(launchIntent);
|
|
835
|
+
} else {
|
|
836
|
+
Log.w(TAG, "Unable to request microphone permission - no activity available");
|
|
837
|
+
handlePermissionFailure();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private void handleMicPermissionResult(Map<String, Boolean> permissions) {
|
|
842
|
+
Boolean granted = permissions.get(Manifest.permission.RECORD_AUDIO);
|
|
843
|
+
Log.d(TAG, "handleMicPermissionResult: granted=" + granted + ", pendingAction=" + pendingPermissionAction);
|
|
844
|
+
if (granted != null && granted) {
|
|
845
|
+
handleMicrophonePermissionGranted();
|
|
846
|
+
} else {
|
|
847
|
+
handleMicrophonePermissionDenied();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private void handleMicrophonePermissionGranted() {
|
|
852
|
+
Log.d(
|
|
853
|
+
TAG,
|
|
854
|
+
"handleMicrophonePermissionGranted: pendingAction=" +
|
|
855
|
+
pendingPermissionAction +
|
|
856
|
+
", pendingCall=" +
|
|
857
|
+
(pendingPermissionCall != null)
|
|
858
|
+
);
|
|
859
|
+
permissionAttemptCount = 0;
|
|
860
|
+
|
|
861
|
+
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL && pendingOutgoingCall != null) {
|
|
862
|
+
PluginCall call = pendingOutgoingCall;
|
|
863
|
+
String to = pendingOutgoingTo != null ? pendingOutgoingTo : "";
|
|
864
|
+
pendingOutgoingCall = null;
|
|
865
|
+
pendingOutgoingTo = null;
|
|
866
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
867
|
+
startOutgoingCall(call, to);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
872
|
+
if (pendingCallSidForPermission != null) {
|
|
873
|
+
String callSid = pendingCallSidForPermission;
|
|
874
|
+
pendingCallSidForPermission = null;
|
|
875
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
876
|
+
proceedAcceptCall(callSid);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
881
|
+
awaitingSettingsResult = false;
|
|
882
|
+
permissionAttemptCount = 0;
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (pendingPermissionCall != null) {
|
|
887
|
+
JSObject ret = new JSObject();
|
|
888
|
+
ret.put("granted", true);
|
|
889
|
+
pendingPermissionCall.setKeepAlive(false);
|
|
890
|
+
pendingPermissionCall.resolve(ret);
|
|
891
|
+
pendingPermissionCall = null;
|
|
892
|
+
awaitingSettingsResult = false;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private void handleMicrophonePermissionDenied() {
|
|
899
|
+
Log.d(TAG, "handleMicrophonePermissionDenied invoked");
|
|
900
|
+
Activity activity = getActivity();
|
|
901
|
+
if (activity == null) {
|
|
902
|
+
Log.w(TAG, "handleMicrophonePermissionDenied: no activity");
|
|
903
|
+
handlePermissionFailure();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
boolean canRequestAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO);
|
|
908
|
+
Log.d(
|
|
909
|
+
TAG,
|
|
910
|
+
"handleMicrophonePermissionDenied: canRequestAgain=" +
|
|
911
|
+
canRequestAgain +
|
|
912
|
+
", attempt=" +
|
|
913
|
+
permissionAttemptCount +
|
|
914
|
+
", pendingAction=" +
|
|
915
|
+
pendingPermissionAction +
|
|
916
|
+
", standaloneCall=" +
|
|
917
|
+
(pendingPermissionCall != null)
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
if (pendingPermissionAction == PendingPermissionAction.NONE && pendingPermissionCall != null) {
|
|
921
|
+
if (canRequestAgain && permissionAttemptCount <= 1) {
|
|
922
|
+
showStandalonePermissionRationaleDialog(activity, pendingPermissionCall);
|
|
923
|
+
} else {
|
|
924
|
+
showStandalonePermissionSettingsDialog(activity, pendingPermissionCall);
|
|
925
|
+
}
|
|
926
|
+
} else if (canRequestAgain && permissionAttemptCount <= 1) {
|
|
927
|
+
showPermissionRationaleDialog(activity);
|
|
928
|
+
} else {
|
|
929
|
+
showPermissionSettingsDialog(activity);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private void showPermissionRationaleDialog(Activity activity) {
|
|
934
|
+
Log.d(TAG, "showPermissionRationaleDialog");
|
|
935
|
+
new AlertDialog.Builder(activity)
|
|
936
|
+
.setTitle("Microphone required")
|
|
937
|
+
.setMessage("Microphone access is required to place and receive calls.")
|
|
938
|
+
.setPositiveButton("Retry", (dialog, which) -> {
|
|
939
|
+
dialog.dismiss();
|
|
940
|
+
requestMicrophonePermission();
|
|
941
|
+
})
|
|
942
|
+
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
943
|
+
dialog.dismiss();
|
|
944
|
+
handlePermissionFailure();
|
|
945
|
+
})
|
|
946
|
+
.setCancelable(false)
|
|
947
|
+
.show();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private void showStandalonePermissionRationaleDialog(Activity activity, PluginCall call) {
|
|
951
|
+
Log.d(TAG, "showStandalonePermissionRationaleDialog");
|
|
952
|
+
new AlertDialog.Builder(activity)
|
|
953
|
+
.setTitle("Microphone required")
|
|
954
|
+
.setMessage("Microphone access is required to place and receive calls.")
|
|
955
|
+
.setPositiveButton("Retry", (dialog, which) -> {
|
|
956
|
+
dialog.dismiss();
|
|
957
|
+
requestMicrophonePermission();
|
|
958
|
+
})
|
|
959
|
+
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
960
|
+
dialog.dismiss();
|
|
961
|
+
JSObject ret = new JSObject();
|
|
962
|
+
ret.put("granted", false);
|
|
963
|
+
call.setKeepAlive(false);
|
|
964
|
+
call.resolve(ret);
|
|
965
|
+
pendingPermissionCall = null;
|
|
966
|
+
awaitingSettingsResult = false;
|
|
967
|
+
permissionAttemptCount = 0;
|
|
968
|
+
})
|
|
969
|
+
.setCancelable(false)
|
|
970
|
+
.show();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private void showPermissionSettingsDialog(Activity activity) {
|
|
974
|
+
Log.d(TAG, "showPermissionSettingsDialog");
|
|
975
|
+
new AlertDialog.Builder(activity)
|
|
976
|
+
.setTitle("Enable microphone")
|
|
977
|
+
.setMessage("You can enable the microphone in Settings to use calling features.")
|
|
978
|
+
.setPositiveButton("Open Settings", (dialog, which) -> {
|
|
979
|
+
dialog.dismiss();
|
|
980
|
+
ensureUnlockedThenOpenSettings();
|
|
981
|
+
})
|
|
982
|
+
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
983
|
+
dialog.dismiss();
|
|
984
|
+
handlePermissionFailure();
|
|
985
|
+
})
|
|
986
|
+
.setCancelable(false)
|
|
987
|
+
.show();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private void showStandalonePermissionSettingsDialog(Activity activity, PluginCall call) {
|
|
991
|
+
Log.d(TAG, "showStandalonePermissionSettingsDialog");
|
|
992
|
+
new AlertDialog.Builder(activity)
|
|
993
|
+
.setTitle("Enable microphone")
|
|
994
|
+
.setMessage("Microphone access is required. Open Settings to enable the permission.")
|
|
995
|
+
.setPositiveButton("Open Settings", (dialog, which) -> {
|
|
996
|
+
dialog.dismiss();
|
|
997
|
+
pendingPermissionCall = call;
|
|
998
|
+
call.setKeepAlive(true);
|
|
999
|
+
awaitingSettingsResult = true;
|
|
1000
|
+
ensureUnlockedThenOpenSettings();
|
|
1001
|
+
})
|
|
1002
|
+
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
1003
|
+
dialog.dismiss();
|
|
1004
|
+
JSObject ret = new JSObject();
|
|
1005
|
+
ret.put("granted", false);
|
|
1006
|
+
call.setKeepAlive(false);
|
|
1007
|
+
call.resolve(ret);
|
|
1008
|
+
pendingPermissionCall = null;
|
|
1009
|
+
awaitingSettingsResult = false;
|
|
1010
|
+
permissionAttemptCount = 0;
|
|
1011
|
+
})
|
|
1012
|
+
.setCancelable(false)
|
|
1013
|
+
.show();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private void openAppSettings() {
|
|
1017
|
+
Context context = getSafeContext();
|
|
1018
|
+
if (pendingPermissionCall != null || pendingPermissionAction != PendingPermissionAction.NONE) {
|
|
1019
|
+
awaitingSettingsResult = true;
|
|
1020
|
+
}
|
|
1021
|
+
Log.d(
|
|
1022
|
+
TAG,
|
|
1023
|
+
"openAppSettings: awaitingSettingsResult=" +
|
|
1024
|
+
awaitingSettingsResult +
|
|
1025
|
+
", pendingAction=" +
|
|
1026
|
+
pendingPermissionAction +
|
|
1027
|
+
", pendingCall=" +
|
|
1028
|
+
(pendingPermissionCall != null)
|
|
1029
|
+
);
|
|
1030
|
+
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
|
1031
|
+
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
|
|
1032
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
1033
|
+
context.startActivity(intent);
|
|
1034
|
+
Toast.makeText(context, "Enable microphone permission and return to the app", Toast.LENGTH_LONG).show();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private boolean isDeviceLocked() {
|
|
1038
|
+
KeyguardManager km = (KeyguardManager) getSafeContext().getSystemService(Context.KEYGUARD_SERVICE);
|
|
1039
|
+
return km != null && km.isKeyguardLocked();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private void ensureUnlockedThenOpenSettings() {
|
|
1043
|
+
Activity activity = getActivity();
|
|
1044
|
+
if (activity == null) {
|
|
1045
|
+
Log.w(TAG, "ensureUnlockedThenOpenSettings: no activity available");
|
|
1046
|
+
openAppSettings();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
KeyguardManager km = (KeyguardManager) getSafeContext().getSystemService(Context.KEYGUARD_SERVICE);
|
|
1051
|
+
if (km == null || !km.isKeyguardLocked()) {
|
|
1052
|
+
openAppSettings();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
km.requestDismissKeyguard(
|
|
1057
|
+
activity,
|
|
1058
|
+
new KeyguardManager.KeyguardDismissCallback() {
|
|
1059
|
+
@Override
|
|
1060
|
+
public void onDismissSucceeded() {
|
|
1061
|
+
Log.d(TAG, "Keyguard dismissed, opening settings");
|
|
1062
|
+
openAppSettings();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
@Override
|
|
1066
|
+
public void onDismissCancelled() {
|
|
1067
|
+
Log.d(TAG, "Keyguard dismissal cancelled");
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
@Override
|
|
1071
|
+
public void onDismissError() {
|
|
1072
|
+
Log.w(TAG, "Keyguard dismissal error");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private void moveAppToBackgroundIfLocked() {
|
|
1079
|
+
Activity activity = getActivity();
|
|
1080
|
+
if (activity != null && isDeviceLocked()) {
|
|
1081
|
+
Log.d(TAG, "moveAppToBackgroundIfLocked: moving task to back");
|
|
1082
|
+
activity.moveTaskToBack(true);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private void handlePermissionFailure() {
|
|
1087
|
+
Log.d(
|
|
1088
|
+
TAG,
|
|
1089
|
+
"handlePermissionFailure: pendingAction=" + pendingPermissionAction + ", pendingCall=" + (pendingPermissionCall != null)
|
|
1090
|
+
);
|
|
1091
|
+
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL) {
|
|
1092
|
+
if (pendingOutgoingCall != null) {
|
|
1093
|
+
pendingOutgoingCall.setKeepAlive(false);
|
|
1094
|
+
pendingOutgoingCall.reject("Microphone permission is required to place a call.");
|
|
1095
|
+
}
|
|
1096
|
+
clearOutgoingPermissionState();
|
|
1097
|
+
} else if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
1098
|
+
if (pendingCallSidForPermission != null) {
|
|
1099
|
+
CallInvite invite = activeCallInvites.get(pendingCallSidForPermission);
|
|
1100
|
+
if (invite != null) {
|
|
1101
|
+
dismissIncomingCallNotification();
|
|
1102
|
+
activeCallInvites.remove(pendingCallSidForPermission);
|
|
1103
|
+
try {
|
|
1104
|
+
invite.reject(getSafeContext());
|
|
1105
|
+
} catch (Exception ex) {
|
|
1106
|
+
Log.w(TAG, "handlePermissionFailure: failed to reject invite", ex);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
JSObject data = new JSObject();
|
|
1110
|
+
data.put("callSid", pendingCallSidForPermission);
|
|
1111
|
+
data.put("reason", "microphone_permission_denied");
|
|
1112
|
+
if (invite != null) {
|
|
1113
|
+
if (invite.getFrom() != null) {
|
|
1114
|
+
data.put("from", invite.getFrom().replace("client:", ""));
|
|
1115
|
+
}
|
|
1116
|
+
if (invite.getTo() != null) {
|
|
1117
|
+
data.put("to", invite.getTo());
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
notifyListeners("callDisconnected", data);
|
|
1121
|
+
}
|
|
1122
|
+
pendingCallSidForPermission = null;
|
|
1123
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
1124
|
+
awaitingSettingsResult = false;
|
|
1125
|
+
pendingPermissionCall = null;
|
|
1126
|
+
moveAppToBackgroundIfLocked();
|
|
1127
|
+
} else if (pendingPermissionCall != null) {
|
|
1128
|
+
JSObject ret = new JSObject();
|
|
1129
|
+
ret.put("granted", false);
|
|
1130
|
+
pendingPermissionCall.setKeepAlive(false);
|
|
1131
|
+
pendingPermissionCall.resolve(ret);
|
|
1132
|
+
pendingPermissionCall = null;
|
|
1133
|
+
awaitingSettingsResult = false;
|
|
707
1134
|
}
|
|
1135
|
+
permissionAttemptCount = 0;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private void clearOutgoingPermissionState() {
|
|
1139
|
+
pendingOutgoingCall = null;
|
|
1140
|
+
pendingOutgoingTo = null;
|
|
1141
|
+
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL) {
|
|
1142
|
+
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
1143
|
+
}
|
|
1144
|
+
permissionAttemptCount = 0;
|
|
708
1145
|
}
|
|
709
1146
|
|
|
710
1147
|
// Call parameter creation is now handled by VoiceCallService
|
|
@@ -754,6 +1191,7 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
754
1191
|
JSObject ret = new JSObject();
|
|
755
1192
|
ret.put("success", true);
|
|
756
1193
|
call.resolve(ret);
|
|
1194
|
+
moveAppToBackgroundIfLocked();
|
|
757
1195
|
}
|
|
758
1196
|
|
|
759
1197
|
@PluginMethod
|
|
@@ -847,25 +1285,82 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
847
1285
|
|
|
848
1286
|
@PluginMethod
|
|
849
1287
|
public void requestMicrophonePermission(PluginCall call) {
|
|
850
|
-
|
|
1288
|
+
Log.d(TAG, "requestMicrophonePermission invoked");
|
|
1289
|
+
if (hasMicrophonePermission()) {
|
|
1290
|
+
Log.d(TAG, "requestMicrophonePermission: already granted");
|
|
1291
|
+
JSObject ret = new JSObject();
|
|
1292
|
+
ret.put("granted", true);
|
|
1293
|
+
call.resolve(ret);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
Activity activity = getActivity();
|
|
1298
|
+
if (activity == null) {
|
|
1299
|
+
Log.w(TAG, "requestMicrophonePermission: no activity available");
|
|
1300
|
+
call.reject("Unable to request permission without an active activity");
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
SharedPreferences prefs = getPrefs();
|
|
1305
|
+
boolean requestedBefore = prefs.getBoolean(PREF_MIC_PERMISSION_REQUESTED, false);
|
|
1306
|
+
boolean shouldShow = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO);
|
|
1307
|
+
Log.d(
|
|
1308
|
+
TAG,
|
|
1309
|
+
"requestMicrophonePermission: requestedBefore=" +
|
|
1310
|
+
requestedBefore +
|
|
1311
|
+
", shouldShow=" +
|
|
1312
|
+
shouldShow +
|
|
1313
|
+
", pendingAction=" +
|
|
1314
|
+
pendingPermissionAction
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
if (pendingPermissionAction == PendingPermissionAction.NONE && !shouldShow && requestedBefore) {
|
|
1318
|
+
showStandalonePermissionSettingsDialog(activity, call);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
prefs.edit().putBoolean(PREF_MIC_PERMISSION_REQUESTED, true).apply();
|
|
1323
|
+
|
|
1324
|
+
if (pendingPermissionAction == PendingPermissionAction.NONE) {
|
|
1325
|
+
pendingPermissionCall = call;
|
|
1326
|
+
call.setKeepAlive(true);
|
|
1327
|
+
}
|
|
1328
|
+
awaitingSettingsResult = false;
|
|
1329
|
+
permissionAttemptCount++;
|
|
1330
|
+
permissionRequestTimestamp = System.currentTimeMillis();
|
|
1331
|
+
Log.d(TAG, "requestMicrophonePermission: invoking ActivityCompat.requestPermissions (attempt " + permissionAttemptCount + ")");
|
|
1332
|
+
|
|
1333
|
+
ActivityCompat.requestPermissions(
|
|
1334
|
+
activity,
|
|
1335
|
+
new String[] { Manifest.permission.RECORD_AUDIO },
|
|
1336
|
+
REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT
|
|
1337
|
+
);
|
|
851
1338
|
}
|
|
852
1339
|
|
|
853
1340
|
@Override
|
|
854
1341
|
protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
|
855
1342
|
super.handleRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
856
1343
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1344
|
+
if (requestCode != REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
boolean granted = hasMicrophonePermission();
|
|
1349
|
+
Log.d(
|
|
1350
|
+
TAG,
|
|
1351
|
+
"handleRequestPermissionsResult: granted=" +
|
|
1352
|
+
granted +
|
|
1353
|
+
", attempt=" +
|
|
1354
|
+
permissionAttemptCount +
|
|
1355
|
+
", pendingAction=" +
|
|
1356
|
+
pendingPermissionAction +
|
|
1357
|
+
", standaloneCall=" +
|
|
1358
|
+
(pendingPermissionCall != null)
|
|
1359
|
+
);
|
|
1360
|
+
if (granted) {
|
|
1361
|
+
handleMicrophonePermissionGranted();
|
|
1362
|
+
} else {
|
|
1363
|
+
handleMicrophonePermissionDenied();
|
|
869
1364
|
}
|
|
870
1365
|
}
|
|
871
1366
|
|
|
@@ -1085,13 +1580,8 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1085
1580
|
}
|
|
1086
1581
|
};*/
|
|
1087
1582
|
|
|
1088
|
-
private void showIncomingCallNotification(CallInvite callInvite, String callSid) {
|
|
1583
|
+
private void showIncomingCallNotification(CallInvite callInvite, String callSid, String callerName) {
|
|
1089
1584
|
try {
|
|
1090
|
-
String callerName = callInvite.getFrom();
|
|
1091
|
-
if (callerName != null && callerName.startsWith("client:")) {
|
|
1092
|
-
callerName = callerName.substring(7); // Remove "client:" prefix
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
1585
|
// Create intent for accepting the call
|
|
1096
1586
|
PendingIntent acceptPendingIntent;
|
|
1097
1587
|
if (this.bridge == null) {
|
|
@@ -1208,16 +1698,22 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1208
1698
|
String callSid = UUID.randomUUID().toString(); // Generate a unique ID
|
|
1209
1699
|
activeCallInvites.put(callSid, callInvite);
|
|
1210
1700
|
|
|
1701
|
+
Map<String, String> params = callInvite.getCustomParameters();
|
|
1702
|
+
String callerName = params.containsKey("CapacitorTwilioCallerName")
|
|
1703
|
+
? params.get("CapacitorTwilioCallerName")
|
|
1704
|
+
: callInvite.getFrom();
|
|
1705
|
+
|
|
1211
1706
|
// Create and show notification
|
|
1212
|
-
showIncomingCallNotification(callInvite, callSid);
|
|
1707
|
+
showIncomingCallNotification(callInvite, callSid, callerName);
|
|
1213
1708
|
|
|
1214
1709
|
// Start ringtone and vibration
|
|
1215
1710
|
startRingtone();
|
|
1216
1711
|
|
|
1217
1712
|
JSObject data = new JSObject();
|
|
1218
1713
|
data.put("callSid", callSid);
|
|
1219
|
-
data.put("from",
|
|
1714
|
+
data.put("from", callerName);
|
|
1220
1715
|
data.put("to", callInvite.getTo());
|
|
1716
|
+
data.put("customParams", new JSONObject(params));
|
|
1221
1717
|
notifyListeners("callInviteReceived", data);
|
|
1222
1718
|
}
|
|
1223
1719
|
|
|
@@ -1278,6 +1774,7 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1278
1774
|
notifyListeners("callDisconnected", data);
|
|
1279
1775
|
|
|
1280
1776
|
Log.d(TAG, "Call rejected from notification");
|
|
1777
|
+
moveAppToBackgroundIfLocked();
|
|
1281
1778
|
} else {
|
|
1282
1779
|
Log.e(TAG, "Call invite not found for SID: " + callSid);
|
|
1283
1780
|
}
|
package/dist/docs.json
CHANGED
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"name": "addListener",
|
|
172
|
-
"signature": "(eventName: 'callInviteReceived', listenerFunc: (data: { callSid: string; from: string; to: string; }) => void) => Promise<PluginListenerHandle>",
|
|
172
|
+
"signature": "(eventName: 'callInviteReceived', listenerFunc: (data: { callSid: string; from: string; to: string; customParams: Record<string, string>; }) => void) => Promise<PluginListenerHandle>",
|
|
173
173
|
"parameters": [
|
|
174
174
|
{
|
|
175
175
|
"name": "eventName",
|
|
@@ -179,14 +179,15 @@
|
|
|
179
179
|
{
|
|
180
180
|
"name": "listenerFunc",
|
|
181
181
|
"docs": "",
|
|
182
|
-
"type": "(data: { callSid: string; from: string; to: string; }) => void"
|
|
182
|
+
"type": "(data: { callSid: string; from: string; to: string; customParams: Record<string, string>; }) => void"
|
|
183
183
|
}
|
|
184
184
|
],
|
|
185
185
|
"returns": "Promise<PluginListenerHandle>",
|
|
186
186
|
"tags": [],
|
|
187
187
|
"docs": "",
|
|
188
188
|
"complexTypes": [
|
|
189
|
-
"PluginListenerHandle"
|
|
189
|
+
"PluginListenerHandle",
|
|
190
|
+
"Record"
|
|
190
191
|
],
|
|
191
192
|
"slug": "addlistenercallinvitereceived-"
|
|
192
193
|
},
|
|
@@ -213,6 +214,75 @@
|
|
|
213
214
|
],
|
|
214
215
|
"slug": "addlistenercallconnected-"
|
|
215
216
|
},
|
|
217
|
+
{
|
|
218
|
+
"name": "addListener",
|
|
219
|
+
"signature": "(eventName: 'callInviteCancelled', listenerFunc: (data: { callSid: string; reason: 'user_declined' | 'remote_cancelled'; }) => void) => Promise<PluginListenerHandle>",
|
|
220
|
+
"parameters": [
|
|
221
|
+
{
|
|
222
|
+
"name": "eventName",
|
|
223
|
+
"docs": "",
|
|
224
|
+
"type": "'callInviteCancelled'"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "listenerFunc",
|
|
228
|
+
"docs": "",
|
|
229
|
+
"type": "(data: { callSid: string; reason: 'user_declined' | 'remote_cancelled'; }) => void"
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
233
|
+
"tags": [],
|
|
234
|
+
"docs": "",
|
|
235
|
+
"complexTypes": [
|
|
236
|
+
"PluginListenerHandle"
|
|
237
|
+
],
|
|
238
|
+
"slug": "addlistenercallinvitecancelled-"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "addListener",
|
|
242
|
+
"signature": "(eventName: 'outgoingCallInitiated', listenerFunc: (data: { callSid: string; to: string; source: 'app' | 'system'; displayName?: string; }) => void) => Promise<PluginListenerHandle>",
|
|
243
|
+
"parameters": [
|
|
244
|
+
{
|
|
245
|
+
"name": "eventName",
|
|
246
|
+
"docs": "",
|
|
247
|
+
"type": "'outgoingCallInitiated'"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "listenerFunc",
|
|
251
|
+
"docs": "",
|
|
252
|
+
"type": "(data: { callSid: string; to: string; source: 'app' | 'system'; displayName?: string | undefined; }) => void"
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
256
|
+
"tags": [],
|
|
257
|
+
"docs": "",
|
|
258
|
+
"complexTypes": [
|
|
259
|
+
"PluginListenerHandle"
|
|
260
|
+
],
|
|
261
|
+
"slug": "addlisteneroutgoingcallinitiated-"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "addListener",
|
|
265
|
+
"signature": "(eventName: 'outgoingCallFailed', listenerFunc: (data: { callSid: string; to: string; reason: 'missing_access_token' | 'connection_failed' | 'no_call_details' | 'microphone_permission_denied' | 'invalid_contact' | 'callkit_request_failed' | 'unsupported_intent'; displayName?: string; }) => void) => Promise<PluginListenerHandle>",
|
|
266
|
+
"parameters": [
|
|
267
|
+
{
|
|
268
|
+
"name": "eventName",
|
|
269
|
+
"docs": "",
|
|
270
|
+
"type": "'outgoingCallFailed'"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "listenerFunc",
|
|
274
|
+
"docs": "",
|
|
275
|
+
"type": "(data: { callSid: string; to: string; reason: 'missing_access_token' | 'connection_failed' | 'no_call_details' | 'microphone_permission_denied' | 'invalid_contact' | 'callkit_request_failed' | 'unsupported_intent'; displayName?: string | undefined; }) => void"
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
279
|
+
"tags": [],
|
|
280
|
+
"docs": "",
|
|
281
|
+
"complexTypes": [
|
|
282
|
+
"PluginListenerHandle"
|
|
283
|
+
],
|
|
284
|
+
"slug": "addlisteneroutgoingcallfailed-"
|
|
285
|
+
},
|
|
216
286
|
{
|
|
217
287
|
"name": "addListener",
|
|
218
288
|
"signature": "(eventName: 'callDisconnected', listenerFunc: (data: { callSid: string; error?: string; }) => void) => Promise<PluginListenerHandle>",
|
|
@@ -425,6 +495,21 @@
|
|
|
425
495
|
}
|
|
426
496
|
],
|
|
427
497
|
"enums": [],
|
|
428
|
-
"typeAliases": [
|
|
498
|
+
"typeAliases": [
|
|
499
|
+
{
|
|
500
|
+
"name": "Record",
|
|
501
|
+
"slug": "record",
|
|
502
|
+
"docs": "Construct a type with a set of properties K of type T",
|
|
503
|
+
"types": [
|
|
504
|
+
{
|
|
505
|
+
"text": "{\r\n [P in K]: T;\r\n}",
|
|
506
|
+
"complexTypes": [
|
|
507
|
+
"K",
|
|
508
|
+
"T"
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
],
|
|
429
514
|
"pluginConfigs": []
|
|
430
515
|
}
|
|
@@ -61,10 +61,27 @@ export interface CapacitorTwilioVoicePlugin {
|
|
|
61
61
|
callSid: string;
|
|
62
62
|
from: string;
|
|
63
63
|
to: string;
|
|
64
|
+
customParams: Record<string, string>;
|
|
64
65
|
}) => void): Promise<PluginListenerHandle>;
|
|
65
66
|
addListener(eventName: 'callConnected', listenerFunc: (data: {
|
|
66
67
|
callSid: string;
|
|
67
68
|
}) => void): Promise<PluginListenerHandle>;
|
|
69
|
+
addListener(eventName: 'callInviteCancelled', listenerFunc: (data: {
|
|
70
|
+
callSid: string;
|
|
71
|
+
reason: 'user_declined' | 'remote_cancelled';
|
|
72
|
+
}) => void): Promise<PluginListenerHandle>;
|
|
73
|
+
addListener(eventName: 'outgoingCallInitiated', listenerFunc: (data: {
|
|
74
|
+
callSid: string;
|
|
75
|
+
to: string;
|
|
76
|
+
source: 'app' | 'system';
|
|
77
|
+
displayName?: string;
|
|
78
|
+
}) => void): Promise<PluginListenerHandle>;
|
|
79
|
+
addListener(eventName: 'outgoingCallFailed', listenerFunc: (data: {
|
|
80
|
+
callSid: string;
|
|
81
|
+
to: string;
|
|
82
|
+
reason: 'missing_access_token' | 'connection_failed' | 'no_call_details' | 'microphone_permission_denied' | 'invalid_contact' | 'callkit_request_failed' | 'unsupported_intent';
|
|
83
|
+
displayName?: string;
|
|
84
|
+
}) => void): Promise<PluginListenerHandle>;
|
|
68
85
|
addListener(eventName: 'callDisconnected', listenerFunc: (data: {
|
|
69
86
|
callSid: string;
|
|
70
87
|
error?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface CapacitorTwilioVoicePlugin {\n // Authentication\n login(options: { accessToken: string }): Promise<{ success: boolean }>;\n logout(): Promise<{ success: boolean }>;\n isLoggedIn(): Promise<{ isLoggedIn: boolean; hasValidToken: boolean; identity?: string }>;\n\n // Call Management\n makeCall(options: { to: string }): Promise<{ success: boolean; callSid?: string }>;\n acceptCall(options: { callSid: string }): Promise<{ success: boolean }>;\n rejectCall(options: { callSid: string }): Promise<{ success: boolean }>;\n endCall(options: { callSid?: string }): Promise<{ success: boolean }>;\n\n // Call Controls\n muteCall(options: { muted: boolean; callSid?: string }): Promise<{ success: boolean }>;\n setSpeaker(options: { enabled: boolean }): Promise<{ success: boolean }>;\n\n // Call Status\n getCallStatus(): Promise<{\n hasActiveCall: boolean;\n isOnHold: boolean;\n isMuted: boolean;\n callSid?: string;\n callState?: string;\n }>;\n\n // Audio Permissions\n checkMicrophonePermission(): Promise<{ granted: boolean }>;\n requestMicrophonePermission(): Promise<{ granted: boolean }>;\n\n // Listeners for events\n addListener(\n eventName: 'callInviteReceived',\n listenerFunc: (data: { callSid: string; from: string; to: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callConnected',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callDisconnected',\n listenerFunc: (data: { callSid: string; error?: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callRinging',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callReconnecting',\n listenerFunc: (data: { callSid: string; error?: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callReconnected',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callQualityWarningsChanged',\n listenerFunc: (data: { callSid: string; currentWarnings: string[]; previousWarnings: string[] }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(eventName: 'registrationSuccess', listenerFunc: () => void): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'registrationFailure',\n listenerFunc: (data: { error: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this plugin\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n\nexport interface PluginListenerHandle {\n remove(): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface CapacitorTwilioVoicePlugin {\n // Authentication\n login(options: { accessToken: string }): Promise<{ success: boolean }>;\n logout(): Promise<{ success: boolean }>;\n isLoggedIn(): Promise<{ isLoggedIn: boolean; hasValidToken: boolean; identity?: string }>;\n\n // Call Management\n makeCall(options: { to: string }): Promise<{ success: boolean; callSid?: string }>;\n acceptCall(options: { callSid: string }): Promise<{ success: boolean }>;\n rejectCall(options: { callSid: string }): Promise<{ success: boolean }>;\n endCall(options: { callSid?: string }): Promise<{ success: boolean }>;\n\n // Call Controls\n muteCall(options: { muted: boolean; callSid?: string }): Promise<{ success: boolean }>;\n setSpeaker(options: { enabled: boolean }): Promise<{ success: boolean }>;\n\n // Call Status\n getCallStatus(): Promise<{\n hasActiveCall: boolean;\n isOnHold: boolean;\n isMuted: boolean;\n callSid?: string;\n callState?: string;\n }>;\n\n // Audio Permissions\n checkMicrophonePermission(): Promise<{ granted: boolean }>;\n requestMicrophonePermission(): Promise<{ granted: boolean }>;\n\n // Listeners for events\n addListener(\n eventName: 'callInviteReceived',\n listenerFunc: (data: { callSid: string; from: string; to: string; customParams: Record<string, string> }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callConnected',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callInviteCancelled',\n listenerFunc: (data: { callSid: string; reason: 'user_declined' | 'remote_cancelled' }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'outgoingCallInitiated',\n listenerFunc: (data: { callSid: string; to: string; source: 'app' | 'system'; displayName?: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'outgoingCallFailed',\n listenerFunc: (data: {\n callSid: string;\n to: string;\n reason:\n | 'missing_access_token'\n | 'connection_failed'\n | 'no_call_details'\n | 'microphone_permission_denied'\n | 'invalid_contact'\n | 'callkit_request_failed'\n | 'unsupported_intent';\n displayName?: string;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callDisconnected',\n listenerFunc: (data: { callSid: string; error?: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callRinging',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callReconnecting',\n listenerFunc: (data: { callSid: string; error?: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callReconnected',\n listenerFunc: (data: { callSid: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'callQualityWarningsChanged',\n listenerFunc: (data: { callSid: string; currentWarnings: string[]; previousWarnings: string[] }) => void,\n ): Promise<PluginListenerHandle>;\n\n addListener(eventName: 'registrationSuccess', listenerFunc: () => void): Promise<PluginListenerHandle>;\n\n addListener(\n eventName: 'registrationFailure',\n listenerFunc: (data: { error: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this plugin\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n\nexport interface PluginListenerHandle {\n remove(): Promise<void>;\n}\n"]}
|
|
@@ -4,6 +4,7 @@ import PushKit
|
|
|
4
4
|
import CallKit
|
|
5
5
|
import TwilioVoice
|
|
6
6
|
import AVFoundation
|
|
7
|
+
import Intents
|
|
7
8
|
|
|
8
9
|
let kRegistrationTTLInDays = 365
|
|
9
10
|
let kCachedDeviceToken = "CachedDeviceToken"
|
|
@@ -26,7 +27,7 @@ public protocol PushKitEventDelegate: AnyObject {
|
|
|
26
27
|
*/
|
|
27
28
|
@objc(CapacitorTwilioVoicePlugin)
|
|
28
29
|
public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEventDelegate {
|
|
29
|
-
private let PLUGIN_VERSION: String = "7.
|
|
30
|
+
private let PLUGIN_VERSION: String = "7.4.2"
|
|
30
31
|
|
|
31
32
|
public let identifier = "CapacitorTwilioVoicePlugin"
|
|
32
33
|
public let jsName = "CapacitorTwilioVoice"
|
|
@@ -60,6 +61,13 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
60
61
|
private var callKitCompletionCallback: ((Bool) -> Void)?
|
|
61
62
|
private var playCustomRingback = false
|
|
62
63
|
private var ringtonePlayer: AVAudioPlayer?
|
|
64
|
+
private struct PendingOutgoingCall {
|
|
65
|
+
let to: String
|
|
66
|
+
let completion: (Bool) -> Void
|
|
67
|
+
let isSystemInitiated: Bool
|
|
68
|
+
let displayName: String?
|
|
69
|
+
}
|
|
70
|
+
private var pendingOutgoingCalls: [UUID: PendingOutgoingCall] = [:]
|
|
63
71
|
|
|
64
72
|
deinit {
|
|
65
73
|
// Remove observers
|
|
@@ -93,6 +101,8 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
93
101
|
let configuration = CXProviderConfiguration(localizedName: "Voice Call")
|
|
94
102
|
configuration.maximumCallGroups = 2
|
|
95
103
|
configuration.maximumCallsPerCallGroup = 1
|
|
104
|
+
configuration.supportedHandleTypes = [.generic, .phoneNumber]
|
|
105
|
+
configuration.includesCallsInRecents = true
|
|
96
106
|
callKitProvider = CXProvider(configuration: configuration)
|
|
97
107
|
if let provider = callKitProvider {
|
|
98
108
|
provider.setDelegate(self, queue: nil)
|
|
@@ -374,13 +384,74 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
374
384
|
}
|
|
375
385
|
|
|
376
386
|
let uuid = UUID()
|
|
377
|
-
self?.performStartCallAction(uuid: uuid,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
self?.performStartCallAction(uuid: uuid,
|
|
388
|
+
handle: to,
|
|
389
|
+
to: to,
|
|
390
|
+
isSystemInitiated: false,
|
|
391
|
+
completion: { success in
|
|
392
|
+
if success {
|
|
393
|
+
call.resolve(["success": true, "callSid": uuid.uuidString])
|
|
394
|
+
} else {
|
|
395
|
+
call.reject("Failed to start call")
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@available(iOS 10.0, *)
|
|
402
|
+
public func handleStartCallIntent(intent: INIntent) {
|
|
403
|
+
if #available(iOS 13.0, *), let callIntent = intent as? INStartCallIntent {
|
|
404
|
+
processStartCallIntent(contact: callIntent.contacts?.first,
|
|
405
|
+
intentType: "INStartCallIntent")
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if let audioIntent = intent as? INStartAudioCallIntent {
|
|
410
|
+
processStartCallIntent(contact: audioIntent.contacts?.first,
|
|
411
|
+
intentType: "INStartAudioCallIntent")
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
NSLog("Unsupported call intent type: \(type(of: intent))")
|
|
416
|
+
emitOutgoingCallFailed(to: "", displayName: nil, reason: "unsupported_intent")
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@available(iOS 10.0, *)
|
|
420
|
+
private func processStartCallIntent(contact: INPerson?, intentType: String) {
|
|
421
|
+
guard let contact = contact else {
|
|
422
|
+
NSLog("\(intentType) received without contact information")
|
|
423
|
+
emitOutgoingCallFailed(to: "", displayName: nil, reason: "invalid_contact")
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
guard let handleValue = contact.personHandle?.value, !handleValue.isEmpty else {
|
|
428
|
+
let displayName = resolveDisplayName(from: contact)
|
|
429
|
+
NSLog("\(intentType) contact missing handle value")
|
|
430
|
+
emitOutgoingCallFailed(to: displayName ?? "", displayName: displayName, reason: "invalid_contact")
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let displayName = resolveDisplayName(from: contact)
|
|
435
|
+
NSLog("Processing \(intentType) for handle: \(handleValue)")
|
|
436
|
+
|
|
437
|
+
checkRecordPermission { [weak self] permissionGranted in
|
|
438
|
+
guard let self = self else { return }
|
|
439
|
+
|
|
440
|
+
guard permissionGranted else {
|
|
441
|
+
NSLog("Microphone permission denied while processing \(intentType)")
|
|
442
|
+
self.emitOutgoingCallFailed(to: handleValue,
|
|
443
|
+
displayName: displayName,
|
|
444
|
+
reason: "microphone_permission_denied")
|
|
445
|
+
return
|
|
383
446
|
}
|
|
447
|
+
|
|
448
|
+
NSLog("Initiating CallKit start call action for intent target: \(handleValue)")
|
|
449
|
+
self.performStartCallAction(uuid: UUID(),
|
|
450
|
+
handle: handleValue,
|
|
451
|
+
to: handleValue,
|
|
452
|
+
isSystemInitiated: true,
|
|
453
|
+
displayName: displayName,
|
|
454
|
+
completion: { _ in })
|
|
384
455
|
}
|
|
385
456
|
}
|
|
386
457
|
|
|
@@ -572,6 +643,44 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
572
643
|
}
|
|
573
644
|
}
|
|
574
645
|
|
|
646
|
+
private func emitOutgoingCallFailed(uuid: UUID = UUID(),
|
|
647
|
+
to: String,
|
|
648
|
+
displayName: String?,
|
|
649
|
+
reason: String) {
|
|
650
|
+
var data: [String: Any] = [
|
|
651
|
+
"callSid": uuid.uuidString,
|
|
652
|
+
"to": to,
|
|
653
|
+
"reason": reason
|
|
654
|
+
]
|
|
655
|
+
|
|
656
|
+
if let displayName = displayName {
|
|
657
|
+
data["displayName"] = displayName
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
DispatchQueue.main.async { [weak self] in
|
|
661
|
+
self?.notifyListeners("outgoingCallFailed", data: data)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
@available(iOS 10.0, *)
|
|
666
|
+
private func resolveDisplayName(from contact: INPerson) -> String? {
|
|
667
|
+
if !contact.displayName.isEmpty {
|
|
668
|
+
return contact.displayName
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let displayName = contact.displayName
|
|
672
|
+
|
|
673
|
+
if let nameComponents = contact.nameComponents {
|
|
674
|
+
let formatter = PersonNameComponentsFormatter()
|
|
675
|
+
let formattedName = formatter.string(from: nameComponents)
|
|
676
|
+
if !formattedName.isEmpty {
|
|
677
|
+
return formattedName
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return contact.personHandle?.value
|
|
682
|
+
}
|
|
683
|
+
|
|
575
684
|
private func toggleAudioRoute(toSpeaker: Bool) {
|
|
576
685
|
audioDevice.block = {
|
|
577
686
|
do {
|
|
@@ -700,12 +809,22 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
700
809
|
|
|
701
810
|
// MARK: - CallKit Actions
|
|
702
811
|
|
|
703
|
-
private func performStartCallAction(uuid: UUID,
|
|
812
|
+
private func performStartCallAction(uuid: UUID,
|
|
813
|
+
handle: String,
|
|
814
|
+
to: String,
|
|
815
|
+
isSystemInitiated: Bool,
|
|
816
|
+
displayName: String? = nil,
|
|
817
|
+
completion: @escaping (Bool) -> Void) {
|
|
704
818
|
guard let provider = callKitProvider else {
|
|
705
819
|
completion(false)
|
|
706
820
|
return
|
|
707
821
|
}
|
|
708
822
|
|
|
823
|
+
pendingOutgoingCalls[uuid] = PendingOutgoingCall(to: to,
|
|
824
|
+
completion: completion,
|
|
825
|
+
isSystemInitiated: isSystemInitiated,
|
|
826
|
+
displayName: displayName)
|
|
827
|
+
|
|
709
828
|
let callHandle = CXHandle(type: .generic, value: handle)
|
|
710
829
|
let startCallAction = CXStartCallAction(call: uuid, handle: callHandle)
|
|
711
830
|
let transaction = CXTransaction(action: startCallAction)
|
|
@@ -713,7 +832,13 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
713
832
|
callKitCallController.request(transaction) { [weak self] error in
|
|
714
833
|
if let error = error {
|
|
715
834
|
NSLog("StartCallAction transaction request failed: \(error.localizedDescription)")
|
|
835
|
+
self?.pendingOutgoingCalls.removeValue(forKey: uuid)
|
|
716
836
|
completion(false)
|
|
837
|
+
|
|
838
|
+
self?.emitOutgoingCallFailed(uuid: uuid,
|
|
839
|
+
to: to,
|
|
840
|
+
displayName: displayName,
|
|
841
|
+
reason: "callkit_request_failed")
|
|
717
842
|
return
|
|
718
843
|
}
|
|
719
844
|
|
|
@@ -726,12 +851,10 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
726
851
|
callUpdate.hasVideo = false
|
|
727
852
|
|
|
728
853
|
provider.reportCall(with: uuid, updated: callUpdate)
|
|
729
|
-
|
|
730
|
-
self?.performVoiceCall(uuid: uuid, to: to, completionHandler: completion)
|
|
731
854
|
}
|
|
732
855
|
}
|
|
733
856
|
|
|
734
|
-
private func reportIncomingCall(from: String, uuid: UUID) {
|
|
857
|
+
private func reportIncomingCall(from: String, niceName: String, uuid: UUID) {
|
|
735
858
|
guard let provider = callKitProvider else { return }
|
|
736
859
|
|
|
737
860
|
let callHandle = CXHandle(type: .generic, value: from)
|
|
@@ -743,6 +866,7 @@ public class CapacitorTwilioVoicePlugin: CAPPlugin, CAPBridgedPlugin, PushKitEve
|
|
|
743
866
|
callUpdate.supportsGrouping = false
|
|
744
867
|
callUpdate.supportsUngrouping = false
|
|
745
868
|
callUpdate.hasVideo = false
|
|
869
|
+
callUpdate.localizedCallerName = niceName
|
|
746
870
|
|
|
747
871
|
provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
|
|
748
872
|
if let error = error {
|
|
@@ -838,13 +962,15 @@ extension CapacitorTwilioVoicePlugin: NotificationDelegate {
|
|
|
838
962
|
UserDefaults.standard.set(Date(), forKey: kCachedBindingDate)
|
|
839
963
|
|
|
840
964
|
let from = (callInvite.from ?? "Unknown").replacingOccurrences(of: "client:", with: "")
|
|
841
|
-
|
|
965
|
+
let niceName = callInvite.customParameters?["CapacitorTwilioCallerName"] ?? from
|
|
966
|
+
reportIncomingCall(from: from, niceName: niceName, uuid: callInvite.uuid)
|
|
842
967
|
activeCallInvites[callInvite.uuid.uuidString] = callInvite
|
|
843
968
|
|
|
844
969
|
notifyListeners("callInviteReceived", data: [
|
|
845
970
|
"callSid": callInvite.uuid.uuidString,
|
|
846
971
|
"from": from,
|
|
847
|
-
"to": callInvite.to
|
|
972
|
+
"to": callInvite.to,
|
|
973
|
+
"customParams": callInvite.customParameters ?? [:]
|
|
848
974
|
])
|
|
849
975
|
}
|
|
850
976
|
|
|
@@ -858,6 +984,11 @@ extension CapacitorTwilioVoicePlugin: NotificationDelegate {
|
|
|
858
984
|
if let callInvite = callInvite {
|
|
859
985
|
performEndCallAction(uuid: callInvite.uuid)
|
|
860
986
|
activeCallInvites.removeValue(forKey: callInvite.uuid.uuidString)
|
|
987
|
+
|
|
988
|
+
notifyListeners("callInviteCancelled", data: [
|
|
989
|
+
"callSid": callInvite.uuid.uuidString,
|
|
990
|
+
"reason": "remote_cancelled"
|
|
991
|
+
])
|
|
861
992
|
}
|
|
862
993
|
}
|
|
863
994
|
}
|
|
@@ -986,7 +1117,69 @@ extension CapacitorTwilioVoicePlugin: CXProviderDelegate {
|
|
|
986
1117
|
}
|
|
987
1118
|
|
|
988
1119
|
public func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
989
|
-
|
|
1120
|
+
let uuid = action.callUUID
|
|
1121
|
+
let handleValue = action.handle.value
|
|
1122
|
+
|
|
1123
|
+
var pendingCall = pendingOutgoingCalls[uuid]
|
|
1124
|
+
if pendingCall == nil {
|
|
1125
|
+
let fallback = PendingOutgoingCall(to: handleValue,
|
|
1126
|
+
completion: { _ in },
|
|
1127
|
+
isSystemInitiated: true,
|
|
1128
|
+
displayName: nil)
|
|
1129
|
+
pendingOutgoingCalls[uuid] = fallback
|
|
1130
|
+
pendingCall = fallback
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
guard let callDetails = pendingCall else {
|
|
1134
|
+
provider.reportCall(with: uuid, endedAt: Date(), reason: .failed)
|
|
1135
|
+
emitOutgoingCallFailed(uuid: uuid,
|
|
1136
|
+
to: handleValue,
|
|
1137
|
+
displayName: nil,
|
|
1138
|
+
reason: "no_call_details")
|
|
1139
|
+
action.fail()
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
let to = callDetails.to
|
|
1144
|
+
let source = callDetails.isSystemInitiated ? "system" : "app"
|
|
1145
|
+
|
|
1146
|
+
var initiatedData: [String: Any] = [
|
|
1147
|
+
"callSid": uuid.uuidString,
|
|
1148
|
+
"to": to,
|
|
1149
|
+
"source": source
|
|
1150
|
+
]
|
|
1151
|
+
|
|
1152
|
+
if let displayName = callDetails.displayName {
|
|
1153
|
+
initiatedData["displayName"] = displayName
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
notifyListeners("outgoingCallInitiated", data: initiatedData)
|
|
1157
|
+
|
|
1158
|
+
guard let accessToken = accessToken, isTokenValid(accessToken) else {
|
|
1159
|
+
pendingOutgoingCalls.removeValue(forKey: uuid)
|
|
1160
|
+
provider.reportCall(with: uuid, endedAt: Date(), reason: .failed)
|
|
1161
|
+
callDetails.completion(false)
|
|
1162
|
+
emitOutgoingCallFailed(uuid: uuid,
|
|
1163
|
+
to: to,
|
|
1164
|
+
displayName: callDetails.displayName,
|
|
1165
|
+
reason: "missing_access_token")
|
|
1166
|
+
action.fail()
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
provider.reportOutgoingCall(with: uuid, startedConnectingAt: Date())
|
|
1171
|
+
|
|
1172
|
+
performVoiceCall(uuid: uuid, to: to) { [weak self] success in
|
|
1173
|
+
callDetails.completion(success)
|
|
1174
|
+
if !success {
|
|
1175
|
+
self?.emitOutgoingCallFailed(uuid: uuid,
|
|
1176
|
+
to: to,
|
|
1177
|
+
displayName: callDetails.displayName,
|
|
1178
|
+
reason: "connection_failed")
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
pendingOutgoingCalls.removeValue(forKey: uuid)
|
|
990
1183
|
action.fulfill()
|
|
991
1184
|
}
|
|
992
1185
|
|
|
@@ -1001,6 +1194,11 @@ extension CapacitorTwilioVoicePlugin: CXProviderDelegate {
|
|
|
1001
1194
|
if let invite = activeCallInvites[action.callUUID.uuidString] {
|
|
1002
1195
|
invite.reject()
|
|
1003
1196
|
activeCallInvites.removeValue(forKey: action.callUUID.uuidString)
|
|
1197
|
+
|
|
1198
|
+
notifyListeners("callInviteCancelled", data: [
|
|
1199
|
+
"callSid": action.callUUID.uuidString,
|
|
1200
|
+
"reason": "user_declined"
|
|
1201
|
+
])
|
|
1004
1202
|
} else if let call = activeCalls[action.callUUID.uuidString] {
|
|
1005
1203
|
call.disconnect()
|
|
1006
1204
|
}
|