diff --git a/CHANGELOG.md b/CHANGELOG.md index 76168f2e..886214c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.3.1 +* Feat: Add raw `Connect({Map?})` for all platforms * Refactor: [Web] Removed unused `web_callkit` event listeners. * Fix: [Web] Check if call SID is present when call is disconnected (this occurs if the call ends abruptly after starting, and `params` does not contain `CallSid`). * Fix: [iOS] unregister removes device push token preventing new access token registration (i.e. user 1 logs out, user 2 and more won't receive any calls). Thanks to [@VinceDollo](https://github.com/VinceDollo) & [@Erchil66](https://github.com/Erchil66) [Issue #273](https://github.com/cybex-dev/twilio_voice/issues/273) diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt index 726d7a3e..bae65b9e 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -636,6 +636,57 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } } + TVMethodChannels.CONNECT -> { + val args = call.arguments as? Map<*, *> ?: run { + result.error( + FlutterErrorCodes.MALFORMED_ARGUMENTS, + "Arguments should be a Map<*, *>", + null + ) + return@onMethodCall + } + + Log.d(TAG, "Making new call via connect") + logEvent("Making new call via connect") + val params = HashMap() + for ((key, value) in args) { + when (key) { + Constants.PARAM_TO, Constants.PARAM_FROM -> {} + else -> { + params[key.toString()] = value.toString() + } + } + } +// callOutgoing = true + val from = call.argument(Constants.PARAM_FROM) ?: run { + logEvent("No 'from' provided or invalid type, ignoring.") + "" + } + + val to = call.argument(Constants.PARAM_TO) ?: run { + logEvent("No 'to' provided or invalid type, ignoring.") + "" + } + val paramsStringify = JSONObject(args).toString() + Log.d(TAG, "calling with parameters: from: '$from' -> to: '$to', params: $paramsStringify") + + accessToken?.let { token -> + context?.let { ctx -> + val success = placeCall(ctx, token, from, to, params, connect = true) + result.success(success) + } ?: run { + Log.e(TAG, "Context is null, cannot place call") + result.success(false) + } + } ?: run { + result.error( + FlutterErrorCodes.MALFORMED_ARGUMENTS, + "No accessToken set, are you registered?", + null + ) + } + } + TVMethodChannels.REGISTER_CLIENT -> { val clientId = call.argument("id") ?: run { @@ -1040,13 +1091,14 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH private fun placeCall( ctx: Context, accessToken: String, - from: String, - to: String, - params: Map + from: String?, + to: String?, + params: Map, + connect: Boolean = false ): Boolean { assert(accessToken.isNotEmpty()) { "Twilio Access Token cannot be empty" } - assert(to.isNotEmpty()) { "To cannot be empty" } - assert(from.isNotEmpty()) { "From cannot be empty" } + assert(!connect && (to == null || to.isNotEmpty())) { "To cannot be empty" } + assert(!connect && (from == null || from.isNotEmpty())) { "From cannot be empty" } telecomManager?.let { tm -> if (!tm.hasCallCapableAccount(ctx, TVConnectionService::class.java.name)) { @@ -1083,6 +1135,9 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH Intent(ctx, TVConnectionService::class.java).apply { action = TVConnectionService.ACTION_PLACE_OUTGOING_CALL putExtra(TVConnectionService.EXTRA_TOKEN, accessToken) + if(connect) { + putExtra(TVConnectionService.EXTRA_CONNECT_RAW, true) + } putExtra(TVConnectionService.EXTRA_TO, to) putExtra(TVConnectionService.EXTRA_FROM, from) putExtra(TVConnectionService.EXTRA_OUTGOING_PARAMS, Bundle().apply { diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnectionService.kt b/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnectionService.kt index 0446b52e..224fb286 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnectionService.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnectionService.kt @@ -138,6 +138,11 @@ class TVConnectionService : ConnectionService() { */ const val EXTRA_TOKEN: String = "EXTRA_TOKEN" + /** + * Extra used with [ACTION_PLACE_OUTGOING_CALL] to place an outgoing call connection, denotes the call parameters treated as a Bundle. + */ + const val EXTRA_CONNECT_RAW: String = "EXTRA_CONNECT_RAW" + /** * Extra used with [ACTION_PLACE_OUTGOING_CALL] to place an outgoing call connection. Denotes the recipient's identity. */ @@ -334,40 +339,38 @@ class TVConnectionService : ConnectionService() { } ACTION_PLACE_OUTGOING_CALL -> { - // check required EXTRA_TOKEN, EXTRA_TO, EXTRA_FROM - val token = it.getStringExtra(EXTRA_TOKEN) ?: run { - Log.e(TAG, "onStartCommand: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TOKEN") - return@let - } - val to = it.getStringExtra(EXTRA_TO) ?: run { - Log.e(TAG, "onStartCommand: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TO") - return@let - } - val from = it.getStringExtra(EXTRA_FROM) ?: run { - Log.e(TAG, "onStartCommand: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_FROM") - return@let - } - // Get all params from bundle - val params = HashMap() - val outGoingParams = it.getParcelableExtraSafe(EXTRA_OUTGOING_PARAMS) - outGoingParams?.keySet()?.forEach { key -> - outGoingParams.getString(key)?.let { value -> - params[key] = value + val rawConnect = it.getBooleanExtra(EXTRA_CONNECT_RAW, false) + + fun getRequiredString(key: String, allowNullIfRaw: Boolean = false): String? { + val value = it.getStringExtra(key) + if (value == null) { + Log.e(TAG, "onStartCommand: ACTION_PLACE_OUTGOING_CALL is missing String $key") + if (!rawConnect || !allowNullIfRaw) return null } + return value } - // Add required params - params[EXTRA_FROM] = from - params[EXTRA_TO] = to - params[EXTRA_TOKEN] = token + val token = getRequiredString(EXTRA_TOKEN) ?: return@let + val to = getRequiredString(EXTRA_TO, allowNullIfRaw = true) + val from = getRequiredString(EXTRA_FROM, allowNullIfRaw = true) + + val params = buildMap { + it.getParcelableExtraSafe(EXTRA_OUTGOING_PARAMS)?.let { bundle -> + for (key in bundle.keySet()) { + bundle.getString(key)?.let { value -> put(key, value) } + } + } + put(EXTRA_TOKEN, token) + if (!rawConnect) { + to?.let { v -> put(EXTRA_TO, v) } + from?.let { v -> put(EXTRA_FROM, v) } + } + } - // Create Twilio Param bundles val myBundle = Bundle().apply { putBundle(EXTRA_OUTGOING_PARAMS, Bundle().apply { - params.forEach { (key, value) -> - putString(key, value) - } + params.forEach { (key, value) -> putString(key, value) } }) } diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/types/TVMethodChannels.kt b/android/src/main/kotlin/com/twilio/twilio_voice/types/TVMethodChannels.kt index 0d756e28..92fcdddc 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/types/TVMethodChannels.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/types/TVMethodChannels.kt @@ -47,7 +47,8 @@ enum class TVMethodChannels(val method: String) { IS_PHONE_ACCOUNT_ENABLED("isPhoneAccountEnabled"), REJECT_CALL_ON_NO_PERMISSIONS("rejectCallOnNoPermissions"), IS_REJECTING_CALL_ON_NO_PERMISSIONS("isRejectingCallOnNoPermissions"), - UPDATE_CALLKIT_ICON("updateCallKitIcon"); + UPDATE_CALLKIT_ICON("updateCallKitIcon"), + CONNECT("connect"); companion object { private val map = TVMethodChannels.values().associateBy(TVMethodChannels::method) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index d8ef9c9e..20741c60 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -141,6 +141,21 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand self.callTo = callTo self.identity = callFrom makeCall(to: callTo) + } else if flutterCall.method == "connect" { + guard let callTo = arguments["To"] as? String? else { + return + } + guard let callFrom = arguments["From"] as? String? else { + return + } + self.callArgs = arguments + self.callOutgoing = true + if let accessToken = arguments["accessToken"] as? String{ + self.accessToken = accessToken + } + self.callTo = callTo ?? "" + self.identity = callFrom ?? "" + makeCall(to: self.callTo) } else if flutterCall.method == "toggleMute" { diff --git a/lib/_internal/method_channel/twilio_call_method_channel.dart b/lib/_internal/method_channel/twilio_call_method_channel.dart index 3da1dc59..c1f1265d 100644 --- a/lib/_internal/method_channel/twilio_call_method_channel.dart +++ b/lib/_internal/method_channel/twilio_call_method_channel.dart @@ -114,9 +114,12 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { return _channel.invokeMethod('isBluetoothOn', {}); } - /// Only web supported for now. @override Future connect({Map? extraOptions}) { - return Future.value(false); + _activeCall = ActiveCall(from: "", to: "", callDirection: CallDirection.outgoing); + final options = { + ...?extraOptions, + }; + return _channel.invokeMethod('connect', options); } } diff --git a/lib/_internal/platform_interface/twilio_call_platform_interface.dart b/lib/_internal/platform_interface/twilio_call_platform_interface.dart index eaf45792..63f94c05 100644 --- a/lib/_internal/platform_interface/twilio_call_platform_interface.dart +++ b/lib/_internal/platform_interface/twilio_call_platform_interface.dart @@ -31,15 +31,9 @@ abstract class TwilioCallPlatform extends SharedPlatformInterface { /// [extraOptions] will be added to the callPayload sent to your server Future place({required String from, required String to, Map? extraOptions}); - /// Place outgoing call with raw parameters. Returns true if successful, false otherwise. - /// Parameters send to Twilio's REST API endpoint 'makeCall' can be passed in [extraOptions]; - /// Parameters are reduced to this format - /// - /// { - /// ...extraOptions - /// } - /// - /// [extraOptions] will be added to the call payload sent to your server + /// Places new call using raw parameters passed directly to Twilio's REST API endpoint 'makeCall'. Returns true if successful, false otherwise. + /// + /// [extraOptions] will be added to the callPayload sent to your server Future connect({Map? extraOptions}); /// Hangs up active call diff --git a/lib/_internal/twilio_voice_web.dart b/lib/_internal/twilio_voice_web.dart index dfad6ec3..e85c273f 100644 --- a/lib/_internal/twilio_voice_web.dart +++ b/lib/_internal/twilio_voice_web.dart @@ -786,14 +786,9 @@ class Call extends MethodChannelTwilioCall { return true; } - /// Place outgoing call with raw parameters. Returns true if successful, false otherwise. - /// Parameters send to Twilio's REST API endpoint 'makeCall' can be passed in [extraOptions]; - /// Parameters are reduced to this format - /// - /// { - /// ...extraOptions - /// } - /// + /// Places new call using raw parameters passed directly to Twilio's REST API endpoint 'makeCall'. Returns true if successful, false otherwise. + /// + /// [extraOptions] will be added to the callPayload sent to your server /// See [twilio_js.Device.connect] @override Future connect({Map? extraOptions}) async { diff --git a/macos/Classes/JsInterop/Device/TVDeviceConnectOptions.swift b/macos/Classes/JsInterop/Device/TVDeviceConnectOptions.swift index 749cec4d..d0705f1b 100644 --- a/macos/Classes/JsInterop/Device/TVDeviceConnectOptions.swift +++ b/macos/Classes/JsInterop/Device/TVDeviceConnectOptions.swift @@ -6,9 +6,9 @@ public class TVDeviceConnectOptions: JSONArgumentSerializer { // TODO(cybex-dev) - add region, edge information, etc. var params: [String: String] = [:] - init(to: String, from: String, customParameters: [String:Any]) { - params[Constants.PARAM_TO] = to - params[Constants.PARAM_FROM] = from + init(to: String?, from: String?, customParameters: [String:Any]) { + if(to != nil) params[Constants.PARAM_TO] = to + if(from != nil) params[Constants.PARAM_FROM] = from let stringMap = customParameters.map({ (key, value) -> (String, String) in (key, String(describing: value)) }); diff --git a/macos/Classes/TwilioVoiceChannelMethods.swift b/macos/Classes/TwilioVoiceChannelMethods.swift index 9458c3a2..36e81261 100644 --- a/macos/Classes/TwilioVoiceChannelMethods.swift +++ b/macos/Classes/TwilioVoiceChannelMethods.swift @@ -3,6 +3,7 @@ import Foundation public enum TwilioVoiceChannelMethods: String { case tokens = "tokens" case makeCall = "makeCall" + case connect = "connect" case toggleMute = "toggleMute" case isMuted = "isMuted" case toggleSpeaker = "toggleSpeaker" diff --git a/macos/Classes/TwilioVoicePlugin.swift b/macos/Classes/TwilioVoicePlugin.swift index 2165bbaf..e8b2486c 100644 --- a/macos/Classes/TwilioVoicePlugin.swift +++ b/macos/Classes/TwilioVoicePlugin.swift @@ -209,7 +209,7 @@ public class TwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler, T /// - to: recipient /// - extraOptions: extra options /// - completionHandler: completion handler -> (Bool?) - private func place(from: String, to: String, extraOptions: [String: Any]?, completionHandler: @escaping OnCompletionValueHandler) -> Void { + private func place(from: String?, to: String?, extraOptions: [String: Any]?, completionHandler: @escaping OnCompletionValueHandler) -> Void { assert(from.isNotEmpty(), "\(Constants.PARAM_FROM) cannot be empty") assert(to.isNotEmpty(), "\(Constants.PARAM_TO) cannot be empty") // assert(extraOptions?.keys.contains(Constants.PARAM_FROM) ?? true, "\(Constants.PARAM_FROM) cannot be passed in extraOptions") @@ -218,7 +218,10 @@ public class TwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler, T logEvent(description: "Making new call") - var params: [String: Any] = [Constants.PARAM_FROM: from, Constants.PARAM_TO: to] + var params: [String: Any] = [ + if(from != nil) Constants.PARAM_FROM: from, + if(to != nil) Constants.PARAM_TO: to, + ] if let extraOptions = extraOptions { params.merge(extraOptions) { (_, new) in new @@ -591,6 +594,21 @@ public class TwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler, T } } + place(from: from, to: to, extraOptions: params) { success in + result(success ?? false) + } + break + case .connect: + guard let to = arguments[Constants.PARAM_TO] as? String? + guard let from = arguments[Constants.PARAM_FROM] as? String + + var params: [String: Any] = [:] + arguments.forEach { (key, value) in + if key != Constants.PARAM_TO && key != Constants.PARAM_FROM { + params[key] = value + } + } + place(from: from, to: to, extraOptions: params) { success in result(success ?? false) }