From 831f56da22732157e2bde4fbac08ed2f9cda07d5 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Wed, 17 Jul 2024 19:04:47 +0530 Subject: [PATCH 01/23] feat: Preserve device token for VoIP push notifications Preserve the device token for VoIP push notifications to avoid the need for users to restart the app to obtain a new device token. Removing the device token from UserDefaults caused registration failures. fix : IOS not displaying caller number if the user doesn't set a default caller name it will display the incoming caller number , if the already set a value user has to call [await TwilioVoice.instance.setDefaultCallerName('');] to make this work --- ios/Classes/SwiftTwilioVoicePlugin.swift | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 0a324c55..53a9279b 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -495,7 +495,10 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand self.sendPhoneCallEvents(description: "LOG|Successfully unregistered from VoIP push notifications.", isError: false) } } - UserDefaults.standard.removeObject(forKey: kCachedDeviceToken) + + //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) @@ -870,7 +873,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 = clients[from] ?? self.clients["defaultCaller"] ?? formatUSPhoneNumber(from) callUpdate.supportsDTMF = true callUpdate.supportsHolding = true callUpdate.supportsGrouping = false @@ -886,6 +890,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) From 257303be75b6ea39ab9e004e18a740bccbda8d21 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Wed, 17 Jul 2024 19:31:06 +0530 Subject: [PATCH 02/23] chore: Remove unnecessary missed call notification code --- ios/Classes/SwiftTwilioVoicePlugin.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 53a9279b..88c87805 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -583,7 +583,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 From d326ac4c1bebed4c3ef46c20adb37503ace9fb28 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Wed, 17 Jul 2024 20:02:03 +0530 Subject: [PATCH 03/23] feat: Update localized caller name handling Update the handling of the localized caller name in the `SwiftTwilioVoicePlugin.swift` file. Instead of using the `clients` dictionary to retrieve the caller name, always use the `formatUSPhoneNumber` function to format the caller's phone number. This ensures consistency and avoids potential issues with missing or incorrect caller names. Note: This commit message follows the established convention of starting with a verb in the imperative form, followed by a concise description of the changes made. --- ios/Classes/SwiftTwilioVoicePlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 88c87805..2161749a 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -875,7 +875,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand let callUpdate = CXCallUpdate() callUpdate.remoteHandle = callHandle // If the client is not registered, USE THE THE FROM NUMBER - callUpdate.localizedCallerName = clients[from] ?? self.clients["defaultCaller"] ?? formatUSPhoneNumber(from) + callUpdate.localizedCallerName = formatUSPhoneNumber(from) callUpdate.supportsDTMF = true callUpdate.supportsHolding = true callUpdate.supportsGrouping = false From b80355235e36f2828456f680dabf1a64bb6eabbf Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 12 Sep 2024 17:45:46 +0530 Subject: [PATCH 04/23] chore: Update TwilioVoice dependency versions --- android/build.gradle | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 96 ++++++++++++-------- ios/twilio_voice.podspec | 2 +- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 39922de5..2c321970 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -56,7 +56,7 @@ android { dependencies { implementation(platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version")) implementation(platform("com.google.firebase:firebase-bom:32.2.2")) - implementation("com.twilio:voice-android:6.3.2") + implementation("com.twilio:voice-android:6.6.4") implementation("com.google.firebase:firebase-messaging-ktx:23.2.1") implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.lifecycle:lifecycle-process:2.6.1' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index dbd89bb9..4577b8a3 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + 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 = ( ); @@ -209,41 +210,36 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 2C0550190AA281E4DABEDF6E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + 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; @@ -259,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 */ @@ -381,7 +399,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -514,7 +532,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -539,7 +557,7 @@ DEVELOPMENT_TEAM = 366269TEN6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/twilio_voice.podspec b/ios/twilio_voice.podspec index d6fe4541..b3c16be1 100644 --- a/ios/twilio_voice.podspec +++ b/ios/twilio_voice.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'TwilioVoice','~> 6.9.1' + s.dependency 'TwilioVoice','~> 6.11.3' s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. From 420649f3d0fcba0cfa7b2311af7a280d54aa361f Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 26 Sep 2024 16:26:59 +0530 Subject: [PATCH 05/23] voice name indication added --- .../twilio/twilio_voice/TwilioVoicePlugin.kt | 19 ++++- .../twilio_voice/call/TVParametersImpl.kt | 73 +++++++++++++------ .../fcm/VoiceFirebaseMessagingService.kt | 2 +- .../twilio_voice/service/TVConnection.kt | 46 +++++++++--- .../service/TVConnectionService.kt | 41 ++++++++++- ios/Classes/SwiftTwilioVoicePlugin.swift | 61 +++++++++++++--- 6 files changed, 188 insertions(+), 54 deletions(-) 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 426e2f63..3883163c 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -1530,6 +1530,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", @@ -1660,7 +1672,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) -> @@ -1727,7 +1739,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) -> @@ -1862,6 +1874,9 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } } + + + // override fun onConnectFailure(call: Call, callException: CallException) { // Log.e(TAG, "onConnectFailure: ${callException.message}") // logEvent("onConnectFailure", callException.message ?: "") 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 e5d1da13..07fb853a 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/fcm/VoiceFirebaseMessagingService.kt b/android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt index a9ee66b8..139afd53 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 @@ -151,7 +151,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 48ab4c87..6b3a49a0 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") @@ -173,12 +181,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 @@ -186,7 +206,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) }) @@ -504,4 +524,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 677618a4..f0c18d37 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 @@ -299,7 +299,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) } @@ -650,10 +650,45 @@ class TVConnectionService : ConnectionService() { 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 = extractClient(name) + val userNumber = extractUserNumber(name) + + if(connection.callDirection == CallDirection.OUTGOING){ + + connection.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, name, null), TelecomManager.PRESENTATION_ALLOWED) + connection.setCallerDisplayName(name, 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 + } + + 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 + } + + + private fun sendBroadcastEvent(ctx: Context, event: String, callSid: String?, extras: Bundle? = null) { Intent(ctx, TVBroadcastReceiver::class.java).apply { action = event diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 2161749a..29cd2ad9 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -559,14 +559,51 @@ 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 client:String = (extractClient(from: incomingCallerDetails) ?? "").replacingOccurrences(of: "_", with: " ") + let userNumber:String = extractUserNumber(from: incomingCallerDetails) ?? "" + + 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 from + } + + func extractClient(from input: String) -> String { + // Define the regular expression pattern to match the client part + let pattern = #"client:([^\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 (client:+11230(123)) + if let range = Range(match.range(at: 1), in: input) { + return String(input[range]) + } + } + return from + } func formatCustomParams(params: [String:Any]?)->String{ guard let customParameters = params else{return ""} do{ @@ -634,16 +671,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 = 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) @@ -956,7 +993,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!)\(formatCustomParams(params: ci.customParameters))", isError:false) + self.sendPhoneCallEvents(description: "Answer|\(String(describing: extractUserNumber(from: theCall.from!) ?? ""))|\(theCall.to!)\(formatCustomParams(params: ci.customParameters))", isError:false) self.call = theCall self.callKitCompletionCallback = completionHandler self.callInvite = nil From 520e2769d0fa40f4323b5bc99ac0d684e49ad336 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 26 Sep 2024 17:56:10 +0530 Subject: [PATCH 06/23] Fix issue with missing call handle in TVConnectionService --- .../com/twilio/twilio_voice/service/TVConnectionService.kt | 1 + 1 file changed, 1 insertion(+) 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 f0c18d37..4d2f9bb9 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 @@ -325,6 +325,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 } From 3e833954c1e7db3b1d690feea84db0b039819f96 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 26 Sep 2024 18:52:12 +0530 Subject: [PATCH 07/23] Refactor extractClient and extractCallHandle functions --- ios/Classes/SwiftTwilioVoicePlugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 29cd2ad9..91356f23 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -585,7 +585,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand return String(input[range]) } } - return from + return input } func extractClient(from input: String) -> String { @@ -602,7 +602,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand return String(input[range]) } } - return from + return input } func formatCustomParams(params: [String:Any]?)->String{ guard let customParameters = params else{return ""} From e0719373fdfa6f4cd269d2a0815d2759adfeeb30 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Fri, 27 Sep 2024 11:35:14 +0530 Subject: [PATCH 08/23] Refactor extractClient and extractCallHandle functions --- .../com/twilio/twilio_voice/service/TVConnectionService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d2f9bb9..206297a0 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 @@ -685,7 +685,7 @@ class TVConnectionService : ConnectionService() { val match = pattern.find(input) // Extract the matched part (client:+11230(123)) - return match?.groups?.get(1)?.value ?: input + return match?.groups?.get(1)?.value?.replace(oldValue = "_", newValue = " " ) ?: input } From 7e8a0203957e0e47d557f541d384ceb5a4890c8f Mon Sep 17 00:00:00 2001 From: bazl-E Date: Mon, 30 Sep 2024 17:34:09 +0530 Subject: [PATCH 09/23] Refactor extractClient and extractCallHandle functions --- ios/Classes/SwiftTwilioVoicePlugin.swift | 80 +++++++++++++----------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 91356f23..592a2bf3 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -560,8 +560,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand UserDefaults.standard.set(Date(), forKey: kCachedBindingDate) let incomingCallerDetails:String = callInvite.from ?? defaultCaller - let client:String = (extractClient(from: incomingCallerDetails) ?? "").replacingOccurrences(of: "_", with: " ") - let userNumber:String = extractUserNumber(from: incomingCallerDetails) ?? "" + let client:String = (extractClient(from: incomingCallerDetails) ).replacingOccurrences(of: "_", with: " ") + let userNumber:String = extractUserNumber(from: incomingCallerDetails) var from:String = callInvite.from ?? defaultCaller from = userNumber @@ -571,39 +571,42 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand 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 input - } - - func extractClient(from input: String) -> String { - // Define the regular expression pattern to match the client part - let pattern = #"client:([^\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 (client:+11230(123)) - if let range = Range(match.range(at: 1), in: input) { - return String(input[range]) - } - } - return input - } + 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 extractClient(from input: String) -> String { + // Define the regular expression pattern to match the client part + let pattern = #"client:([^\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 (client:+11230(123)) + 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{ @@ -671,7 +674,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand // MARK: TVOCallDelegate public func callDidStartRinging(call: Call) { let direction = (self.callOutgoing ? "Outgoing" : "Incoming") - let from = extractUserNumber(from: (call.from ?? "")) ?? "" + let from = call.from ?? self.identity +// extractUserNumber(from: (call.from ?? "")) let to = (call.to ?? self.callTo) self.sendPhoneCallEvents(description: "Ringing|\(String(describing: from))|\(to)|\(direction)", isError: false) @@ -680,7 +684,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand public func callDidConnect(call: Call) { let direction = (self.callOutgoing ? "Outgoing" : "Incoming") - let from = extractUserNumber(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) @@ -993,7 +997,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|\(String(describing: extractUserNumber(from: theCall.from!) ?? ""))|\(theCall.to!)\(formatCustomParams(params: ci.customParameters))", isError:false) + self.sendPhoneCallEvents(description: "Answer|\(String(describing: extractUserNumber(from: theCall.from!)))|\(theCall.to!)\(formatCustomParams(params: ci.customParameters))", isError:false) self.call = theCall self.callKitCompletionCallback = completionHandler self.callInvite = nil From ccb48b46923e0ab372cc126e5e800c2b7f169e94 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Tue, 1 Oct 2024 10:09:50 +0530 Subject: [PATCH 10/23] Refactor callDidStartRinging function to handle outgoing calls properly --- ios/Classes/SwiftTwilioVoicePlugin.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 592a2bf3..d11b6bc1 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -674,8 +674,7 @@ 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 -// extractUserNumber(from: (call.from ?? "")) + let from = self.callOutgoing ? call.from ?? self.identity : extractUserNumber(from: (call.from ?? "")) let to = (call.to ?? self.callTo) self.sendPhoneCallEvents(description: "Ringing|\(String(describing: from))|\(to)|\(direction)", isError: false) From 37a24e54b254e452737b05210ea59efb75c7fae3 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Tue, 1 Oct 2024 11:35:18 +0530 Subject: [PATCH 11/23] terminated sate call handle ios --- ios/Classes/SwiftTwilioVoicePlugin.swift | 13 +++++++ .../twilio_call_method_channel.dart | 36 ++++++++++++++----- .../twilio_call_platform_interface.dart | 8 ++++- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index d11b6bc1..b35a07d3 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -200,6 +200,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} diff --git a/lib/_internal/method_channel/twilio_call_method_channel.dart b/lib/_internal/method_channel/twilio_call_method_channel.dart index 3da1dc59..b4ccab7d 100644 --- a/lib/_internal/method_channel/twilio_call_method_channel.dart +++ b/lib/_internal/method_channel/twilio_call_method_channel.dart @@ -23,8 +23,12 @@ 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, + Map? extraOptions}) { + _activeCall = + ActiveCall(from: from, to: to, callDirection: CallDirection.outgoing); var options = extraOptions ?? {}; options['From'] = from; @@ -41,13 +45,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 +67,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 +80,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 +93,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 +110,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 @@ -119,4 +130,11 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { Future connect({Map? extraOptions}) { return Future.value(false); } + + //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 eaf45792..b9ade4f6 100644 --- a/lib/_internal/platform_interface/twilio_call_platform_interface.dart +++ b/lib/_internal/platform_interface/twilio_call_platform_interface.dart @@ -29,7 +29,10 @@ 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, + 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]; @@ -83,4 +86,7 @@ abstract class TwilioCallPlatform extends SharedPlatformInterface { /// Send digits to active call Future sendDigits(String digits); + + //getActiveCallOnResumeFromTerminatedState + Future getActiveCallOnResumeFromTerminatedState(); } From ba92a7a543bbd75cda713ecd53f9c63bdde7e9b4 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Tue, 1 Oct 2024 12:35:57 +0530 Subject: [PATCH 12/23] Refactor onReconnected and GetActiveCallOnResumeFromTerminatedState functions --- .../twilio/twilio_voice/TwilioVoicePlugin.kt | 19 +++++++++++++++++++ .../twilio_voice/types/TVMethodChannels.kt | 2 ++ 2 files changed, 21 insertions(+) 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 3883163c..2a33fcc0 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -208,6 +208,8 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH Log.d(TAG, "onReconnecting") } + + override fun onReconnected(call: Call) { Log.d(TAG, "onReconnected") } @@ -471,6 +473,21 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } } + TVMethodChannels.GetActiveCallOnResumeFromTerminatedState -> { + //is on call + val hasActiveCalls = isOnCall() + if(hasActiveCalls){ + val activeCalls = TVConnectionService.Companion.activeConnections + val call = activeCalls.values.firstOrNull() + val from = call?.twilioCall?.from ?: "" + val to = call?.twilioCall?.to ?: "" + val callDirection = call?.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) @@ -514,6 +531,8 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH result.success(isOnCall()) return + + // Disabled for now until a better solution for TelecomManager.isInCall() is found - this returns true for any ConnectionService including Cellular calls. // context?.let { ctx -> // telecomManager?.let { tm -> 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..91bce3d0 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,8 +47,10 @@ 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"); + companion object { private val map = TVMethodChannels.values().associateBy(TVMethodChannels::method) fun fromValue(method: String) = map[method] From 3603a9b564b90229784697faa08b137abbecf4fa Mon Sep 17 00:00:00 2001 From: bazl-E Date: Tue, 1 Oct 2024 12:58:25 +0530 Subject: [PATCH 13/23] Refactor logEvents function to only log connected calls --- .../com/twilio/twilio_voice/TwilioVoicePlugin.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 2a33fcc0..a532437c 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -478,11 +478,14 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH val hasActiveCalls = isOnCall() if(hasActiveCalls){ val activeCalls = TVConnectionService.Companion.activeConnections - val call = activeCalls.values.firstOrNull() - val from = call?.twilioCall?.from ?: "" - val to = call?.twilioCall?.to ?: "" - val callDirection = call?.callDirection ?: CallDirection.INCOMING - logEvents("", arrayOf("Connected", from, to, callDirection.label )) + val currentCall = activeCalls.values.firstOrNull() + val isAnsweredCall = currentCall?.twilioCall?.state == Call.State.CONNECTED + if(isAnsweredCall){ + val from = currentCall?.twilioCall?.from ?: "" + val to = currentCall?.twilioCall?.to ?: "" + val callDirection = currentCall?.callDirection ?: CallDirection.INCOMING + logEvents("", arrayOf("Connected", from, to, callDirection.label )) + } } result.success(true) From c0f300a28fa142822dba243dc02eb38fce7a11fa Mon Sep 17 00:00:00 2001 From: bazl-E Date: Fri, 8 Nov 2024 13:57:04 +0530 Subject: [PATCH 14/23] Add callerName parameter to place method and update call handling --- ios/Classes/SwiftTwilioVoicePlugin.swift | 7 +- .../twilio_call_method_channel.dart | 2 + .../twilio_call_platform_interface.dart | 1 + lib/_internal/twilio_voice_web.dart | 78 ++++++++++++------- 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index b35a07d3..03d2138f 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -43,6 +43,7 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand var callKitCallController: CXCallController var userInitiatedDisconnect: Bool = false var callOutgoing: Bool = false + var outgoingCallerName = "" static var appName: String { get { @@ -122,6 +123,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{ @@ -893,6 +896,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand } else { action.fail() } + + } // MARK: Call Kit Actions @@ -911,7 +916,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 diff --git a/lib/_internal/method_channel/twilio_call_method_channel.dart b/lib/_internal/method_channel/twilio_call_method_channel.dart index b4ccab7d..b518819b 100644 --- a/lib/_internal/method_channel/twilio_call_method_channel.dart +++ b/lib/_internal/method_channel/twilio_call_method_channel.dart @@ -26,6 +26,7 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { Future place( {required String from, required String to, + required String callerName, Map? extraOptions}) { _activeCall = ActiveCall(from: from, to: to, callDirection: CallDirection.outgoing); @@ -33,6 +34,7 @@ class MethodChannelTwilioCall extends TwilioCallPlatform { var options = extraOptions ?? {}; options['From'] = from; options['To'] = to; + options['CallerName'] = callerName; return _channel.invokeMethod('makeCall', options); } diff --git a/lib/_internal/platform_interface/twilio_call_platform_interface.dart b/lib/_internal/platform_interface/twilio_call_platform_interface.dart index b9ade4f6..d3e028ec 100644 --- a/lib/_internal/platform_interface/twilio_call_platform_interface.dart +++ b/lib/_internal/platform_interface/twilio_call_platform_interface.dart @@ -32,6 +32,7 @@ abstract class TwilioCallPlatform extends SharedPlatformInterface { Future place( {required String from, required String to, + required String callerName, Map? extraOptions}); /// Place outgoing call with raw parameters. Returns true if successful, false otherwise. diff --git a/lib/_internal/twilio_voice_web.dart b/lib/_internal/twilio_voice_web.dart index 3c9e4c82..5903adcd 100644 --- a/lib/_internal/twilio_voice_web.dart +++ b/lib/_internal/twilio_voice_web.dart @@ -5,15 +5,13 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - // Added as temporary measure till sky_engine includes js_util (allowInterop()) import 'package:js/js.dart' as js; -import 'package:js/js_util.dart' as js_util; -import 'package:twilio_voice/_internal/js/call/call_status.dart'; - // This is required for JS interop, do not remove even though linter complains // ignore: unused_import import 'package:js/js_util.dart'; +import 'package:js/js_util.dart' as js_util; +import 'package:twilio_voice/_internal/js/call/call_status.dart'; import 'package:twilio_voice/_internal/platform_interface/twilio_voice_platform_interface.dart'; import '../twilio_voice.dart'; @@ -50,7 +48,8 @@ class TwilioSW { void _setupServiceWorker() { _webServiceWorkerContainerDelegate = html.window.navigator.serviceWorker; if (_webServiceWorkerContainerDelegate == null) { - printDebug("No service worker found, check if you've registered the `twilio-sw.js` service worker and if the script is present."); + printDebug( + "No service worker found, check if you've registered the `twilio-sw.js` service worker and if the script is present."); return; } @@ -59,7 +58,8 @@ class TwilioSW { _webServiceWorkerDelegate = _webServiceWorkerContainerDelegate?.controller; if (_webServiceWorkerDelegate == null) { - printDebug("No service worker registered and/or controlling the page. Try (soft) refreshing?"); + printDebug( + "No service worker registered and/or controlling the page. Try (soft) refreshing?"); return; } } @@ -70,7 +70,8 @@ class TwilioSW { // already registered, we don't have to register again return; } - _webServiceWorkerMessageSubscription = _webServiceWorkerContainerDelegate!.onMessage.listen((event) { + _webServiceWorkerMessageSubscription = + _webServiceWorkerContainerDelegate!.onMessage.listen((event) { _messageReceived?.call(event.data); }); } @@ -110,9 +111,9 @@ class NotificationService { List>? actions, }) async { // request background permissions - if(!await hasPermission()) { + if (!await hasPermission()) { bool result = await requestPermission(); - if(!result) { + if (!result) { printDebug("Cannot show notification with permission."); return; } @@ -180,8 +181,10 @@ class Logger { /// The event will be sent as a String with the following format: /// - (if prefix is not empty): "prefix|description", where '|' is separator /// - (if prefix is empty): "description" - static void logLocalEventEntries(List entries, {String prefix = "LOG", String separator = "|"}) { - logLocalEvent(entries.join(separator), prefix: prefix, separator: separator); + static void logLocalEventEntries(List entries, + {String prefix = "LOG", String separator = "|"}) { + logLocalEvent(entries.join(separator), + prefix: prefix, separator: separator); } /// Logs event to EventChannel. @@ -189,9 +192,11 @@ class Logger { /// The event will be sent as a String with the following format: /// - (if prefix is not empty): "prefix|description", where '|' is separator /// - (if prefix is empty): "description" - static void logLocalEvent(String description, {String prefix = "LOG", String separator = "|"}) async { + static void logLocalEvent(String description, + {String prefix = "LOG", String separator = "|"}) async { if (!kIsWeb) { - throw UnimplementedError("Use eventChannel() via sendPhoneEvents on platform implementation"); + throw UnimplementedError( + "Use eventChannel() via sendPhoneEvents on platform implementation"); } // eventChannel.binaryMessenger.handlePlatformMessage( // _kEventChannelName, @@ -228,7 +233,8 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { html.Navigator get _webNavigatorDelegate => html.window.navigator; - html.Permissions? get _webPermissionsDelegate => _webNavigatorDelegate.permissions; + html.Permissions? get _webPermissionsDelegate => + _webNavigatorDelegate.permissions; late final Call _call = Call(); @@ -326,7 +332,8 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { /// This dirty hack to get media stream. Request (to show permissions popup on Chrome and other browsers, then stop the stream to release the permission) /// TODO(cybex-dev) - check supported media streams - html.MediaStream mediaStream = await _webNavigatorDelegate.getUserMedia(audio: true); + html.MediaStream mediaStream = + await _webNavigatorDelegate.getUserMedia(audio: true); mediaStream.getTracks().forEach((track) => track.stop()); return hasMicAccess(); } catch (e) { @@ -422,7 +429,8 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { /// See [twilio_js.Device.new] /// Note: [deviceToken] is ignored for web @override - Future setTokens({required String accessToken, String? deviceToken}) async { + Future setTokens( + {required String accessToken, String? deviceToken}) async { // TODO use updateOptions for Twilio device assert(accessToken.isNotEmpty, "Access token cannot be empty"); // assert(deviceToken != null && deviceToken.isNotEmpty, "Device token cannot be null or empty"); @@ -542,7 +550,9 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { final from = params["From"] ?? ""; if (from.startsWith("client:")) { final clientName = from.substring(7); - return _localStorage.getRegisteredClient(clientName) ?? _localStorage.getRegisteredClient("defaultCaller") ?? clientName; + return _localStorage.getRegisteredClient(clientName) ?? + _localStorage.getRegisteredClient("defaultCaller") ?? + clientName; } else { return from; } @@ -641,7 +651,8 @@ class Call extends MethodChannelTwilioCall { /// Not currently implemented for web @override Future toggleSpeaker(bool speakerIsOn) async { - Logger.logLocalEvent(speakerIsOn ? "Speaker On" : "Speaker Off", prefix: ""); + Logger.logLocalEvent(speakerIsOn ? "Speaker On" : "Speaker Off", + prefix: ""); return Future.value(false); } @@ -749,7 +760,8 @@ class Call extends MethodChannelTwilioCall { CallStatus callStatus = getCallStatus(_jsCall!); // reject incoming call that is both outbound ringing or inbound pending - if (callStatus == CallStatus.ringing || callStatus == CallStatus.pending) { + if (callStatus == CallStatus.ringing || + callStatus == CallStatus.pending) { _jsCall!.reject(); } else { _jsCall!.disconnect(); @@ -774,13 +786,20 @@ class Call extends MethodChannelTwilioCall { /// /// See [twilio_js.Device.connect] @override - Future place({required String from, required String to, Map? extraOptions}) async { + Future place({ + required String from, + required String to, + required String callerName, + Map? extraOptions, + }) async { assert(device != null, "Twilio device is null, make sure you have initialized the device first by calling [ setTokens({required String accessToken, String? deviceToken}) ] "); assert(from.isNotEmpty, "From cannot be empty"); assert(to.isNotEmpty, "To cannot be empty"); - assert(extraOptions?.keys.contains("From") ?? true, "From cannot be passed in extraOptions"); - assert(extraOptions?.keys.contains("To") ?? true, "To cannot be passed in extraOptions"); + assert(extraOptions?.keys.contains("From") ?? true, + "From cannot be passed in extraOptions"); + assert(extraOptions?.keys.contains("To") ?? true, + "To cannot be passed in extraOptions"); Logger.logLocalEvent("Making new call"); // handle parameters @@ -911,7 +930,8 @@ class Call extends MethodChannelTwilioCall { final params = getCallParams(_jsCall!); final from = params["From"] ?? ""; final to = params["To"] ?? ""; - final direction = _jsCall!.direction == "INCOMING" ? "Incoming" : "Outgoing"; + final direction = + _jsCall!.direction == "INCOMING" ? "Incoming" : "Outgoing"; Logger.logLocalEventEntries( ["Ringing", from, to, direction], prefix: "", @@ -965,8 +985,6 @@ class Call extends MethodChannelTwilioCall { Logger.logLocalEvent("Call Ended", prefix: ""); } - - Future _showMissedCallNotification(twilio_js.Call call) async { const action = 'missed'; final callParams = getCallParams(call); @@ -1050,7 +1068,8 @@ Map _getCustomCallParameters(dynamic callParameters) { final list = toArray(callParameters) as List; final entries = list.map((e) { final entry = e as List; - return MapEntry(entry.first.toString(), entry.last.toString()); + return MapEntry( + entry.first.toString(), entry.last.toString()); }); return Map.fromEntries(entries); } @@ -1063,7 +1082,8 @@ Map getCallParams(twilio_js.Call call) { return Map.from(customParams)..addAll(params); } -ActiveCall activeCallFromNativeJsCall(twilio_js.Call call, {DateTime? initiated}) { +ActiveCall activeCallFromNativeJsCall(twilio_js.Call call, + {DateTime? initiated}) { final params = getCallParams(call); final from = params["From"] ?? params["from"] ?? ""; final to = params["To"] ?? params["to"] ?? ""; @@ -1080,7 +1100,9 @@ ActiveCall activeCallFromNativeJsCall(twilio_js.Call call, {DateTime? initiated} // call.customParameters["To"] ?? "", customParams: params, //call.customParameters as Map?, - callDirection: direction == "INCOMING" ? CallDirection.incoming : CallDirection.outgoing, + callDirection: direction == "INCOMING" + ? CallDirection.incoming + : CallDirection.outgoing, initiated: date, ); } From 333cc2328a1033c50e39d9552c13986b791cf77e Mon Sep 17 00:00:00 2001 From: bazl-E Date: Fri, 8 Nov 2024 17:37:27 +0530 Subject: [PATCH 15/23] Add callerName parameter to placeCall method and update related handling --- .../twilio/twilio_voice/TwilioVoicePlugin.kt | 17 ++++++++++++-- .../twilio_voice/constants/Constants.kt | 1 + .../service/TVConnectionService.kt | 22 +++++++++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) 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 a532437c..c11b807b 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -639,11 +639,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") @@ -1064,7 +1075,8 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH accessToken: String, from: String, to: String, - params: Map + params: Map, + callerName: String, ): Boolean { assert(accessToken.isNotEmpty()) { "Twilio Access Token cannot be empty" } assert(to.isNotEmpty()) { "To cannot be empty" } @@ -1107,6 +1119,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH putExtra(TVConnectionService.EXTRA_TOKEN, accessToken) 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) 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/service/TVConnectionService.kt b/android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnectionService.kt index 206297a0..a9264de4 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 @@ -143,6 +143,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. */ @@ -349,6 +351,11 @@ class TVConnectionService : ConnectionService() { return@let } + val outgoingName = it.getStringExtra(EXTRA_CALLER_NAME) ?: 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) @@ -362,6 +369,8 @@ class TVConnectionService : ConnectionService() { params[EXTRA_FROM] = from params[EXTRA_TO] = to params[EXTRA_TOKEN] = token + params[EXTRA_CALLER_NAME] = outgoingName + // Create Twilio Param bundles val myBundle = Bundle().apply { @@ -514,7 +523,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() @@ -548,6 +557,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 -> @@ -589,7 +603,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 } @@ -646,7 +660,7 @@ 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) } @@ -658,7 +672,7 @@ class TVConnectionService : ConnectionService() { if(connection.callDirection == CallDirection.OUTGOING){ connection.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, name, null), TelecomManager.PRESENTATION_ALLOWED) - connection.setCallerDisplayName(name, 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) From 3fd105087dbf14b3c206d193e7d97907d94bcc50 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Mon, 11 Nov 2024 18:21:36 +0530 Subject: [PATCH 16/23] Update client name extraction to use custom parameters and remove unused method --- .../service/TVConnectionService.kt | 22 +++++------ ios/Classes/SwiftTwilioVoicePlugin.swift | 37 +++++++++---------- 2 files changed, 29 insertions(+), 30 deletions(-) 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 a9264de4..5b9d6c37 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 @@ -666,7 +666,7 @@ class TVConnectionService : ConnectionService() { } val name = if(connection.callDirection == CallDirection.OUTGOING) params.to else params.from - val userName = extractClient(name) + val userName = params.customParameters["client_name"] val userNumber = extractUserNumber(name) if(connection.callDirection == CallDirection.OUTGOING){ @@ -691,16 +691,16 @@ class TVConnectionService : ConnectionService() { 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?.replace(oldValue = "_", newValue = " " ) ?: 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?.replace(oldValue = "_", newValue = " " ) ?: input +// } diff --git a/ios/Classes/SwiftTwilioVoicePlugin.swift b/ios/Classes/SwiftTwilioVoicePlugin.swift index 03d2138f..429ba65d 100644 --- a/ios/Classes/SwiftTwilioVoicePlugin.swift +++ b/ios/Classes/SwiftTwilioVoicePlugin.swift @@ -576,9 +576,8 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand UserDefaults.standard.set(Date(), forKey: kCachedBindingDate) let incomingCallerDetails:String = callInvite.from ?? defaultCaller - let client:String = (extractClient(from: incomingCallerDetails) ).replacingOccurrences(of: "_", with: " ") let userNumber:String = extractUserNumber(from: incomingCallerDetails) - + let client:String = callInvite.customParameters?["client_name"] ?? userNumber var from:String = callInvite.from ?? defaultCaller from = userNumber @@ -605,23 +604,23 @@ public class SwiftTwilioVoicePlugin: NSObject, FlutterPlugin, FlutterStreamHand return input } - func extractClient(from input: String) -> String { - // Define the regular expression pattern to match the client part - let pattern = #"client:([^\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 (client:+11230(123)) - 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 extractClient(from input: String) -> String { +// // Define the regular expression pattern to match the client part +// let pattern = #"client:([^\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 (client:+11230(123)) +// 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 ""} From 5c81c1e71f7012da84b719e2deba42808d7c5482 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 14 Nov 2024 10:17:06 +0530 Subject: [PATCH 17/23] Refactor main.dart for improved readability and add callerName parameter to place method --- example/lib/main.dart | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index fb0e1542..d4fa7819 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; @@ -279,7 +291,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(); } } @@ -319,7 +332,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 { @@ -398,7 +415,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; @@ -435,7 +453,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) { From d04fc94a4df66b0dc08768928d38c27db550c663 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Wed, 20 Nov 2024 11:52:42 +0530 Subject: [PATCH 18/23] Extract user number from current call in TwilioVoicePlugin for improved clarity --- .../main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c11b807b..4d543e99 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -481,7 +481,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH val currentCall = activeCalls.values.firstOrNull() val isAnsweredCall = currentCall?.twilioCall?.state == Call.State.CONNECTED if(isAnsweredCall){ - val from = currentCall?.twilioCall?.from ?: "" + val from = extractUserNumber(currentCall?.twilioCall?.from ?: "") val to = currentCall?.twilioCall?.to ?: "" val callDirection = currentCall?.callDirection ?: CallDirection.INCOMING logEvents("", arrayOf("Connected", from, to, callDirection.label )) From c1e1a97b343ebd808010f301fd7e77f7bc6b2209 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 21 Nov 2024 12:41:50 +0530 Subject: [PATCH 19/23] Refactor TwilioVoicePlugin to improve user number extraction clarity --- .../main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d543e99..8b8b667a 100644 --- a/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt +++ b/android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt @@ -481,7 +481,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH val currentCall = activeCalls.values.firstOrNull() val isAnsweredCall = currentCall?.twilioCall?.state == Call.State.CONNECTED if(isAnsweredCall){ - val from = extractUserNumber(currentCall?.twilioCall?.from ?: "") + val from = extractUserNumber(currentCall?.twilioCall?.from ?: "") val to = currentCall?.twilioCall?.to ?: "" val callDirection = currentCall?.callDirection ?: CallDirection.INCOMING logEvents("", arrayOf("Connected", from, to, callDirection.label )) From dda22adb62b1e1dd2387cdd29aee2fd765df478f Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 9 Jan 2025 16:28:03 +0530 Subject: [PATCH 20/23] call_error_case_handled --- .../twilio/twilio_voice/TwilioVoicePlugin.kt | 32 +++++++++++++++++-- .../service/TVConnectionService.kt | 11 +++++++ 2 files changed, 40 insertions(+), 3 deletions(-) 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 8b8b667a..d76c2b3a 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) { @@ -1653,6 +1656,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( @@ -1685,11 +1689,12 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVBroadcastReceiver.ACTION_ACTIVE_CALL_CHANGED -> { + println("Event called Basil : TVBroadcastReceiver.ACTION_ACTIVE_CALL_CHANGED") Log.d(TAG, "handleBroadcastIntent: Active call changed to $callSid") } TVBroadcastReceiver.ACTION_INCOMING_CALL -> { - // TODO + println("Event called Basil : TVBroadcastReceiver.ACTION_INCOMING_CALL") val callHandle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: run { Log.e( @@ -1720,6 +1725,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( @@ -1734,6 +1740,8 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVBroadcastReceiver.ACTION_CALL_STATE -> { + println("Event called Basil : TVBroadcastReceiver.ACTION_CALL_STATE") + // isMuted = intent.getBooleanExtra(TVBroadcastReceiver.EXTRA_MUTE_STATE, isMuted).also { // Log.d(TAG, "handleBroadcastIntent: Call muted $it") // } @@ -1744,6 +1752,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVBroadcastReceiver.ACTION_INCOMING_CALL_IGNORED -> { + println("Event called Basil : TVBroadcastReceiver.ACTION_INCOMING_CALL_IGNORED") val reason = intent.getStringArrayExtra(TVBroadcastReceiver.EXTRA_INCOMING_CALL_IGNORED_REASON) ?: arrayOf() val handle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: "N/A" Log.w( @@ -1756,7 +1765,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVNativeCallActions.ACTION_ANSWERED -> { - // TODO + println("Event called Basil : TVNativeCallActions.ACTION_ANSWERED") val callHandle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: run { Log.e( @@ -1786,6 +1795,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVNativeCallActions.ACTION_DTMF -> { + println("Event called Basil : TVNativeCallActions.ACTION_DTMF") val dtmf = intent.getStringExtra(TVNativeCallActions.EXTRA_DTMF_TONE) ?: run { Log.e( TAG, @@ -1800,29 +1810,35 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVNativeCallActions.ACTION_REJECTED -> { + println("Event called Basil : TVNativeCallActions.ACTION_REJECTED") logEvent("Call Rejected") } TVNativeCallActions.ACTION_ABORT -> { + println("Event called Basil : TVNativeCallActions.ACTION_ABORT") Log.d(TAG, "handleBroadcastIntent: Abort") logEvent("", "Call Ended") } TVNativeCallActions.ACTION_HOLD -> { + println("Event called Basil : TVNativeCallActions.ACTION_HOLD") Log.d(TAG, "handleBroadcastIntent: Hold") logEvent("", "Hold") } TVNativeCallActions.ACTION_UNHOLD -> { + println("Event called Basil :TVNativeCallActions.ACTION_UNHOLD") Log.d(TAG, "handleBroadcastIntent: Unhold") logEvent("", "Unhold") } TVNativeCallEvents.EVENT_CONNECTING -> { + println("Event called Basil : TVNativeCallEvents.EVENT_CONNECTING") Log.d(TAG, "handleBroadcastIntent: Connecting") } TVNativeCallEvents.EVENT_RINGING -> { + println("Event called Basil : TVNativeCallEvents.EVENT_RINGING") val callHandle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: run { Log.e(TAG, "No 'EXTRA_CALL_INVITE' provided or invalid type") @@ -1844,7 +1860,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVNativeCallEvents.EVENT_CONNECTED -> { - // TODO + println("Event called Basil : TVNativeCallEvents.EVENT_CONNECTED") val callHandle = intent.getStringExtra(TVBroadcastReceiver.EXTRA_CALL_HANDLE) ?: run { Log.e( @@ -1874,36 +1890,46 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH } TVNativeCallEvents.EVENT_CONNECT_FAILURE -> { + println("Event called Basil : TVNativeCallEvents.EVENT_CONNECT_FAILURE") val code = intent.getIntExtra(CallExceptionExtension.EXTRA_CODE, -1) val message = intent.getStringExtra(CallExceptionExtension.EXTRA_MESSAGE) ?: run { Log.e(TAG, "No 'EXTRA_MESSAGE' provided or invalid type") return } logEvent("Call Error: ${code}, $message"); + logEvent("", "Call Ended") + TVConnectionService.clearActiveConnections() + } TVNativeCallEvents.EVENT_RECONNECTING -> { + println("Event called Basil : TVNativeCallEvents.EVENT_RECONNECTING") logEvent("", "Reconnecting"); } TVNativeCallEvents.EVENT_RECONNECTED -> { + println("Event called Basil : TVNativeCallEvents.EVENT_RECONNECTED") logEvent("", "Reconnected"); } TVNativeCallEvents.EVENT_DISCONNECTED_LOCAL -> { + println("Event called Basil : TVNativeCallEvents.EVENT_DISCONNECTED_LOCAL") logEvent("", "Call Ended") } TVNativeCallEvents.EVENT_DISCONNECTED_REMOTE -> { + println("Event called Basil : TVNativeCallEvents.EVENT_DISCONNECTED_REMOTE") logEvent("", "Call Ended") } TVNativeCallEvents.EVENT_MISSED -> { + println("Event called Basil : TVNativeCallEvents.EVENT_MISSED") logEvent("", "Missed Call") logEvent("", "Call Ended") } else -> { + println("Event called Basil : Received unknown action ${intent.action}") Log.e(TAG, "[VoiceBroadcastReceiver] Received unknown action ${intent.action}") } } 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 5b9d6c37..0578663f 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 @@ -180,6 +180,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. @@ -725,6 +733,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() } From a2a96b656589e6d42ab64ace03cf7c7b0ecbad9e Mon Sep 17 00:00:00 2001 From: bazl-E Date: Thu, 20 Mar 2025 00:23:43 +0530 Subject: [PATCH 21/23] Add namespace to Android build.gradle for Twilio Voice integration --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index 2c321970..8fa3872c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.android' android { + namespace = "com.twilio.twilio_voice" compileSdk 34 compileOptions { From ec3e629bbfdd067fdf5fa5afaf5d0c73faafa594 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Fri, 20 Jun 2025 13:23:13 +0530 Subject: [PATCH 22/23] feat(lldb): add LLDB helper and initialization script for debugging integration --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 +++++++++++++++++++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ 2 files changed, 37 insertions(+) create mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit 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 From 9954cb4238b89f73f19da0c14583331840fcc7c1 Mon Sep 17 00:00:00 2001 From: bazl-E Date: Fri, 20 Jun 2025 15:54:33 +0530 Subject: [PATCH 23/23] fix: correct plugin registration for Firebase functions and analytics --- example/macos/Flutter/GeneratedPluginRegistrant.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"))