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 bae65b9e..f52362d6 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -195,6 +195,9 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH error.message ) logEvent(message) + logEvent("", "Call Ended") + TVConnectionService.clearActiveConnections() + } override fun onConnected(call: Call) { @@ -471,6 +474,24 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } } + TVMethodChannels.GetActiveCallOnResumeFromTerminatedState -> { + //is on call + val hasActiveCalls = isOnCall() + if(hasActiveCalls){ + val activeCalls = TVConnectionService.Companion.activeConnections + val currentCall = activeCalls.values.firstOrNull() + val isAnsweredCall = currentCall?.twilioCall?.state == Call.State.CONNECTED + if(isAnsweredCall){ + val from = extractUserNumber(currentCall?.twilioCall?.from ?: "") + val to = currentCall?.twilioCall?.to ?: "" + val callDirection = currentCall?.callDirection ?: CallDirection.INCOMING + logEvents("", arrayOf("Connected", from, to, callDirection.label )) + } + } + result.success(true) + + } + TVMethodChannels.IS_BLUETOOTH_ON -> { Log.d(TAG, "isBluetoothOn invoked") result.success(isBluetoothOn) @@ -617,11 +638,22 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH ) return@onMethodCall } + + val callerName = call.argument(Constants.CALLER_NAME) ?: run { + result.error( + FlutterErrorCodes.MALFORMED_ARGUMENTS, + "No '${Constants.CALLER_NAME}' provided or invalid type", + null + ) + return@onMethodCall + } + + Log.d(TAG, "calling $from -> $to") accessToken?.let { token -> context?.let { ctx -> - val success = placeCall(ctx, token, from, to, params) + val success = placeCall(ctx, token, from, to, params,callerName ) result.success(success) } ?: run { Log.e(TAG, "Context is null, cannot place call") @@ -672,7 +704,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH accessToken?.let { token -> context?.let { ctx -> - val success = placeCall(ctx, token, from, to, params, connect = true) + val success = placeCall(ctx, token, from, to, params,null, connect = true) result.success(success) } ?: run { Log.e(TAG, "Context is null, cannot place call") @@ -1094,6 +1126,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH from: String?, to: String?, params: Map, + callerName: String?, connect: Boolean = false ): Boolean { assert(accessToken.isNotEmpty()) { "Twilio Access Token cannot be empty" } @@ -1140,6 +1173,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } putExtra(TVConnectionService.EXTRA_TO, to) putExtra(TVConnectionService.EXTRA_FROM, from) + putExtra(TVConnectionService.EXTRA_CALLER_NAME, callerName) putExtra(TVConnectionService.EXTRA_OUTGOING_PARAMS, Bundle().apply { for ((key, value) in params) { putString(key, value) @@ -1585,6 +1619,18 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } } + + fun extractUserNumber(input: String): String { + // Define the regular expression pattern to match the user_number part + val pattern = Regex("""user_number:([^\s:]+)""") + + // Search for the first match in the input string + val match = pattern.find(input) + + // Extract the matched part (user_number:+11230123) + return match?.groups?.get(1)?.value ?: input + } + private fun requestPermissionForPhoneState(onPermissionResult: (Boolean) -> Unit) { return requestPermissionOrShowRationale( "Read Phone State", @@ -1661,6 +1707,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH fun handleBroadcastIntent(intent: Intent) { when (intent.action) { TVBroadcastReceiver.ACTION_AUDIO_STATE -> { + println("Event called Basil : TVBroadcastReceiver.ACTION_AUDIO_STATE") val callAudioState: CallAudioState = intent.getParcelableExtraSafe(TVBroadcastReceiver.EXTRA_AUDIO_STATE) ?: run { Log.e( @@ -1715,7 +1762,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH ) return } - val from = callInvite.from ?: "" + val from = extractUserNumber(callInvite.from ?: "") val to = callInvite.to val params = JSONObject().apply { callInvite.customParameters.forEach { (key, value) -> @@ -1728,6 +1775,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVBroadcastReceiver.ACTION_CALL_ENDED -> { + println("Event called Basil : TVBroadcastReceiver.ACTION_CALL_ENDED") val callHandle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: run { Log.e( @@ -1782,7 +1830,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH ) return } - val from = ci.from ?: "" + val from = extractUserNumber(ci.from ?: "") val to = ci.to val params = JSONObject().apply { ci.customParameters.forEach { (key, value) -> @@ -1892,6 +1940,9 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH return } logEvent("Call Error: ${code}, $message"); + logEvent("", "Call Ended") + TVConnectionService.clearActiveConnections() + } TVNativeCallEvents.EVENT_RECONNECTING -> { diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/call/TVParametersImpl.kt b/android/src/main/kotlin/com/twilio/twilio_voice/call/TVParametersImpl.kt index 6ae3e91a..3e8b35a2 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/call/TVParametersImpl.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/call/TVParametersImpl.kt @@ -22,17 +22,18 @@ class TVCallInviteParametersImpl(storage: Storage, callInvite: CallInvite) : TVP ?: customParameters[PARAM_CALLER_ID]?.let { resolveHumanReadableName(it) } ?: run { val mFrom = mCallInvite.from ?: "" - if (mFrom.isEmpty()) { - return mStorage.defaultCaller - } - - if (!mFrom.startsWith("client:")) { - // we have a number, return as is - return mFrom - } - - val mToName = mFrom.replace("client:", "") - return resolveHumanReadableName(mToName) +// if (mFrom.isEmpty()) { +// return mStorage.defaultCaller +// } +// +// if (!mFrom.startsWith("client:")) { +// // we have a number, return as is +// return mFrom +// } +// +// val mToName = mFrom.replace("client:", "") +// return resolveHumanReadableName(mToName) + return extractClient(mFrom) } } @@ -55,7 +56,7 @@ class TVCallInviteParametersImpl(storage: Storage, callInvite: CallInvite) : TVP override val fromRaw: String get() { - return mCallInvite.from ?: "" + return extractClient( mCallInvite.from ?: "") } override val toRaw: String @@ -91,17 +92,18 @@ class TVCallParametersImpl(storage: Storage, call: Call, callTo: String, callFro return customParameters[PARAM_CALLER_NAME] ?: customParameters[PARAM_CALLER_ID]?.let { resolveHumanReadableName(it) } ?: run { - if (mFrom.isEmpty()) { - return mStorage.defaultCaller - } - - if (!mFrom.startsWith("client:")) { - // we have a number, return as is - return mFrom - } - - val mFromName = mFrom.replace("client:", "") - return resolveHumanReadableName(mFromName) +// if (mFrom.isEmpty()) { +// return mStorage.defaultCaller +// } +// +// if (!mFrom.startsWith("client:")) { +// // we have a number, return as is +// return mFrom +// } +// +// val mFromName = mFrom.replace("client:", "") +// return resolveHumanReadableName(mFromName) + return extractClient(mFrom) } } @@ -126,7 +128,7 @@ class TVCallParametersImpl(storage: Storage, call: Call, callTo: String, callFro override val fromRaw: String get() { - return mFrom + return extractClient(mFrom) } override val toRaw: String @@ -197,4 +199,27 @@ open class TVParametersImpl(storage: Storage, override val callSid: String = "", override fun toString(): String { return "TVParametersImpl(callSid='$callSid', from='$from', fromRaw='$fromRaw' to='$to', toRaw='$toRaw', customParameters=$customParameters)" } + + fun extractUserNumber(input: String): String { + // Define the regular expression pattern to match the user_number part + val pattern = Regex("""user_number:([^\s:]+)""") + + // Search for the first match in the input string + val match = pattern.find(input) + + // Extract the matched part (user_number:+11230123) + return match?.groups?.get(1)?.value ?: input + } + + fun extractClient(input: String): String { + // // Define the regular expression pattern to match the client part + // val pattern = Regex("""client:([^\s:]+)""") + + // // Search for the first match in the input string + // val match = pattern.find(input) + + // // Extract the matched part (client:+11230(123)) + // return match?.groups?.get(1)?.value ?: input + return input + } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/constants/Constants.kt b/android/src/main/kotlin/com/twilio/twilio_voice/constants/Constants.kt index 0bed977a..31d9e57d 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/constants/Constants.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/constants/Constants.kt @@ -18,4 +18,5 @@ object Constants { const val kDefaultCaller: String = "defaultCaller" const val kDEVICETOKEN: String = "DEVICETOKEN" + const val CALLER_NAME: String = "CallerName" } \ No newline at end of file diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt b/android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt index ab916fbc..2c4d6c11 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt @@ -152,7 +152,7 @@ class VoiceFirebaseMessagingService : FirebaseMessagingService(), MessageListene // send broadcast to TVBroadcastReceiver, we notify Flutter about incoming call Intent(applicationContext, TVBroadcastReceiver::class.java).apply { action = TVBroadcastReceiver.ACTION_INCOMING_CALL - putExtra(TVBroadcastReceiver.EXTRA_CALL_INVITE, callInvite) + putExtra(TVBroadcastReceiver.EXTRA_CALL_INVITE , callInvite) putExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE, callInvite.callSid) LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(this) } diff --git a/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnection.kt b/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnection.kt index adeb7f75..7ede5a81 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnection.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnection.kt @@ -44,15 +44,23 @@ class TVCallInviteConnection( setCallParameters(callParams) } - override fun onAnswer() { - Log.d(TAG, "onAnswer: onAnswer") - super.onAnswer() - twilioCall = callInvite.accept(context, this) - onAction?.onChange(TVNativeCallActions.ACTION_ANSWERED, Bundle().apply { - putParcelable(TVBroadcastReceiver.EXTRA_CALL_INVITE, callInvite) - putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id) - }) - } + override fun onAnswer() { + Log.d(TAG, "onAnswer: onAnswer") + super.onAnswer() + + // Accept the call and assign it to twilioCall + twilioCall = callInvite.accept(context, this) + + // Extract the user number from callInvite.from using your custom extractUserNumber method + val extractedFrom = extractUserNumber(callInvite.from ?: "") + + // Broadcast the call answered action with the extracted number + onAction?.onChange(TVNativeCallActions.ACTION_ANSWERED, Bundle().apply { + putParcelable(TVBroadcastReceiver.EXTRA_CALL_INVITE, callInvite) + putString(TVBroadcastReceiver.EXTRA_CALL_FROM, extractedFrom) // Use extracted number here + putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id) + }) +} fun acceptInvite() { Log.d(TAG, "acceptInvite: acceptInvite") @@ -183,12 +191,24 @@ open class TVCallConnection( onCallStateListener?.withValue(call.state) onEvent?.onChange(TVNativeCallEvents.EVENT_RINGING, Bundle().apply { putString(TVBroadcastReceiver.EXTRA_CALL_HANDLE, callParams?.callSid) - putString(TVBroadcastReceiver.EXTRA_CALL_FROM, callParams?.fromRaw) + putString(TVBroadcastReceiver.EXTRA_CALL_FROM,extractUserNumber( callParams?.fromRaw ?: "")) putString(TVBroadcastReceiver.EXTRA_CALL_TO, callParams?.toRaw) putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id) }) } + + fun extractUserNumber(input: String): String { + // Define the regular expression pattern to match the user_number part + val pattern = Regex("""user_number:([^\s:]+)""") + + // Search for the first match in the input string + val match = pattern.find(input) + + // Extract the matched part (user_number:+11230123) + return match?.groups?.get(1)?.value ?: input + } + override fun onConnected(call: Call) { Log.d(TAG, "onConnected: onConnected") twilioCall = call @@ -196,7 +216,7 @@ open class TVCallConnection( onCallStateListener?.withValue(call.state) onEvent?.onChange(TVNativeCallEvents.EVENT_CONNECTED, Bundle().apply { putString(TVBroadcastReceiver.EXTRA_CALL_HANDLE, callParams?.callSid) - putString(TVBroadcastReceiver.EXTRA_CALL_FROM, callParams?.fromRaw) + putString(TVBroadcastReceiver.EXTRA_CALL_FROM,extractUserNumber(callParams?.fromRaw ?: "" )) putString(TVBroadcastReceiver.EXTRA_CALL_TO, callParams?.toRaw) putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id) }) @@ -512,4 +532,6 @@ open class TVCallConnection( Log.e(TAG, "sendDigits: Unable to send digits, active call is null") } } -} \ No newline at end of file +} + + 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 224fb286..caff44c8 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 @@ -148,6 +148,8 @@ class TVConnectionService : ConnectionService() { */ const val EXTRA_TO: String = "EXTRA_TO" + const val EXTRA_CALLER_NAME: String = "EXTRA_CALLER_NAME" + /** * Extra used with [ACTION_PLACE_OUTGOING_CALL] to place an outgoing call connection. Denotes the caller's identity. */ @@ -183,6 +185,14 @@ class TVConnectionService : ConnectionService() { return activeConnections.isNotEmpty() } + //clear active connections + fun clearActiveConnections() { + activeConnections.clear() + } + + + + /** * Active call definition is extended to include calls in which one can actively communicate, or call is on hold, or call is ringing or dialing. This applies only to this and calling functions. * Gets the first ongoing call handle, if any. Else, gets the first call on hold. Lastly, gets the first call in either a ringing or dialing state, if any. Returns null if there are no active calls. If there are more than one active calls, the first call handle is returned. @@ -304,7 +314,7 @@ class TVConnectionService : ConnectionService() { putString(TelecomManager.EXTRA_CALL_SUBJECT, callInvite.customParameters["_TWI_SUBJECT"]) } } - + android.util.Log.d(TAG, "onCallRecived basil: $extras") // Add new incoming call to the telecom manager telecomManager.addNewIncomingCall(phoneAccountHandle, extras) } @@ -330,6 +340,7 @@ class TVConnectionService : ConnectionService() { ACTION_HANGUP -> { val callHandle = it.getStringExtra(EXTRA_CALL_HANDLE) ?: getActiveCallHandle() ?: run { Log.e(TAG, "onStartCommand: ACTION_HANGUP is missing String EXTRA_CALL_HANDLE") + activeConnections.clear() return@let } @@ -354,7 +365,7 @@ class TVConnectionService : ConnectionService() { val token = getRequiredString(EXTRA_TOKEN) ?: return@let val to = getRequiredString(EXTRA_TO, allowNullIfRaw = true) val from = getRequiredString(EXTRA_FROM, allowNullIfRaw = true) - + val outgoingName = getRequiredString(EXTRA_CALLER_NAME, allowNullIfRaw = true) val params = buildMap { it.getParcelableExtraSafe(EXTRA_OUTGOING_PARAMS)?.let { bundle -> for (key in bundle.keySet()) { @@ -362,6 +373,7 @@ class TVConnectionService : ConnectionService() { } } put(EXTRA_TOKEN, token) + put(EXTRA_CALLER_NAME, outgoingName) if (!rawConnect) { to?.let { v -> put(EXTRA_TO, v) } from?.let { v -> put(EXTRA_FROM, v) } @@ -516,7 +528,7 @@ class TVConnectionService : ConnectionService() { // Setup connection event listeners and UI parameters attachCallEventListeners(connection, ci.callSid) - applyParameters(connection, callParams) + applyParameters(connection, callParams,null) connection.setRinging() startForegroundService() @@ -550,6 +562,11 @@ class TVConnectionService : ConnectionService() { throw Exception("onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_FROM"); } + val outGoingCallerName = myBundle.getString(EXTRA_CALLER_NAME) ?: run { + Log.e(TAG, "onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_FROM") + throw Exception("onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_CALLER_NAME"); + } + // Get all params from bundle val params = HashMap() myBundle.keySet().forEach { key -> @@ -591,7 +608,7 @@ class TVConnectionService : ConnectionService() { // If call is not attached, attach it if (!activeConnections.containsKey(callSid)) { - applyParameters(connection, callParams) + applyParameters(connection, callParams,outGoingCallerName ) attachCallEventListeners(connection, callSid) callParams.callSid = callSid } @@ -687,15 +704,38 @@ class TVConnectionService : ConnectionService() { * @param connection The connection to apply the parameters to. * @param params The parameters to apply to the connection. */ - private fun applyParameters(connection: T, params: TVParameters) { + private fun applyParameters(connection: T, params: TVParameters, outgoingCallerName: String?) { params.getExtra(TVParameters.PARAM_SUBJECT, null)?.let { connection.extras.putString(TelecomManager.EXTRA_CALL_SUBJECT, it) } val name = if(connection.callDirection == CallDirection.OUTGOING) params.to else params.from - connection.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, name, null), TelecomManager.PRESENTATION_ALLOWED) - connection.setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED) + + val userName = params.customParameters["client_name"] + val userNumber = extractUserNumber(name) + + if(connection.callDirection == CallDirection.OUTGOING){ + + connection.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, name, null), TelecomManager.PRESENTATION_ALLOWED) + connection.setCallerDisplayName(outgoingCallerName, TelecomManager.PRESENTATION_ALLOWED) + } else { + connection.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, userNumber, null), TelecomManager.PRESENTATION_ALLOWED) + connection.setCallerDisplayName(userName, TelecomManager.PRESENTATION_ALLOWED) + } + } + fun extractUserNumber(input: String): String { + // Define the regular expression pattern to match the user_number part + val pattern = Regex("""user_number:([^\s:]+)""") + + // Search for the first match in the input string + val match = pattern.find(input) + + // Extract the matched part (user_number:+11230123) + return match?.groups?.get(1)?.value ?: input + } + + private fun sendBroadcastEvent(ctx: Context, event: String, callSid: String?, extras: Bundle? = null) { Intent(ctx, TVBroadcastReceiver::class.java).apply { action = event @@ -717,6 +757,9 @@ class TVConnectionService : ConnectionService() { override fun onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?) { super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request) Log.d(TAG, "onCreateOutgoingConnectionFailed") + println("Call error happened basil") + //clear the active connections + activeConnections.clear() stopForegroundService() } 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 92fcdddc..366dc10b 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,6 +47,7 @@ enum class TVMethodChannels(val method: String) { IS_PHONE_ACCOUNT_ENABLED("isPhoneAccountEnabled"), REJECT_CALL_ON_NO_PERMISSIONS("rejectCallOnNoPermissions"), IS_REJECTING_CALL_ON_NO_PERMISSIONS("isRejectingCallOnNoPermissions"), + GetActiveCallOnResumeFromTerminatedState("getActiveCallOnResumeFromTerminatedState"), UPDATE_CALLKIT_ICON("updateCallKitIcon"), CONNECT("connect"); diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 571ef349..4577b8a3 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ + 03CB5A298F020DFEF5C15BF9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05447BA51642A37726BD13F6 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 31921B64B8BFB1D14D5B3FD8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4B6D8E27AE1C4FFBA3F93FB /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -32,14 +32,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 05447BA51642A37726BD13F6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0FD3BBD256FE4A9D5485441A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 11DC5E9496FA4567BF18955B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 459910D105676AEA61D927D4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 842FBBAEA17BDB340A4F1B1A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -47,8 +48,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A1A5B7F7A8766107BD0F93D1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D4B6D8E27AE1C4FFBA3F93FB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D081A7677F6E275D726CDCC0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; F112FE34262881BD00CD6B41 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; F18CB62F25F2968500653E8A /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; F1DAF69F2624E5BF00652032 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -60,17 +60,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 31921B64B8BFB1D14D5B3FD8 /* Pods_Runner.framework in Frameworks */, + 03CB5A298F020DFEF5C15BF9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0E04F9C8A2F4C5BDEBF53455 /* Frameworks */ = { + 3F251483BF6591EB5D9A52FE /* Frameworks */ = { isa = PBXGroup; children = ( - D4B6D8E27AE1C4FFBA3F93FB /* Pods_Runner.framework */, + 05447BA51642A37726BD13F6 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -78,9 +78,9 @@ 8070562AC07E27A0A2D66AC1 /* Pods */ = { isa = PBXGroup; children = ( - 459910D105676AEA61D927D4 /* Pods-Runner.debug.xcconfig */, - A1A5B7F7A8766107BD0F93D1 /* Pods-Runner.release.xcconfig */, - 842FBBAEA17BDB340A4F1B1A /* Pods-Runner.profile.xcconfig */, + 0FD3BBD256FE4A9D5485441A /* Pods-Runner.debug.xcconfig */, + 11DC5E9496FA4567BF18955B /* Pods-Runner.release.xcconfig */, + D081A7677F6E275D726CDCC0 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -103,7 +103,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 8070562AC07E27A0A2D66AC1 /* Pods */, - 0E04F9C8A2F4C5BDEBF53455 /* Frameworks */, + 3F251483BF6591EB5D9A52FE /* Frameworks */, ); sourceTree = ""; }; @@ -140,14 +140,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 4BF2D767FFDDA6DFF200B4DE /* [CP] Check Pods Manifest.lock */, + DEDD89877982A2AF7CD3C3DA /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - E523679BDB4BF3E08E047D74 /* [CP] Embed Pods Frameworks */, + 2C0550190AA281E4DABEDF6E /* [CP] Embed Pods Frameworks */, + BBC5A4627D503F8C23DDACC1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -164,7 +165,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -209,47 +210,39 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 2C0550190AA281E4DABEDF6E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Thin Binary"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 4BF2D767FFDDA6DFF200B4DE /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -262,21 +255,43 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - E523679BDB4BF3E08E047D74 /* [CP] Embed Pods Frameworks */ = { + BBC5A4627D503F8C23DDACC1 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DEDD89877982A2AF7CD3C3DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -364,7 +379,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -384,7 +399,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -445,7 +460,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -495,7 +510,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -517,7 +532,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -542,7 +557,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/lib/main.dart b/example/lib/main.dart index c917e9d8..2dbc435f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:io'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twilio_voice/twilio_voice.dart'; @@ -31,7 +31,8 @@ enum RegistrationMethod { static RegistrationMethod? fromString(String? value) { if (value == null) return null; - return RegistrationMethod.values.firstWhereOrNull((element) => element.name == value); + return RegistrationMethod.values + .firstWhereOrNull((element) => element.name == value); } static RegistrationMethod? loadFromEnvironment() { @@ -43,8 +44,7 @@ enum RegistrationMethod { void main() async { WidgetsFlutterBinding.ensureInitialized(); if (kIsWeb) { - if(firebaseEnabled) { - + if (firebaseEnabled) { // Add firebase config here const options = FirebaseOptions( apiKey: '', @@ -68,11 +68,13 @@ void main() async { await Firebase.initializeApp(); } - if(firebaseEnabled) { + if (firebaseEnabled) { FirebaseAnalytics.instance.logEvent(name: "app_started"); } - final app = App(registrationMethod: RegistrationMethod.loadFromEnvironment() ?? RegistrationMethod.env); + final app = App( + registrationMethod: + RegistrationMethod.loadFromEnvironment() ?? RegistrationMethod.env); return runApp(MaterialApp(home: app)); } @@ -134,10 +136,18 @@ class _AppState extends State { // } // } - if(firebaseEnabled) { + if (firebaseEnabled) { FirebaseAnalytics.instance.logEvent(name: "registration", parameters: { "method": widget.registrationMethod.name, - "platform": kIsWeb ? "web" : Platform.isAndroid ? "android" : Platform.isIOS ? "ios" : Platform.isMacOS ? "macos" : "unknown" + "platform": kIsWeb + ? "web" + : Platform.isAndroid + ? "android" + : Platform.isIOS + ? "ios" + : Platform.isMacOS + ? "macos" + : "unknown" }); } } @@ -147,12 +157,13 @@ class _AppState extends State { printDebug("voip-registering access token"); String? androidToken; - if(!kIsWeb && Platform.isAndroid) { + if (!kIsWeb && Platform.isAndroid) { // Get device token for Android only androidToken = await FirebaseMessaging.instance.getToken(); printDebug("androidToken is ${androidToken!}"); } - final result = await TwilioVoice.instance.setTokens(accessToken: accessToken, deviceToken: androidToken); + final result = await TwilioVoice.instance + .setTokens(accessToken: accessToken, deviceToken: androidToken); return result ?? false; } @@ -172,7 +183,8 @@ class _AppState extends State { printDebug("voip-registering with environment variables"); if (myId == null || myToken == null) { - printDebug("Failed to register with environment variables, please provide ID and TOKEN"); + printDebug( + "Failed to register with environment variables, please provide ID and TOKEN"); return false; } userId = myId; @@ -289,7 +301,8 @@ class _AppState extends State { // applies to web only if (kIsWeb || Platform.isAndroid) { final activeCall = TwilioVoice.instance.call.activeCall; - if (activeCall != null && activeCall.callDirection == CallDirection.incoming) { + if (activeCall != null && + activeCall.callDirection == CallDirection.incoming) { _showWebIncomingCallDialog(); } } @@ -329,7 +342,11 @@ class _AppState extends State { return; } printDebug("starting call to $clientIdentifier"); - TwilioVoice.instance.call.place(to: clientIdentifier, from: userId, extraOptions: {"_TWI_SUBJECT": "Company Name"}); + TwilioVoice.instance.call.place( + to: clientIdentifier, + from: userId, + extraOptions: {"_TWI_SUBJECT": "Company Name"}, + callerName: ""); } Future _onRegisterWithToken(String token, [String? identity]) async { @@ -409,7 +426,8 @@ class _AppState extends State { } } - Future showIncomingCallScreen(BuildContext context, ActiveCall activeCall) async { + Future showIncomingCallScreen( + BuildContext context, ActiveCall activeCall) async { if (!kIsWeb && !Platform.isAndroid) { printDebug("showIncomingCallScreen only for web"); return false; @@ -446,7 +464,8 @@ class _LogoutAction extends StatelessWidget { final void Function()? onSuccess; final void Function(String error)? onFailure; - const _LogoutAction({Key? key, this.onSuccess, this.onFailure}) : super(key: key); + const _LogoutAction({Key? key, this.onSuccess, this.onFailure}) + : super(key: key); @override Widget build(BuildContext context) { diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index a9ff770c..e27a8f68 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,8 +13,8 @@ import firebase_messaging import twilio_voice func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) - FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) + FirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FirebaseFunctionsPlugin")) + FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 91647558..9649bd95 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -44,9 +44,10 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand var callKitCallController: CXCallController var userInitiatedDisconnect: Bool = false var callOutgoing: Bool = false - + var outgoingCallerName = "" + private var activeCalls: [UUID: CXCall] = [:] - + static var appName: String { get { return (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? "Define CFBundleName" @@ -69,7 +70,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand //super.init(coder: aDecoder) super.init() callObserver.setDelegate(self, queue: DispatchQueue.main) - + callKitProvider.setDelegate(self, queue: nil) _ = updateCallKitIcon(icon: defaultIcon) @@ -133,6 +134,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand } else if flutterCall.method == "makeCall" { guard let callTo = arguments["To"] as? String else {return} guard let callFrom = arguments["From"] as? String else {return} + let callerName = arguments["CallerName"] as? String + outgoingCallerName = callerName ?? "" self.callArgs = arguments self.callOutgoing = true if let accessToken = arguments["accessToken"] as? String{ @@ -226,6 +229,19 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand self.call!.sendDigits(digits); } } + else if flutterCall.method == "getActiveCallOnResumeFromTerminatedState" + { + let isCallAnswered = self.call != nil + if let call = self.call{ + let direction = (self.callOutgoing ? "Outgoing" : "Incoming") + let from = extractUserNumber(from: call.from ?? self.identity) + let to = call.to ?? self.callTo + self.sendPhoneCallEvents(description: "Connected|\(from)|\(to)|\(direction)", isError: false) + } + result(true) + } + + /* else if flutterCall.method == "receiveCalls" { guard let clientIdentity = arguments["clientIdentifier"] as? String else {return} @@ -381,7 +397,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand func answerCall(callInvite: CallInvite) { let answerCallAction = CXAnswerCallAction(call: callInvite.uuid) let transaction = CXTransaction(action: answerCallAction) - + callKitCallController.request(transaction) { error in if let error = error { self.sendPhoneCallEvents(description: "LOG|AnswerCallAction transaction request failed: \(error.localizedDescription)", isError: false) @@ -389,7 +405,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand } } } - + func makeCall(to: String) { // Cancel the previous call before making another one. @@ -549,8 +565,10 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand self.sendPhoneCallEvents(description: "LOG|Successfully unregistered from VoIP push notifications.", isError: false) } } + //DO NOT REMOVE DEVICE TOKEN , AS IT IS UNNECESSARY AND USER WILL HAVE TO RESTART THE APP TO GET NEW DEVICE TOKEN + //IF WE REMOVED FROM HERE , WHICH WILL CAUSE TO FAILURE IN REGISTRATION //UserDefaults.standard.removeObject(forKey: kCachedDeviceToken) - + // Remove the cached binding as credentials are invalidated //UserDefaults.standard.removeObject(forKey: kCachedBindingDate) } @@ -602,12 +620,12 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand activeCalls[uuid] = call // Add or update call } } - + // Check if a call with a given UUID exists func isCallActive(uuid: UUID) -> Bool { return activeCalls[uuid] != nil } - + func incomingPushHandled() { if let completion = self.incomingPushCompletionCallback { self.incomingPushCompletionCallback = nil @@ -626,14 +644,36 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand */ UserDefaults.standard.set(Date(), forKey: kCachedBindingDate) - var from:String = callInvite.from ?? defaultCaller - from = from.replacingOccurrences(of: "client:", with: "") - - self.sendPhoneCallEvents(description: "Ringing|\(from)|\(callInvite.to)|Incoming\(formatCustomParams(params: callInvite.customParameters))", isError: false) - reportIncomingCall(from: from, uuid: callInvite.uuid) - self.callInvite = callInvite + let incomingCallerDetails:String = callInvite.from ?? defaultCaller + let userNumber:String = extractUserNumber(from: incomingCallerDetails) + let client:String = callInvite.customParameters?["client_name"] ?? userNumber + var from:String = callInvite.from ?? defaultCaller + from = userNumber + + self.sendPhoneCallEvents(description: "Ringing|\(from)|\(callInvite.to)|Incoming\(formatCustomParams(params: callInvite.customParameters))", isError: false) + reportIncomingCall(from: client, uuid: callInvite.uuid) + self.callInvite = callInvite + } + + func extractUserNumber(from input: String) -> String { + // Define the regular expression pattern to match the user_number part + let pattern = #"user_number:([^\s:]+)"# + + // Create a regular expression object + let regex = try? NSRegularExpression(pattern: pattern) + + // Search for the first match in the input string + if let match = regex?.firstMatch(in: input, range: NSRange(location: 0, length: input.utf16.count)) { + // Extract the matched part (user_number:+11230123) + if let range = Range(match.range(at: 1), in: input) { + return String(input[range]) + } + } + // Return the input if no match is found + return input } - + + func formatCustomParams(params: [String:Any]?)->String{ guard let customParameters = params else{return ""} do{ @@ -650,7 +690,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand public func cancelledCallInviteReceived(cancelledCallInvite: CancelledCallInvite, error: Error) { self.sendPhoneCallEvents(description: "Missed Call", isError: false) self.sendPhoneCallEvents(description: "LOG|cancelledCallInviteCanceled:", isError: false) - self.showMissedCallNotification(from: cancelledCallInvite.from, to: cancelledCallInvite.to) + //no need to notification for easify + // self.showMissedCallNotification(from: cancelledCallInvite.from, to: cancelledCallInvite.to) if (self.callInvite == nil) { self.sendPhoneCallEvents(description: "LOG|No pending call invite", isError: false) return @@ -700,16 +741,16 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand // MARK: TVOCallDelegate public func callDidStartRinging(call: Call) { let direction = (self.callOutgoing ? "Outgoing" : "Incoming") - let from = (call.from ?? self.identity) + let from = self.callOutgoing ? call.from ?? self.identity : extractUserNumber(from: (call.from ?? "")) let to = (call.to ?? self.callTo) - self.sendPhoneCallEvents(description: "Ringing|\(from)|\(to)|\(direction)", isError: false) + self.sendPhoneCallEvents(description: "Ringing|\(String(describing: from))|\(to)|\(direction)", isError: false) //self.placeCallButton.setTitle("Ringing", for: .normal) } public func callDidConnect(call: Call) { let direction = (self.callOutgoing ? "Outgoing" : "Incoming") - let from = (call.from ?? self.identity) + let from = extractUserNumber(from:(call.from ?? self.identity)) let to = (call.to ?? self.callTo) self.sendPhoneCallEvents(description: "Connected|\(from)|\(to)|\(direction)", isError: false) @@ -924,7 +965,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand let callUpdate = CXCallUpdate() callUpdate.remoteHandle = callHandle - callUpdate.localizedCallerName = self.clients[handle] ?? self.clients["defaultCaller"] ?? self.defaultCaller + callUpdate.localizedCallerName = self.outgoingCallerName callUpdate.supportsDTMF = false callUpdate.supportsHolding = true callUpdate.supportsGrouping = false @@ -940,7 +981,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand let callUpdate = CXCallUpdate() callUpdate.remoteHandle = callHandle - callUpdate.localizedCallerName = clients[from] ?? self.clients["defaultCaller"] ?? defaultCaller + // If the client is not registered, USE THE THE FROM NUMBER + callUpdate.localizedCallerName = formatUSPhoneNumber(from) callUpdate.supportsDTMF = true callUpdate.supportsHolding = true callUpdate.supportsGrouping = false @@ -956,6 +998,29 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand } } + // Format the phone number to US format if it is a valid US number else return the number as is + func formatUSPhoneNumber(_ number: String) -> String { + // Ensure the number starts with "+1" and has exactly 12 characters + guard number.hasPrefix("+1"), number.count == 12 else { + return number + } + + // Extract the digits after "+1" + let digits = number.suffix(10) + + // Check if all characters are digits + guard digits.allSatisfy({ $0.isNumber }) else { + return number + } + + // Format the number + let areaCode = digits.prefix(3) + let middle = digits.dropFirst(3).prefix(3) + let last = digits.suffix(4) + + return "+1 (\(areaCode)) \(middle)-\(last)" + } + func performEndCallAction(uuid: UUID) { self.sendPhoneCallEvents(description: "LOG|performEndCallAction method invoked", isError: false) @@ -966,7 +1031,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand self.sendPhoneCallEvents(description: "Call Ended", isError: false) return } - + let endCallAction = CXEndCallAction(call: uuid) let transaction = CXTransaction(action: endCallAction) @@ -1005,7 +1070,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand } self.sendPhoneCallEvents(description: "LOG|performAnswerVoiceCall: answering call", isError: false) let theCall = ci.accept(options: acceptOptions, delegate: self) - self.sendPhoneCallEvents(description: "Answer|\(theCall.from!)|\(theCall.to!)|Incoming\(formatCustomParams(params: ci.customParameters))", isError:false) + self.sendPhoneCallEvents(description: "Answer|\(String(describing: extractUserNumber(from: theCall.from!)))|\(theCall.to!)|Incoming\(formatCustomParams(params: ci.customParameters))", isError:false) self.call = theCall self.callKitCompletionCallback = completionHandler self.callInvite = nil diff --git a/lib/_internal/method_channel/twilio_call_method_channel.dart b/lib/_internal/method_channel/twilio_call_method_channel.dart index c1f1265d..e277c909 100644 --- a/lib/_internal/method_channel/twilio_call_method_channel.dart +++ b/lib/_internal/method_channel/twilio_call_method_channel.dart @@ -23,12 +23,18 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { /// /// [extraOptions] will be added to the callPayload sent to your server @override - Future place({required String from, required String to, Map? extraOptions}) { - _activeCall = ActiveCall(from: from, to: to, callDirection: CallDirection.outgoing); + Future place( + {required String from, + required String to, + required String callerName, + Map? extraOptions}) { + _activeCall = + ActiveCall(from: from, to: to, callDirection: CallDirection.outgoing); var options = extraOptions ?? {}; options['From'] = from; options['To'] = to; + options['CallerName'] = callerName; return _channel.invokeMethod('makeCall', options); } @@ -41,13 +47,15 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { /// Checks if there is an ongoing call @override Future isOnCall() { - return _channel.invokeMethod('isOnCall', {}).then((bool? value) => value ?? false); + return _channel.invokeMethod('isOnCall', + {}).then((bool? value) => value ?? false); } /// Gets the active call's SID. This will be null until the first Ringing event occurs @override Future getSid() { - return _channel.invokeMethod('call-sid', {}).then((String? value) => value); + return _channel.invokeMethod('call-sid', + {}).then((String? value) => value); } /// Answers incoming call @@ -61,7 +69,8 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { /// In future, native mobile will also respect the [holdCall] value. @override Future holdCall({bool holdCall = true}) { - return _channel.invokeMethod('holdCall', {"shouldHold": holdCall}); + return _channel + .invokeMethod('holdCall', {"shouldHold": holdCall}); } /// Query's active call holding state @@ -73,7 +82,8 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { /// Toggles mute state to provided value @override Future toggleMute(bool isMuted) { - return _channel.invokeMethod('toggleMute', {"muted": isMuted}); + return _channel + .invokeMethod('toggleMute', {"muted": isMuted}); } /// Query's mute status of call, true if call is muted @@ -85,7 +95,8 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { /// Toggles speaker state to provided value @override Future toggleSpeaker(bool speakerIsOn) { - return _channel.invokeMethod('toggleSpeaker', {"speakerIsOn": speakerIsOn}); + return _channel.invokeMethod( + 'toggleSpeaker', {"speakerIsOn": speakerIsOn}); } /// Switches Audio Device @@ -101,12 +112,14 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { @override Future sendDigits(String digits) { - return _channel.invokeMethod('sendDigits', {"digits": digits}); + return _channel + .invokeMethod('sendDigits', {"digits": digits}); } @override Future toggleBluetooth({bool bluetoothOn = true}) { - return _channel.invokeMethod('toggleBluetooth', {"bluetoothOn": bluetoothOn}); + return _channel.invokeMethod( + 'toggleBluetooth', {"bluetoothOn": bluetoothOn}); } @override @@ -122,4 +135,11 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { }; return _channel.invokeMethod('connect', options); } + + //getActiveCallOnResumeFromTerminatedState + @override + Future getActiveCallOnResumeFromTerminatedState() { + return _channel.invokeMethod( + 'getActiveCallOnResumeFromTerminatedState', {}); + } } diff --git a/lib/_internal/platform_interface/twilio_call_platform_interface.dart b/lib/_internal/platform_interface/twilio_call_platform_interface.dart index 63f94c05..537b3207 100644 --- a/lib/_internal/platform_interface/twilio_call_platform_interface.dart +++ b/lib/_internal/platform_interface/twilio_call_platform_interface.dart @@ -29,7 +29,11 @@ abstract class TwilioCallPlatform extends SharedPlatformInterface { /// Places new call /// /// [extraOptions] will be added to the callPayload sent to your server - Future place({required String from, required String to, Map? extraOptions}); + Future place( + {required String from, + required String to, + required String callerName, + Map? extraOptions}); /// Places new call using raw parameters passed directly to Twilio's REST API endpoint 'makeCall'. Returns true if successful, false otherwise. /// @@ -77,4 +81,7 @@ abstract class TwilioCallPlatform extends SharedPlatformInterface { /// Send digits to active call Future sendDigits(String digits); + + //getActiveCallOnResumeFromTerminatedState + Future getActiveCallOnResumeFromTerminatedState(); }