@capgo/capacitor-twilio-voice 7.3.0-alpha.1 → 7.3.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/android/src/main/java/ee/forgr/capacitor_twilio_voice/CapacitorTwilioVoicePlugin.java +76 -560
- package/dist/docs.json +23 -89
- package/dist/esm/definitions.d.ts +9 -17
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -0
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorTwilioVoicePlugin/CapacitorTwilioVoicePlugin.swift +16 -207
- package/package.json +1 -1
package/android/src/main/java/ee/forgr/capacitor_twilio_voice/CapacitorTwilioVoicePlugin.java
CHANGED
|
@@ -2,9 +2,6 @@ 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;
|
|
8
5
|
import android.app.NotificationChannel;
|
|
9
6
|
import android.app.NotificationManager;
|
|
10
7
|
import android.app.PendingIntent;
|
|
@@ -24,18 +21,13 @@ import android.os.Build;
|
|
|
24
21
|
import android.os.IBinder;
|
|
25
22
|
import android.os.VibrationEffect;
|
|
26
23
|
import android.os.Vibrator;
|
|
27
|
-
import android.provider.Settings;
|
|
28
24
|
import android.util.Base64;
|
|
29
25
|
import android.util.Log;
|
|
30
|
-
import android.widget.Toast;
|
|
31
|
-
import androidx.activity.result.ActivityResultLauncher;
|
|
32
|
-
import androidx.activity.result.contract.ActivityResultContracts;
|
|
33
26
|
import androidx.annotation.NonNull;
|
|
34
27
|
import androidx.annotation.Nullable;
|
|
35
28
|
import androidx.core.app.ActivityCompat;
|
|
36
29
|
import androidx.core.app.NotificationCompat;
|
|
37
30
|
import androidx.core.app.NotificationManagerCompat;
|
|
38
|
-
import androidx.core.content.ContextCompat;
|
|
39
31
|
import com.getcapacitor.JSArray;
|
|
40
32
|
import com.getcapacitor.JSObject;
|
|
41
33
|
import com.getcapacitor.Plugin;
|
|
@@ -75,11 +67,11 @@ import org.json.JSONObject;
|
|
|
75
67
|
)
|
|
76
68
|
public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
77
69
|
|
|
70
|
+
private final String PLUGIN_VERSION = "7.3.2";
|
|
71
|
+
|
|
78
72
|
private static final String TAG = "CapacitorTwilioVoice";
|
|
79
73
|
private static final String PREF_ACCESS_TOKEN = "twilio_access_token";
|
|
80
74
|
private static final String PREF_FCM_TOKEN = "twilio_fcm_token";
|
|
81
|
-
private static final String PREFS_NAME = "capacitor_twilio_voice_prefs";
|
|
82
|
-
private static final String PREF_MIC_PERMISSION_REQUESTED = "mic_permission_requested";
|
|
83
75
|
|
|
84
76
|
public static CapacitorTwilioVoicePlugin instance;
|
|
85
77
|
|
|
@@ -111,21 +103,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
111
103
|
private static final int REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT = 2001;
|
|
112
104
|
private String pendingCallSidForPermission;
|
|
113
105
|
|
|
114
|
-
private enum PendingPermissionAction {
|
|
115
|
-
NONE,
|
|
116
|
-
OUTGOING_CALL,
|
|
117
|
-
ACCEPT_CALL
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private PendingPermissionAction pendingPermissionAction = PendingPermissionAction.NONE;
|
|
121
|
-
private PluginCall pendingOutgoingCall;
|
|
122
|
-
private String pendingOutgoingTo;
|
|
123
|
-
private PluginCall pendingPermissionCall;
|
|
124
|
-
private long permissionRequestTimestamp = 0L;
|
|
125
|
-
private int permissionAttemptCount = 0;
|
|
126
|
-
private boolean awaitingSettingsResult = false;
|
|
127
|
-
private ActivityResultLauncher<String[]> micPermissionLauncher;
|
|
128
|
-
|
|
129
106
|
// Voice Call Service
|
|
130
107
|
private VoiceCallService voiceCallService;
|
|
131
108
|
private boolean isServiceBound = false;
|
|
@@ -173,7 +150,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
173
150
|
data.put("error", error.getMessage());
|
|
174
151
|
}
|
|
175
152
|
notifyListeners("callDisconnected", data);
|
|
176
|
-
moveAppToBackgroundIfLocked();
|
|
177
153
|
}
|
|
178
154
|
|
|
179
155
|
@Override
|
|
@@ -264,11 +240,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
264
240
|
bindToVoiceCallService();
|
|
265
241
|
|
|
266
242
|
Log.d(TAG, "CapacitorTwilioVoice plugin loaded");
|
|
267
|
-
|
|
268
|
-
micPermissionLauncher = getBridge().registerForActivityResult(
|
|
269
|
-
new ActivityResultContracts.RequestMultiplePermissions(),
|
|
270
|
-
(permissions) -> handleMicPermissionResult(permissions)
|
|
271
|
-
);
|
|
272
243
|
}
|
|
273
244
|
|
|
274
245
|
private void bindToVoiceCallService() {
|
|
@@ -301,12 +272,12 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
301
272
|
|
|
302
273
|
// Delay the auto-accept slightly to ensure plugin is fully loaded
|
|
303
274
|
new android.os.Handler().postDelayed(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
275
|
+
() -> {
|
|
276
|
+
Log.d(TAG, "Auto-accepting call: " + callSid);
|
|
277
|
+
ensureMicPermissionThenAccept(callSid);
|
|
278
|
+
},
|
|
279
|
+
500
|
|
280
|
+
);
|
|
310
281
|
}
|
|
311
282
|
}
|
|
312
283
|
}
|
|
@@ -340,20 +311,20 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
340
311
|
if (callInvite != null) {
|
|
341
312
|
// Delay sending the event to ensure JavaScript is ready
|
|
342
313
|
new android.os.Handler().postDelayed(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
314
|
+
() -> {
|
|
315
|
+
Log.d(TAG, "Sending incoming call event to JavaScript: " + callSid);
|
|
316
|
+
|
|
317
|
+
JSObject data = new JSObject();
|
|
318
|
+
data.put("callSid", callSid);
|
|
319
|
+
data.put("from", callFrom);
|
|
320
|
+
data.put("to", callInvite.getTo());
|
|
321
|
+
data.put("callerName", callerName != null ? callerName : callFrom);
|
|
322
|
+
data.put("openedFromNotification", true);
|
|
323
|
+
|
|
324
|
+
notifyListeners("callInviteReceived", data);
|
|
325
|
+
},
|
|
326
|
+
1000
|
|
327
|
+
); // Give JavaScript more time to initialize
|
|
357
328
|
} else {
|
|
358
329
|
Log.w(TAG, "Call invite not found for SID: " + callSid + " (may have been cancelled)");
|
|
359
330
|
}
|
|
@@ -385,44 +356,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
385
356
|
Log.d(TAG, "CapacitorTwilioVoice plugin destroyed");
|
|
386
357
|
}
|
|
387
358
|
|
|
388
|
-
@Override
|
|
389
|
-
protected void handleOnResume() {
|
|
390
|
-
super.handleOnResume();
|
|
391
|
-
Log.d(
|
|
392
|
-
TAG,
|
|
393
|
-
"handleOnResume: hasPermission=" +
|
|
394
|
-
hasMicrophonePermission() +
|
|
395
|
-
", awaitingSettings=" +
|
|
396
|
-
awaitingSettingsResult +
|
|
397
|
-
", pendingCall=" +
|
|
398
|
-
(pendingPermissionCall != null) +
|
|
399
|
-
", pendingAction=" +
|
|
400
|
-
pendingPermissionAction
|
|
401
|
-
);
|
|
402
|
-
if (hasMicrophonePermission()) {
|
|
403
|
-
if (pendingPermissionAction != PendingPermissionAction.NONE || pendingPermissionCall != null) {
|
|
404
|
-
awaitingSettingsResult = false;
|
|
405
|
-
Log.d(TAG, "handleOnResume: permission granted, resuming pending flow");
|
|
406
|
-
handleMicrophonePermissionGranted();
|
|
407
|
-
}
|
|
408
|
-
} else if (awaitingSettingsResult && pendingPermissionCall != null) {
|
|
409
|
-
awaitingSettingsResult = false;
|
|
410
|
-
Log.d(TAG, "handleOnResume: permission still denied after returning from settings");
|
|
411
|
-
JSObject ret = new JSObject();
|
|
412
|
-
ret.put("granted", false);
|
|
413
|
-
pendingPermissionCall.setKeepAlive(false);
|
|
414
|
-
pendingPermissionCall.resolve(ret);
|
|
415
|
-
pendingPermissionCall = null;
|
|
416
|
-
} else if (awaitingSettingsResult && pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
417
|
-
awaitingSettingsResult = false;
|
|
418
|
-
Log.d(TAG, "handleOnResume: settings return without permission for accept flow");
|
|
419
|
-
handlePermissionFailure();
|
|
420
|
-
} else if (pendingPermissionCall != null) {
|
|
421
|
-
Log.d(TAG, "handleOnResume: permission denied from dialog, invoking fallback handling");
|
|
422
|
-
handleMicrophonePermissionDenied();
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
359
|
public void setInjectedContext(Context injectedContext) {
|
|
427
360
|
this.injectedContext = injectedContext;
|
|
428
361
|
}
|
|
@@ -455,19 +388,33 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
455
388
|
}
|
|
456
389
|
|
|
457
390
|
private void ensureMicPermissionThenAccept(String callSid) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
391
|
+
Context context = getSafeContext();
|
|
392
|
+
boolean granted =
|
|
393
|
+
ActivityCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
394
|
+
if (granted) {
|
|
461
395
|
proceedAcceptCall(callSid);
|
|
462
396
|
return;
|
|
463
397
|
}
|
|
464
398
|
|
|
399
|
+
// Remember callSid and request via Activity runtime permission API
|
|
465
400
|
pendingCallSidForPermission = callSid;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
401
|
+
Activity activity = getActivity();
|
|
402
|
+
if (activity != null) {
|
|
403
|
+
ActivityCompat.requestPermissions(
|
|
404
|
+
activity,
|
|
405
|
+
new String[] { Manifest.permission.RECORD_AUDIO },
|
|
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
|
+
}
|
|
471
418
|
}
|
|
472
419
|
|
|
473
420
|
// Helper to actually start the service once permission is granted
|
|
@@ -488,12 +435,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
488
435
|
Log.d(TAG, "Call acceptance started via service (permission granted)");
|
|
489
436
|
} catch (Exception e) {
|
|
490
437
|
Log.e(TAG, "Error accepting call via service", e);
|
|
491
|
-
} finally {
|
|
492
|
-
pendingCallSidForPermission = null;
|
|
493
|
-
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
494
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
495
|
-
}
|
|
496
|
-
permissionAttemptCount = 0;
|
|
497
438
|
}
|
|
498
439
|
}
|
|
499
440
|
|
|
@@ -742,32 +683,11 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
742
683
|
return;
|
|
743
684
|
}
|
|
744
685
|
|
|
745
|
-
if (pendingOutgoingCall != null) {
|
|
746
|
-
pendingOutgoingCall.setKeepAlive(false);
|
|
747
|
-
pendingOutgoingCall.reject("Another call is awaiting microphone permission.");
|
|
748
|
-
clearOutgoingPermissionState();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
686
|
String to = call.getString("to");
|
|
752
687
|
if (to == null) {
|
|
753
688
|
to = ""; // Empty string for echo test
|
|
754
689
|
}
|
|
755
690
|
|
|
756
|
-
if (hasMicrophonePermission()) {
|
|
757
|
-
startOutgoingCall(call, to);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
pendingOutgoingCall = call;
|
|
762
|
-
pendingOutgoingTo = to;
|
|
763
|
-
pendingPermissionAction = PendingPermissionAction.OUTGOING_CALL;
|
|
764
|
-
permissionAttemptCount = 0;
|
|
765
|
-
call.setKeepAlive(true);
|
|
766
|
-
requestMicrophonePermission();
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
private void startOutgoingCall(PluginCall call, String to) {
|
|
770
|
-
Log.d(TAG, "startOutgoingCall: to=" + to);
|
|
771
691
|
// Start call via the foreground service
|
|
772
692
|
Intent serviceIntent = new Intent(getSafeContext(), VoiceCallService.class);
|
|
773
693
|
serviceIntent.setAction(VoiceCallService.ACTION_START_CALL);
|
|
@@ -780,368 +700,13 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
780
700
|
JSObject ret = new JSObject();
|
|
781
701
|
ret.put("success", true);
|
|
782
702
|
ret.put("callSid", "pending"); // Will be updated when service connects
|
|
783
|
-
call.setKeepAlive(false);
|
|
784
703
|
call.resolve(ret);
|
|
785
704
|
} catch (Exception e) {
|
|
786
|
-
call.setKeepAlive(false);
|
|
787
705
|
Log.e(TAG, "Error starting call service", e);
|
|
788
706
|
call.reject("Failed to start call: " + e.getMessage());
|
|
789
|
-
} finally {
|
|
790
|
-
clearOutgoingPermissionState();
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
private boolean hasMicrophonePermission() {
|
|
795
|
-
return ContextCompat.checkSelfPermission(getSafeContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
private SharedPreferences getPrefs() {
|
|
799
|
-
return getSafeContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private void requestMicrophonePermission() {
|
|
803
|
-
Activity activity = getActivity();
|
|
804
|
-
permissionRequestTimestamp = System.currentTimeMillis();
|
|
805
|
-
permissionAttemptCount++;
|
|
806
|
-
|
|
807
|
-
if (activity != null) {
|
|
808
|
-
activity.runOnUiThread(() -> {
|
|
809
|
-
Log.d(TAG, "requestMicrophonePermission: requesting RECORD_AUDIO (attempt " + permissionAttemptCount + ")");
|
|
810
|
-
if (micPermissionLauncher != null) {
|
|
811
|
-
micPermissionLauncher.launch(new String[] { Manifest.permission.RECORD_AUDIO });
|
|
812
|
-
} else {
|
|
813
|
-
ActivityCompat.requestPermissions(
|
|
814
|
-
activity,
|
|
815
|
-
new String[] { Manifest.permission.RECORD_AUDIO },
|
|
816
|
-
REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (mainActivityClass != null) {
|
|
824
|
-
Context context = getSafeContext();
|
|
825
|
-
Intent launchIntent = new Intent(context, mainActivityClass);
|
|
826
|
-
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
827
|
-
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL && pendingCallSidForPermission != null) {
|
|
828
|
-
launchIntent.putExtra("AUTO_ACCEPT_CALL", true);
|
|
829
|
-
launchIntent.putExtra(EXTRA_CALL_SID, pendingCallSidForPermission);
|
|
830
|
-
}
|
|
831
|
-
Log.d(TAG, "requestMicrophonePermission: launching activity to request permission");
|
|
832
|
-
context.startActivity(launchIntent);
|
|
833
|
-
} else {
|
|
834
|
-
Log.w(TAG, "Unable to request microphone permission - no activity available");
|
|
835
|
-
handlePermissionFailure();
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
private void handleMicPermissionResult(Map<String, Boolean> permissions) {
|
|
840
|
-
Boolean granted = permissions.get(Manifest.permission.RECORD_AUDIO);
|
|
841
|
-
Log.d(TAG, "handleMicPermissionResult: granted=" + granted + ", pendingAction=" + pendingPermissionAction);
|
|
842
|
-
if (granted != null && granted) {
|
|
843
|
-
handleMicrophonePermissionGranted();
|
|
844
|
-
} else {
|
|
845
|
-
handleMicrophonePermissionDenied();
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
private void handleMicrophonePermissionGranted() {
|
|
850
|
-
Log.d(
|
|
851
|
-
TAG,
|
|
852
|
-
"handleMicrophonePermissionGranted: pendingAction=" +
|
|
853
|
-
pendingPermissionAction +
|
|
854
|
-
", pendingCall=" +
|
|
855
|
-
(pendingPermissionCall != null)
|
|
856
|
-
);
|
|
857
|
-
permissionAttemptCount = 0;
|
|
858
|
-
|
|
859
|
-
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL && pendingOutgoingCall != null) {
|
|
860
|
-
PluginCall call = pendingOutgoingCall;
|
|
861
|
-
String to = pendingOutgoingTo != null ? pendingOutgoingTo : "";
|
|
862
|
-
pendingOutgoingCall = null;
|
|
863
|
-
pendingOutgoingTo = null;
|
|
864
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
865
|
-
startOutgoingCall(call, to);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
870
|
-
if (pendingCallSidForPermission != null) {
|
|
871
|
-
String callSid = pendingCallSidForPermission;
|
|
872
|
-
pendingCallSidForPermission = null;
|
|
873
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
874
|
-
proceedAcceptCall(callSid);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
879
|
-
awaitingSettingsResult = false;
|
|
880
|
-
permissionAttemptCount = 0;
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
if (pendingPermissionCall != null) {
|
|
885
|
-
JSObject ret = new JSObject();
|
|
886
|
-
ret.put("granted", true);
|
|
887
|
-
pendingPermissionCall.setKeepAlive(false);
|
|
888
|
-
pendingPermissionCall.resolve(ret);
|
|
889
|
-
pendingPermissionCall = null;
|
|
890
|
-
awaitingSettingsResult = false;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
private void handleMicrophonePermissionDenied() {
|
|
897
|
-
Log.d(TAG, "handleMicrophonePermissionDenied invoked");
|
|
898
|
-
Activity activity = getActivity();
|
|
899
|
-
if (activity == null) {
|
|
900
|
-
Log.w(TAG, "handleMicrophonePermissionDenied: no activity");
|
|
901
|
-
handlePermissionFailure();
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
boolean canRequestAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO);
|
|
906
|
-
Log.d(
|
|
907
|
-
TAG,
|
|
908
|
-
"handleMicrophonePermissionDenied: canRequestAgain=" +
|
|
909
|
-
canRequestAgain +
|
|
910
|
-
", attempt=" +
|
|
911
|
-
permissionAttemptCount +
|
|
912
|
-
", pendingAction=" +
|
|
913
|
-
pendingPermissionAction +
|
|
914
|
-
", standaloneCall=" +
|
|
915
|
-
(pendingPermissionCall != null)
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
if (pendingPermissionAction == PendingPermissionAction.NONE && pendingPermissionCall != null) {
|
|
919
|
-
if (canRequestAgain && permissionAttemptCount <= 1) {
|
|
920
|
-
showStandalonePermissionRationaleDialog(activity, pendingPermissionCall);
|
|
921
|
-
} else {
|
|
922
|
-
showStandalonePermissionSettingsDialog(activity, pendingPermissionCall);
|
|
923
|
-
}
|
|
924
|
-
} else if (canRequestAgain && permissionAttemptCount <= 1) {
|
|
925
|
-
showPermissionRationaleDialog(activity);
|
|
926
|
-
} else {
|
|
927
|
-
showPermissionSettingsDialog(activity);
|
|
928
707
|
}
|
|
929
708
|
}
|
|
930
709
|
|
|
931
|
-
private void showPermissionRationaleDialog(Activity activity) {
|
|
932
|
-
Log.d(TAG, "showPermissionRationaleDialog");
|
|
933
|
-
new AlertDialog.Builder(activity)
|
|
934
|
-
.setTitle("Microphone required")
|
|
935
|
-
.setMessage("Microphone access is required to place and receive calls.")
|
|
936
|
-
.setPositiveButton("Retry", (dialog, which) -> {
|
|
937
|
-
dialog.dismiss();
|
|
938
|
-
requestMicrophonePermission();
|
|
939
|
-
})
|
|
940
|
-
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
941
|
-
dialog.dismiss();
|
|
942
|
-
handlePermissionFailure();
|
|
943
|
-
})
|
|
944
|
-
.setCancelable(false)
|
|
945
|
-
.show();
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
private void showStandalonePermissionRationaleDialog(Activity activity, PluginCall call) {
|
|
949
|
-
Log.d(TAG, "showStandalonePermissionRationaleDialog");
|
|
950
|
-
new AlertDialog.Builder(activity)
|
|
951
|
-
.setTitle("Microphone required")
|
|
952
|
-
.setMessage("Microphone access is required to place and receive calls.")
|
|
953
|
-
.setPositiveButton("Retry", (dialog, which) -> {
|
|
954
|
-
dialog.dismiss();
|
|
955
|
-
requestMicrophonePermission();
|
|
956
|
-
})
|
|
957
|
-
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
958
|
-
dialog.dismiss();
|
|
959
|
-
JSObject ret = new JSObject();
|
|
960
|
-
ret.put("granted", false);
|
|
961
|
-
call.setKeepAlive(false);
|
|
962
|
-
call.resolve(ret);
|
|
963
|
-
pendingPermissionCall = null;
|
|
964
|
-
awaitingSettingsResult = false;
|
|
965
|
-
permissionAttemptCount = 0;
|
|
966
|
-
})
|
|
967
|
-
.setCancelable(false)
|
|
968
|
-
.show();
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
private void showPermissionSettingsDialog(Activity activity) {
|
|
972
|
-
Log.d(TAG, "showPermissionSettingsDialog");
|
|
973
|
-
new AlertDialog.Builder(activity)
|
|
974
|
-
.setTitle("Enable microphone")
|
|
975
|
-
.setMessage("You can enable the microphone in Settings to use calling features.")
|
|
976
|
-
.setPositiveButton("Open Settings", (dialog, which) -> {
|
|
977
|
-
dialog.dismiss();
|
|
978
|
-
ensureUnlockedThenOpenSettings();
|
|
979
|
-
})
|
|
980
|
-
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
981
|
-
dialog.dismiss();
|
|
982
|
-
handlePermissionFailure();
|
|
983
|
-
})
|
|
984
|
-
.setCancelable(false)
|
|
985
|
-
.show();
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
private void showStandalonePermissionSettingsDialog(Activity activity, PluginCall call) {
|
|
989
|
-
Log.d(TAG, "showStandalonePermissionSettingsDialog");
|
|
990
|
-
new AlertDialog.Builder(activity)
|
|
991
|
-
.setTitle("Enable microphone")
|
|
992
|
-
.setMessage("Microphone access is required. Open Settings to enable the permission.")
|
|
993
|
-
.setPositiveButton("Open Settings", (dialog, which) -> {
|
|
994
|
-
dialog.dismiss();
|
|
995
|
-
pendingPermissionCall = call;
|
|
996
|
-
call.setKeepAlive(true);
|
|
997
|
-
awaitingSettingsResult = true;
|
|
998
|
-
ensureUnlockedThenOpenSettings();
|
|
999
|
-
})
|
|
1000
|
-
.setNegativeButton("Cancel", (dialog, which) -> {
|
|
1001
|
-
dialog.dismiss();
|
|
1002
|
-
JSObject ret = new JSObject();
|
|
1003
|
-
ret.put("granted", false);
|
|
1004
|
-
call.setKeepAlive(false);
|
|
1005
|
-
call.resolve(ret);
|
|
1006
|
-
pendingPermissionCall = null;
|
|
1007
|
-
awaitingSettingsResult = false;
|
|
1008
|
-
permissionAttemptCount = 0;
|
|
1009
|
-
})
|
|
1010
|
-
.setCancelable(false)
|
|
1011
|
-
.show();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
private void openAppSettings() {
|
|
1015
|
-
Context context = getSafeContext();
|
|
1016
|
-
if (pendingPermissionCall != null || pendingPermissionAction != PendingPermissionAction.NONE) {
|
|
1017
|
-
awaitingSettingsResult = true;
|
|
1018
|
-
}
|
|
1019
|
-
Log.d(
|
|
1020
|
-
TAG,
|
|
1021
|
-
"openAppSettings: awaitingSettingsResult=" +
|
|
1022
|
-
awaitingSettingsResult +
|
|
1023
|
-
", pendingAction=" +
|
|
1024
|
-
pendingPermissionAction +
|
|
1025
|
-
", pendingCall=" +
|
|
1026
|
-
(pendingPermissionCall != null)
|
|
1027
|
-
);
|
|
1028
|
-
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
|
1029
|
-
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
|
|
1030
|
-
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
1031
|
-
context.startActivity(intent);
|
|
1032
|
-
Toast.makeText(context, "Enable microphone permission and return to the app", Toast.LENGTH_LONG).show();
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
private boolean isDeviceLocked() {
|
|
1036
|
-
KeyguardManager km = (KeyguardManager) getSafeContext().getSystemService(Context.KEYGUARD_SERVICE);
|
|
1037
|
-
return km != null && km.isKeyguardLocked();
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
private void ensureUnlockedThenOpenSettings() {
|
|
1041
|
-
Activity activity = getActivity();
|
|
1042
|
-
if (activity == null) {
|
|
1043
|
-
Log.w(TAG, "ensureUnlockedThenOpenSettings: no activity available");
|
|
1044
|
-
openAppSettings();
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
KeyguardManager km = (KeyguardManager) getSafeContext().getSystemService(Context.KEYGUARD_SERVICE);
|
|
1049
|
-
if (km == null || !km.isKeyguardLocked()) {
|
|
1050
|
-
openAppSettings();
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
km.requestDismissKeyguard(
|
|
1055
|
-
activity,
|
|
1056
|
-
new KeyguardManager.KeyguardDismissCallback() {
|
|
1057
|
-
@Override
|
|
1058
|
-
public void onDismissSucceeded() {
|
|
1059
|
-
Log.d(TAG, "Keyguard dismissed, opening settings");
|
|
1060
|
-
openAppSettings();
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
@Override
|
|
1064
|
-
public void onDismissCancelled() {
|
|
1065
|
-
Log.d(TAG, "Keyguard dismissal cancelled");
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
@Override
|
|
1069
|
-
public void onDismissError() {
|
|
1070
|
-
Log.w(TAG, "Keyguard dismissal error");
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
private void moveAppToBackgroundIfLocked() {
|
|
1077
|
-
Activity activity = getActivity();
|
|
1078
|
-
if (activity != null && isDeviceLocked()) {
|
|
1079
|
-
Log.d(TAG, "moveAppToBackgroundIfLocked: moving task to back");
|
|
1080
|
-
activity.moveTaskToBack(true);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
private void handlePermissionFailure() {
|
|
1085
|
-
Log.d(
|
|
1086
|
-
TAG,
|
|
1087
|
-
"handlePermissionFailure: pendingAction=" + pendingPermissionAction + ", pendingCall=" + (pendingPermissionCall != null)
|
|
1088
|
-
);
|
|
1089
|
-
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL) {
|
|
1090
|
-
if (pendingOutgoingCall != null) {
|
|
1091
|
-
pendingOutgoingCall.setKeepAlive(false);
|
|
1092
|
-
pendingOutgoingCall.reject("Microphone permission is required to place a call.");
|
|
1093
|
-
}
|
|
1094
|
-
clearOutgoingPermissionState();
|
|
1095
|
-
} else if (pendingPermissionAction == PendingPermissionAction.ACCEPT_CALL) {
|
|
1096
|
-
if (pendingCallSidForPermission != null) {
|
|
1097
|
-
CallInvite invite = activeCallInvites.get(pendingCallSidForPermission);
|
|
1098
|
-
if (invite != null) {
|
|
1099
|
-
dismissIncomingCallNotification();
|
|
1100
|
-
activeCallInvites.remove(pendingCallSidForPermission);
|
|
1101
|
-
try {
|
|
1102
|
-
invite.reject(getSafeContext());
|
|
1103
|
-
} catch (Exception ex) {
|
|
1104
|
-
Log.w(TAG, "handlePermissionFailure: failed to reject invite", ex);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
JSObject data = new JSObject();
|
|
1108
|
-
data.put("callSid", pendingCallSidForPermission);
|
|
1109
|
-
data.put("reason", "microphone_permission_denied");
|
|
1110
|
-
if (invite != null) {
|
|
1111
|
-
if (invite.getFrom() != null) {
|
|
1112
|
-
data.put("from", invite.getFrom().replace("client:", ""));
|
|
1113
|
-
}
|
|
1114
|
-
if (invite.getTo() != null) {
|
|
1115
|
-
data.put("to", invite.getTo());
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
notifyListeners("callDisconnected", data);
|
|
1119
|
-
}
|
|
1120
|
-
pendingCallSidForPermission = null;
|
|
1121
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
1122
|
-
awaitingSettingsResult = false;
|
|
1123
|
-
pendingPermissionCall = null;
|
|
1124
|
-
moveAppToBackgroundIfLocked();
|
|
1125
|
-
} else if (pendingPermissionCall != null) {
|
|
1126
|
-
JSObject ret = new JSObject();
|
|
1127
|
-
ret.put("granted", false);
|
|
1128
|
-
pendingPermissionCall.setKeepAlive(false);
|
|
1129
|
-
pendingPermissionCall.resolve(ret);
|
|
1130
|
-
pendingPermissionCall = null;
|
|
1131
|
-
awaitingSettingsResult = false;
|
|
1132
|
-
}
|
|
1133
|
-
permissionAttemptCount = 0;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
private void clearOutgoingPermissionState() {
|
|
1137
|
-
pendingOutgoingCall = null;
|
|
1138
|
-
pendingOutgoingTo = null;
|
|
1139
|
-
if (pendingPermissionAction == PendingPermissionAction.OUTGOING_CALL) {
|
|
1140
|
-
pendingPermissionAction = PendingPermissionAction.NONE;
|
|
1141
|
-
}
|
|
1142
|
-
permissionAttemptCount = 0;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
710
|
// Call parameter creation is now handled by VoiceCallService
|
|
1146
711
|
|
|
1147
712
|
@PluginMethod
|
|
@@ -1189,7 +754,6 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1189
754
|
JSObject ret = new JSObject();
|
|
1190
755
|
ret.put("success", true);
|
|
1191
756
|
call.resolve(ret);
|
|
1192
|
-
moveAppToBackgroundIfLocked();
|
|
1193
757
|
}
|
|
1194
758
|
|
|
1195
759
|
@PluginMethod
|
|
@@ -1283,82 +847,25 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1283
847
|
|
|
1284
848
|
@PluginMethod
|
|
1285
849
|
public void requestMicrophonePermission(PluginCall call) {
|
|
1286
|
-
|
|
1287
|
-
if (hasMicrophonePermission()) {
|
|
1288
|
-
Log.d(TAG, "requestMicrophonePermission: already granted");
|
|
1289
|
-
JSObject ret = new JSObject();
|
|
1290
|
-
ret.put("granted", true);
|
|
1291
|
-
call.resolve(ret);
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
Activity activity = getActivity();
|
|
1296
|
-
if (activity == null) {
|
|
1297
|
-
Log.w(TAG, "requestMicrophonePermission: no activity available");
|
|
1298
|
-
call.reject("Unable to request permission without an active activity");
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
SharedPreferences prefs = getPrefs();
|
|
1303
|
-
boolean requestedBefore = prefs.getBoolean(PREF_MIC_PERMISSION_REQUESTED, false);
|
|
1304
|
-
boolean shouldShow = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO);
|
|
1305
|
-
Log.d(
|
|
1306
|
-
TAG,
|
|
1307
|
-
"requestMicrophonePermission: requestedBefore=" +
|
|
1308
|
-
requestedBefore +
|
|
1309
|
-
", shouldShow=" +
|
|
1310
|
-
shouldShow +
|
|
1311
|
-
", pendingAction=" +
|
|
1312
|
-
pendingPermissionAction
|
|
1313
|
-
);
|
|
1314
|
-
|
|
1315
|
-
if (pendingPermissionAction == PendingPermissionAction.NONE && !shouldShow && requestedBefore) {
|
|
1316
|
-
showStandalonePermissionSettingsDialog(activity, call);
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
prefs.edit().putBoolean(PREF_MIC_PERMISSION_REQUESTED, true).apply();
|
|
1321
|
-
|
|
1322
|
-
if (pendingPermissionAction == PendingPermissionAction.NONE) {
|
|
1323
|
-
pendingPermissionCall = call;
|
|
1324
|
-
call.setKeepAlive(true);
|
|
1325
|
-
}
|
|
1326
|
-
awaitingSettingsResult = false;
|
|
1327
|
-
permissionAttemptCount++;
|
|
1328
|
-
permissionRequestTimestamp = System.currentTimeMillis();
|
|
1329
|
-
Log.d(TAG, "requestMicrophonePermission: invoking ActivityCompat.requestPermissions (attempt " + permissionAttemptCount + ")");
|
|
1330
|
-
|
|
1331
|
-
ActivityCompat.requestPermissions(
|
|
1332
|
-
activity,
|
|
1333
|
-
new String[] { Manifest.permission.RECORD_AUDIO },
|
|
1334
|
-
REQUEST_CODE_RECORD_AUDIO_FOR_ACCEPT
|
|
1335
|
-
);
|
|
850
|
+
requestPermissions(call);
|
|
1336
851
|
}
|
|
1337
852
|
|
|
1338
853
|
@Override
|
|
1339
854
|
protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
|
1340
855
|
super.handleRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
1341
856
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
pendingPermissionAction +
|
|
1355
|
-
", standaloneCall=" +
|
|
1356
|
-
(pendingPermissionCall != null)
|
|
1357
|
-
);
|
|
1358
|
-
if (granted) {
|
|
1359
|
-
handleMicrophonePermissionGranted();
|
|
1360
|
-
} else {
|
|
1361
|
-
handleMicrophonePermissionDenied();
|
|
857
|
+
// If we were waiting to accept a call after mic permission
|
|
858
|
+
if (pendingCallSidForPermission != null) {
|
|
859
|
+
boolean granted =
|
|
860
|
+
ActivityCompat.checkSelfPermission(getSafeContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
861
|
+
String callSid = pendingCallSidForPermission;
|
|
862
|
+
pendingCallSidForPermission = null;
|
|
863
|
+
if (granted) {
|
|
864
|
+
Log.d(TAG, "RECORD_AUDIO granted from permission flow; proceeding to accept call");
|
|
865
|
+
proceedAcceptCall(callSid);
|
|
866
|
+
} else {
|
|
867
|
+
Log.w(TAG, "RECORD_AUDIO denied; cannot accept call");
|
|
868
|
+
}
|
|
1362
869
|
}
|
|
1363
870
|
}
|
|
1364
871
|
|
|
@@ -1578,8 +1085,13 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1578
1085
|
}
|
|
1579
1086
|
};*/
|
|
1580
1087
|
|
|
1581
|
-
private void showIncomingCallNotification(CallInvite callInvite, String callSid
|
|
1088
|
+
private void showIncomingCallNotification(CallInvite callInvite, String callSid) {
|
|
1582
1089
|
try {
|
|
1090
|
+
String callerName = callInvite.getFrom();
|
|
1091
|
+
if (callerName != null && callerName.startsWith("client:")) {
|
|
1092
|
+
callerName = callerName.substring(7); // Remove "client:" prefix
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1583
1095
|
// Create intent for accepting the call
|
|
1584
1096
|
PendingIntent acceptPendingIntent;
|
|
1585
1097
|
if (this.bridge == null) {
|
|
@@ -1696,22 +1208,16 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1696
1208
|
String callSid = UUID.randomUUID().toString(); // Generate a unique ID
|
|
1697
1209
|
activeCallInvites.put(callSid, callInvite);
|
|
1698
1210
|
|
|
1699
|
-
Map<String, String> params = callInvite.getCustomParameters();
|
|
1700
|
-
String callerName = params.containsKey("CapacitorTwilioCallerName")
|
|
1701
|
-
? params.get("CapacitorTwilioCallerName")
|
|
1702
|
-
: callInvite.getFrom();
|
|
1703
|
-
|
|
1704
1211
|
// Create and show notification
|
|
1705
|
-
showIncomingCallNotification(callInvite, callSid
|
|
1212
|
+
showIncomingCallNotification(callInvite, callSid);
|
|
1706
1213
|
|
|
1707
1214
|
// Start ringtone and vibration
|
|
1708
1215
|
startRingtone();
|
|
1709
1216
|
|
|
1710
1217
|
JSObject data = new JSObject();
|
|
1711
1218
|
data.put("callSid", callSid);
|
|
1712
|
-
data.put("from",
|
|
1219
|
+
data.put("from", callInvite.getFrom());
|
|
1713
1220
|
data.put("to", callInvite.getTo());
|
|
1714
|
-
data.put("customParams", new JSONObject(params));
|
|
1715
1221
|
notifyListeners("callInviteReceived", data);
|
|
1716
1222
|
}
|
|
1717
1223
|
|
|
@@ -1772,9 +1278,19 @@ public class CapacitorTwilioVoicePlugin extends Plugin {
|
|
|
1772
1278
|
notifyListeners("callDisconnected", data);
|
|
1773
1279
|
|
|
1774
1280
|
Log.d(TAG, "Call rejected from notification");
|
|
1775
|
-
moveAppToBackgroundIfLocked();
|
|
1776
1281
|
} else {
|
|
1777
1282
|
Log.e(TAG, "Call invite not found for SID: " + callSid);
|
|
1778
1283
|
}
|
|
1779
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
@PluginMethod
|
|
1287
|
+
public void getPluginVersion(final PluginCall call) {
|
|
1288
|
+
try {
|
|
1289
|
+
final JSObject ret = new JSObject();
|
|
1290
|
+
ret.put("version", this.PLUGIN_VERSION);
|
|
1291
|
+
call.resolve(ret);
|
|
1292
|
+
} catch (final Exception e) {
|
|
1293
|
+
call.reject("Could not get plugin version", e);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1780
1296
|
}
|