diff --git a/.idea/misc.xml b/.idea/misc.xml index f4f6d798..f1fbd79c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,4 +6,7 @@ + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8fad4a..4648f013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.1 + +* Fixed some bugs. +* `Android` using Telecom Framework +* Add `silenceEvents` +* Add `normalHandle` props https://github.com/hiennguyen92/flutter_callkit_incoming/pull/403 +* Android add `textColor` props https://github.com/hiennguyen92/flutter_callkit_incoming/pull/398 +* Android invisible avatar for default https://github.com/hiennguyen92/flutter_callkit_incoming/pull/393 +* Add Method for call API when accept/decline/end/timeout + ## 2.0.0+2 * Fixed some bugs. diff --git a/README.md b/README.md index 07fab3b0..a2679a3e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca ``` ... - @@ -65,7 +65,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca * Import ```console import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; - ``` + ``` * Received an incoming call ```dart this._currentUuid = _uuid.v4(); @@ -94,6 +94,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca backgroundColor: '#0955fa', backgroundUrl: 'https://i.pravatar.cc/500', actionColor: '#4CAF50', + textColor: '#ffffff', incomingCallNotificationChannelName: "Incoming Call", missedCallNotificationChannelName: "Missed Call" ), @@ -194,7 +195,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca //Example d6a77ca80c5f09f87f353cdd328ec8d7d34e92eb108d046c91906f27f54949cd - + ``` Make sure using `SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)` inside AppDelegate.swift (Example) ```swift @@ -204,7 +205,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca //Save deviceToken to your server SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken) } - + func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { print("didInvalidatePushTokenFor") SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("") @@ -263,7 +264,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca } }); ``` - * Call from Native (iOS/Android) + * Call from Native (iOS/Android) ```swift //Swift iOS @@ -274,12 +275,12 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca info["type"] = 1 //... set more data SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(flutter_callkit_incoming.Data(args: info), fromPushKit: true) - + //please make sure call `completion()` at the end of the pushRegistry(......, completion: @escaping () -> Void) // or `DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { completion() }` // if you don't call completion() in pushRegistry(......, completion: @escaping () -> Void), there may be app crash by system when receiving voIP ``` - + ```kotlin //Kotlin/Java Android FlutterCallkitIncomingPlugin.getInstance().showIncomingNotification(...) @@ -295,7 +296,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca //... set more data SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) ``` - +
```objc @@ -312,7 +313,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca //... set more data [SwiftFlutterCallkitIncomingPlugin.sharedInstance showCallkitIncoming:data fromPushKit:YES]; ``` - +
```swift @@ -325,6 +326,75 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca //Kotlin/Java Android FlutterCallkitIncomingPlugin.getInstance().sendEventCustom(body: Map) ``` + * 3.1 Call API when accept/decline/end/timeout + ```swift + //Appdelegate + ... + @UIApplicationMain + @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate { + ... + + // Func Call api for Accept + func onAccept(_ call: Call) { + let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any] + print("LOG: onAccept") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + // Func Call API for Decline + func onDecline(_ call: Call) { + let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any] + print("LOG: onDecline") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + func onEnd(_ call: Call) { + let json = ["action": "END", "data": call.data.toJSON()] as [String: Any] + print("LOG: onEnd") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + func onTimeOut(_ call: Call) { + let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any] + print("LOG: onTimeOut") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + ... + + ``` + Please check full: Example 4. Properties @@ -366,11 +436,12 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca | **`backgroundColor`** | Incoming call screen background color. | `#0955fa` | | **`backgroundUrl`** | Using image background for Incoming call screen. example: http://... https://... or "assets/abc.png" | _None_ | | **`actionColor`** | Color used in button/text on notification. | `#4CAF50` | + | **`textColor`** | Color used for the text in full screen notification. | `#ffffff` | | **`incomingCallNotificationChannelName`** | Notification channel name of incoming call. | `Incoming call` | | **`missedCallNotificationChannelName`** | Notification channel name of missed call. | `Missed call` |
- + * iOS | Prop | Description | Default | @@ -415,7 +486,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca ## :bulb: Demo -1. Demo Illustration: +1. Demo Illustration: 2. Image diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index dabdfcb3..fc2f0a8c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + @@ -48,5 +49,15 @@ android:exported="true" android:name="com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerService"/> + + + + + + diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt index 7c6989d3..ce217788 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt @@ -50,6 +50,8 @@ data class Data(val args: Map) { var backgroundColor: String @JsonProperty("backgroundUrl") var backgroundUrl: String + @JsonProperty("textColor") + var textColor: String @JsonProperty("actionColor") var actionColor: String @JsonProperty("incomingCallNotificationChannelName") @@ -71,6 +73,13 @@ data class Data(val args: Map) { @JsonProperty("isAccepted") var isAccepted: Boolean = false + @JsonProperty("isOnHold") + var isOnHold: Boolean = (args["isOnHold"] as? Boolean) ?: false + @JsonProperty("audioRoute") + var audioRoute: Int = (args["audioRoute"] as? Int) ?: 1 + @JsonProperty("isMuted") + var isMuted: Boolean = (args["isMuted"] as? Boolean) ?: false + init { var android: Map? = args["android"] as? HashMap? android = android ?: args @@ -81,6 +90,7 @@ data class Data(val args: Map) { backgroundColor = android["backgroundColor"] as? String ?: "#0955fa" backgroundUrl = android["backgroundUrl"] as? String ?: "" actionColor = android["actionColor"] as? String ?: "#4CAF50" + textColor = android["textColor"] as? String ?: "#ffffff" incomingCallNotificationChannelName = android["incomingCallNotificationChannelName"] as? String missedCallNotificationChannelName = android["missedCallNotificationChannelName"] as? String @@ -178,6 +188,7 @@ data class Data(val args: Map) { CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, backgroundUrl ) + bundle.putString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, textColor) bundle.putString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, actionColor) bundle.putString(CallkitConstants.EXTRA_CALLKIT_ACTION_FROM, from) bundle.putString( @@ -256,6 +267,10 @@ data class Data(val args: Map) { CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, "#4CAF50" ) + data.textColor = bundle.getString( + CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, + "#FFFFFF" + ) data.from = bundle.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_FROM, "") diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt index 2b4a61c1..9f50545d 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt @@ -20,6 +20,9 @@ object CallkitConstants { "com.hiennv.flutter_callkit_incoming.ACTION_CALL_CALLBACK" const val ACTION_CALL_CUSTOM = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_CUSTOM" + const val ACTION_CALL_AUDIO_STATE_CHANGE = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_AUDIO_STATE_CHANGE" + const val ACTION_CALL_HELD = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_HELD" + const val ACTION_CALL_UNHELD = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_UNHELD" const val EXTRA_CALLKIT_INCOMING_DATA = "EXTRA_CALLKIT_INCOMING_DATA" @@ -52,10 +55,11 @@ object CallkitConstants { const val EXTRA_CALLKIT_BACKGROUND_COLOR = "EXTRA_CALLKIT_BACKGROUND_COLOR" const val EXTRA_CALLKIT_BACKGROUND_URL = "EXTRA_CALLKIT_BACKGROUND_URL" const val EXTRA_CALLKIT_ACTION_COLOR = "EXTRA_CALLKIT_ACTION_COLOR" + const val EXTRA_CALLKIT_TEXT_COLOR = "EXTRA_CALLKIT_TEXT_COLOR" const val EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME = "EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME" const val EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME = "EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME" const val EXTRA_CALLKIT_ACTION_FROM = "EXTRA_CALLKIT_ACTION_FROM" -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt index 58916391..d21e949f 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt @@ -166,9 +166,16 @@ class CallkitIncomingActivity : Activity() { val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) if (data == null) finish() + val textColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, "#ffffff") tvNameCaller.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") tvNumber.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "") + try { + tvNameCaller.setTextColor(Color.parseColor(textColor)) + tvNumber.setTextColor(Color.parseColor(textColor)) + } catch (error: Exception) { + } + val isShowLogo = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_LOGO, false) ivLogo.visibility = if (isShowLogo == true) View.VISIBLE else View.INVISIBLE @@ -197,6 +204,12 @@ class CallkitIncomingActivity : Activity() { val textDecline = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "") tvDecline.text = if (TextUtils.isEmpty(textDecline)) getString(R.string.text_decline) else textDecline + try { + tvAccept.setTextColor(Color.parseColor(textColor)) + tvDecline.setTextColor(Color.parseColor(textColor)) + } catch (error: Exception) { + } + val backgroundColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, "#0955fa") try { ivBackground.setBackgroundColor(Color.parseColor(backgroundColor)) diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt index 52457203..8b528543 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt @@ -12,54 +12,67 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { companion object { private const val TAG = "CallkitIncomingReceiver" + var silenceEvents = false fun getIntent(context: Context, action: String, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - this.action = "${context.packageName}.${action}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + this.action = "${context.packageName}.${action}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentIncoming(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentStart(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentAccept(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentDecline(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentEnded(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentTimeout(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } fun getIntentCallback(context: Context, data: Bundle?) = - Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - } + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } + + fun getIntentHeldByCell(context: Context, data: Bundle?) = + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_HELD}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } + + fun getIntentUnHeldByCell(context: Context, data: Bundle?) = + Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_UNHELD}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + } } @@ -76,7 +89,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { addCall(context, Data.fromBundle(data)) if (callkitNotificationManager.incomingChannelEnabled()) { val soundPlayerServiceIntent = - Intent(context, CallkitSoundPlayerService::class.java) + Intent(context, CallkitSoundPlayerService::class.java) soundPlayerServiceIntent.putExtras(data) context.startService(soundPlayerServiceIntent) } @@ -84,6 +97,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_START, data) @@ -92,6 +106,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_ACCEPT, data) @@ -102,6 +117,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_DECLINE, data) @@ -112,6 +128,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_ENDED, data) @@ -122,6 +139,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_TIMEOUT, data) @@ -134,6 +152,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" -> { try { callkitNotificationManager.clearMissCallNotification(data) @@ -150,45 +169,48 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { } private fun sendEventFlutter(event: String, data: Bundle) { + if (silenceEvents) return + val android = mapOf( - "isCustomNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false), - "isCustomSmallExNotification" to data.getBoolean( - CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, - false - ), - "ringtonePath" to data.getString(CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH, ""), - "backgroundColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, ""), - "backgroundUrl" to data.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, ""), - "actionColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, ""), - "incomingCallNotificationChannelName" to data.getString( - CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, - "" - ), - "missedCallNotificationChannelName" to data.getString( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, - "" - ), + "isCustomNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false), + "isCustomSmallExNotification" to data.getBoolean( + CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, + false + ), + "ringtonePath" to data.getString(CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH, ""), + "backgroundColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, ""), + "backgroundUrl" to data.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, ""), + "actionColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, ""), + "textColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, ""), + "incomingCallNotificationChannelName" to data.getString( + CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, + "" + ), + "missedCallNotificationChannelName" to data.getString( + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, + "" + ), ) val notification = mapOf( - "id" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID), - "showNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW), - "count" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT), - "subtitle" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE), - "callbackText" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT), - "isShowCallback" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW), + "id" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID), + "showNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW), + "count" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT), + "subtitle" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE), + "callbackText" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT), + "isShowCallback" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW), ) val forwardData = mapOf( - "id" to data.getString(CallkitConstants.EXTRA_CALLKIT_ID, ""), - "nameCaller" to data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, ""), - "avatar" to data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, ""), - "number" to data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, ""), - "type" to data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0), - "duration" to data.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L), - "textAccept" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, ""), - "textDecline" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, ""), - "extra" to data.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA)!!, - "missedCallNotification" to notification, - "android" to android + "id" to data.getString(CallkitConstants.EXTRA_CALLKIT_ID, ""), + "nameCaller" to data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, ""), + "avatar" to data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, ""), + "number" to data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, ""), + "type" to data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0), + "duration" to data.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L), + "textAccept" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, ""), + "textDecline" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, ""), + "extra" to data.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA)!!, + "missedCallNotification" to notification, + "android" to android ) FlutterCallkitIncomingPlugin.sendEvent(event, forwardData) } diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt index a4cbf13f..92d23667 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt @@ -1,10 +1,8 @@ package com.hiennv.flutter_callkit_incoming -import android.annotation.SuppressLint import android.app.Service import android.content.Context import android.content.Intent -import android.content.res.AssetFileDescriptor import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer @@ -37,6 +35,9 @@ class CallkitSoundPlayerService : Service() { mediaPlayer?.stop() mediaPlayer?.release() vibrator?.cancel() + + mediaPlayer = null + vibrator = null } private fun prepare() { diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt index f3db194c..1478a2d8 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt @@ -1,21 +1,16 @@ package com.hiennv.flutter_callkit_incoming -import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.content.Context -import android.content.DialogInterface import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper -import android.provider.Settings +import android.util.Log import androidx.annotation.NonNull -import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat import com.hiennv.flutter_callkit_incoming.Utils.Companion.reapCollection +import com.hiennv.flutter_callkit_incoming.telecom.TelecomUtilities import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -34,6 +29,9 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA @SuppressLint("StaticFieldLeak") private lateinit var instance: FlutterCallkitIncomingPlugin + @SuppressLint("StaticFieldLeak") + private lateinit var telecomUtilities: TelecomUtilities + public fun getInstance(): FlutterCallkitIncomingPlugin { return instance } @@ -79,6 +77,9 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA val handler = EventCallbackHandler() eventHandlers.add(WeakReference(handler)) events.setStreamHandler(handler) + + telecomUtilities = TelecomUtilities(context) + TelecomUtilities.telecomUtilitiesSingleton = telecomUtilities } } @@ -161,14 +162,34 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA data.toBundle() ) ) + + // only report to telecom if it's a voice call + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.reportIncomingCall(data) + } + + result.success("OK") + } + + "showCallkitIncomingSilently" -> { + val data = Data(call.arguments() ?: HashMap()) + data.from = "notification" + + // we don't need to send a broadcast, we only need to report the data to telecom + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.reportIncomingCall(data) + } + result.success("OK") } + "showMissCallNotification" -> { val data = Data(call.arguments() ?: HashMap()) data.from = "notification" callkitNotificationManager?.showMissCallNotification(data.toBundle()) result.success("OK") } + "startCall" -> { val data = Data(call.arguments() ?: HashMap()) context?.sendBroadcast( @@ -177,8 +198,14 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA data.toBundle() ) ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.startCall(data) + } + result.success("OK") } + "muteCall" -> { val map = buildMap { val args = call.arguments @@ -186,9 +213,16 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA putAll(args as Map) } } - sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_MUTE, map); + sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_MUTE, map) + + val data = Data(call.arguments() ?: HashMap()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.muteCall(data) + } + result.success("OK") } + "holdCall" -> { val map = buildMap { val args = call.arguments @@ -196,12 +230,24 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA putAll(args as Map) } } - sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_HOLD, map); + sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_HOLD, map) + + val data = Data(call.arguments() ?: HashMap()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (data.isOnHold) { + telecomUtilities.holdCall(data) + } else { + telecomUtilities.unHoldCall(data) + } + } + result.success("OK") } + "isMuted" -> { result.success(false) } + "endCall" -> { val data = Data(call.arguments() ?: HashMap()) context?.sendBroadcast( @@ -210,11 +256,22 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA data.toBundle() ) ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.endCall(data) + } + result.success("OK") } + "callConnected" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.acceptCall(Data(call.arguments() ?: HashMap())) + } + result.success("OK") } + "endAllCalls" -> { val calls = getDataActiveCalls(context) calls.forEach { @@ -235,14 +292,29 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } } removeAllCalls(context) + + //Additional safety net + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.endAllActiveCalls() + } + result.success("OK") } + "activeCalls" -> { result.success(getDataActiveCallsForFlutter(context)) } + "getDevicePushTokenVoIP" -> { result.success("") } + + "silenceEvents" -> { + val silence = call.arguments as? Boolean ?: false + CallkitIncomingBroadcastReceiver.silenceEvents = silence + result.success("") + } + "requestNotificationPermission" -> { val map = buildMap { val args = call.arguments @@ -252,6 +324,26 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } callkitNotificationManager?.requestNotificationPermission(activity, map) } + // EDIT - clear the incoming notification/ring (after accept/decline/timeout) + "hideCallkitIncoming" -> { + val data = Data(call.arguments() ?: HashMap()) + context?.stopService(Intent(context, CallkitSoundPlayerService::class.java)) + callkitNotificationManager?.clearIncomingNotification(data.toBundle(), false) + } + + "endNativeSubsystemOnly" -> { + val data = Data(call.arguments() ?: HashMap()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + telecomUtilities.endCall(data) + } + } + + "setAudioRoute" -> { + val data = Data(call.arguments() ?: HashMap()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + telecomUtilities.setAudioRoute(data) + } + } } } catch (error: Exception) { result.error("error", error.message, "") @@ -278,7 +370,12 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA binding.addRequestPermissionsResultListener(this) } - override fun onDetachedFromActivity() {} + override fun onDetachedFromActivity() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Log.d("FlutterCallkitPlugin", "onDetachedFromActivity: called -- activity destroyed? ${activity?.isDestroyed}") + if (activity?.isDestroyed == true) telecomUtilities.endAllActiveCalls() + } + } class EventCallbackHandler : EventChannel.StreamHandler { @@ -309,5 +406,4 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } - } diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt new file mode 100644 index 00000000..09ca3f84 --- /dev/null +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt @@ -0,0 +1,199 @@ +package com.hiennv.flutter_callkit_incoming.telecom + + +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.telecom.CallAudioState +import android.telecom.Connection +import android.telecom.DisconnectCause +import android.telecom.TelecomManager +import android.util.Log +import androidx.core.os.bundleOf +import com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_ACCEPT +import com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_AUDIO_STATE_CHANGE +import com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_ENDED +import com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_HELD +import com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_UNHELD +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_HANDLE +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_ID +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_NAME_CALLER +import com.hiennv.flutter_callkit_incoming.CallkitIncomingBroadcastReceiver +import com.hiennv.flutter_callkit_incoming.telecom.TelecomUtilities.Companion.androidToJsRouteMap +import java.io.PrintWriter +import java.io.StringWriter + + +// REF https://developer.android.com/reference/android/telecom/Connection +// the handle hashmap has the uuid under `EXTRA_CALLKIT_ID` +class TelecomConnection internal constructor(private val context: Context, private val handle: HashMap) : Connection() { + init { + // previously, the caps and voip mode were set in two different places for incoming/outgoing connections + // moreover, the voip mode was set in the "onAnswer" method for incoming calls which caused the connection to be incorrectly set up if it was answered from the app UI + connectionCapabilities = PROPERTY_SELF_MANAGED or CAPABILITY_MUTE or CAPABILITY_HOLD or CAPABILITY_SUPPORT_HOLD + audioModeIsVoip = true + + val number = handle[EXTRA_CALLKIT_HANDLE] + val name = handle[EXTRA_CALLKIT_NAME_CALLER] + + if (number != null) setAddress(Uri.parse(number), TelecomManager.PRESENTATION_ALLOWED) + if (name != null && name != "") setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED) + } + + // called when answered from bt device/car + override fun onAnswer() { + super.onAnswer() + TelecomUtilities.logToFile("[TelecomConnection] onAnswer called") + + val uuid = handle[EXTRA_CALLKIT_ID] ?: "" + val data: Map = object : HashMap() { + init { + put("event", ACTION_CALL_ACCEPT) + put(EXTRA_CALLKIT_ID, uuid) + } + } + TelecomUtilities.logToFile("[TelecomConnection] On Answer data: $data") + + context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntentAccept(context, bundleOf(*data.toList().toTypedArray()))) + + TelecomUtilities.logToFile("[TelecomConnection] onAnswer executed") + setActive() + } + + override fun onAbort() { + super.onAbort() + TelecomUtilities.logToFile("[TelecomConnection] onAbort") + setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) + endCall() + TelecomUtilities.logToFile("[TelecomConnection] onAbort executed") + } + + override fun onReject() { + super.onReject() + setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) + TelecomUtilities.logToFile("[TelecomConnection] onReject") + endCall() + TelecomUtilities.logToFile("[TelecomConnection] onReject executed") + } + override fun onDisconnect() { + super.onDisconnect() + TelecomUtilities.logToFile("[TelecomConnection] onDisconnect") + setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) + endCall() + } + public fun endCall() { + TelecomUtilities.logToFile("[TelecomConnection] Ending call - disconnectCause: $disconnectCause") + val uuid = handle[EXTRA_CALLKIT_ID] ?: "" + val data: Map = object : HashMap() { + init { + put("event", ACTION_CALL_ENDED) + put("disconnectCause", disconnectCause.toString()) + put(EXTRA_CALLKIT_ID, uuid) + } + } + context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntentEnded(context, bundleOf(*data.toList().toTypedArray()))) + try { + TelecomConnectionService.deinitConnection(handle[EXTRA_CALLKIT_ID] ?: "") + + } catch (exception: Throwable) { + Log.e(TAG, "Handle map error", exception) + + val stackTrace = StringWriter() + exception.printStackTrace(PrintWriter(stackTrace)) + + TelecomUtilities.logToFile("[TelecomUtilities] EXCEPTION reportIncomingCall -- $exception -- message: ${exception.message} -- stack: $stackTrace") + } + + destroy() + } + + override fun onHold() { + TelecomUtilities.logToFile("[TelecomConnection] On hold") + super.onHold() + //GF not needed + + val uuid = handle[EXTRA_CALLKIT_ID] ?: "" + val data: Map = object : HashMap() { + init { + put("event", ACTION_CALL_HELD) + put(EXTRA_CALLKIT_ID, uuid) + put("args", 1) + } + } + context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntentHeldByCell(context, bundleOf(*data.toList().toTypedArray()))) + + setOnHold(); + + //context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntent(context, ACTION_CALL_HELD, bundleOf(*data.toList().toTypedArray()))) + + + + } + override fun onUnhold() { + super.onUnhold() + val uuid = handle[EXTRA_CALLKIT_ID] ?: "" + val data: Map = object : HashMap() { + init { + put("event", ACTION_CALL_UNHELD) + put(EXTRA_CALLKIT_ID, uuid) + put("args", 0) + } + } + //context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntentCallback(context, bundleOf(*data.toList().toTypedArray()))) + + context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntentUnHeldByCell(context, bundleOf(*data.toList().toTypedArray()))) + TelecomConnectionService.setAllOthersOnHold(uuid) + setActive() + } + + // dnc + override fun onPlayDtmfTone(dtmf: Char) { + TelecomUtilities.logToFile("[TelecomConnection] OnPlayDTMFTone") + } + + // dnc - should be used to show the (fullscreen) notification for the user + override fun onShowIncomingCallUi() { + super.onShowIncomingCallUi() + TelecomUtilities.logToFile("[TelecomConnection] Show incoming call UI") + } + // dnc - should be used to silence the ringer when the user presses the volume down button + override fun onSilence() { + super.onSilence() + TelecomUtilities.logToFile("[TelecomConnection] TODO silence ringer") + } + + // from inCallService (not used in self_managed) + override fun onCallEvent(event: String, extras: Bundle?) { + super.onCallEvent(event, extras) + TelecomUtilities.logToFile("[TelecomConnection] CALL EVENT: $event") + } + + override fun onStateChanged(state: Int) { + super.onStateChanged(state) + TelecomUtilities.logToFile("[TelecomConnection] ON STATE CHANGED: $state") + // Toast.makeText(context, "onStateChanged $state", Toast.LENGTH_LONG).show() + } + + // IMPORTANT (note: deprecated in Android 14 - API 34) + // this event triggers for both mute state and audio route + // actually it doesn't trigger for mute changes!! + override fun onCallAudioStateChanged(state: CallAudioState) { + super.onCallAudioStateChanged(state) + TelecomUtilities.logToFile("[TelecomConnection] On Call Audio State Changed -- route: ${state.route} -- is muted: ${state.isMuted}") + + val uuid = handle[EXTRA_CALLKIT_ID] ?: "" + val data: Map = object : HashMap() { + init { + put("event", ACTION_CALL_AUDIO_STATE_CHANGE) + put(EXTRA_CALLKIT_ID, uuid) + put("args", androidToJsRouteMap[state.route] ?: 1) // TODO use a different key than "args"? + } + } + + context.sendBroadcast(CallkitIncomingBroadcastReceiver.getIntent(context, ACTION_CALL_AUDIO_STATE_CHANGE, bundleOf(*data.toList().toTypedArray()))) + } + + companion object { + private const val TAG = "TelecomConnection" + } +} diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt new file mode 100644 index 00000000..8b3fe766 --- /dev/null +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt @@ -0,0 +1,149 @@ +package com.hiennv.flutter_callkit_incoming.telecom + +import android.content.Context +import android.os.Bundle +import android.telecom.Connection +import android.telecom.ConnectionRequest +import android.telecom.ConnectionService +import android.telecom.PhoneAccountHandle +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_HANDLE +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_ID +import com.hiennv.flutter_callkit_incoming.CallkitConstants.EXTRA_CALLKIT_NAME_CALLER +import java.util.UUID +import android.util.Log + +// for now, I don't care about notifying anybody about connection creations/failures +// the connection itself is supposed to do that? +class TelecomConnectionService : ConnectionService() { + + + + override fun onCreate() { + super.onCreate() + TelecomConnectionService.applicationContext = applicationContext + } + + override fun onDestroy() { + + try { + Log.d(TAG, "[TelecomConnectionService] onDestroy") + TelecomUtilities.logToFile("[TelecomConnectionService] onDestroy ") + TelecomUtilities.logToFile("[TelecomConnectionService] onDestroy - kill all calls "); + + //We end all connections + for ((key, value) in currentConnections) { + value.endCall() + + } + } + catch (er: Exception) { + TelecomUtilities.logToFile("EXCEPTION reportIncomingCall -- $er") + } + + } + + override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest): Connection { + TelecomUtilities.logToFile("[TelecomConnectionService] OnCreateIncomingConnection -- UUID: number:${request.extras.getString(EXTRA_CALLKIT_ID)}") + + // to test global exception handling + // throw Exception("EXCEPTION from onCreateIncomingConnection") + + val incomingCallConnection = createConnection(request) + incomingCallConnection.setRinging() + incomingCallConnection.setInitialized() + + return incomingCallConnection + } + + override fun onCreateIncomingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest) { + super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request) + TelecomUtilities.logToFile("[TelecomConnectionService] OnCreateIncomingConnection FAILED") + } + + override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest): Connection { + val extras = request.extras + val number = request.address?.schemeSpecificPart ?: "Outbound Call" + val displayName = extras.getString(EXTRA_CALLKIT_NAME_CALLER) + + TelecomUtilities.logToFile("[TelecomConnectionService] onCreateOutgoingConnection -- UUID: ${request.extras.getString(EXTRA_CALLKIT_ID)} number: $number, displayName:$displayName") + + val outgoingCallConnection = createConnection(request) + outgoingCallConnection.setDialing() + + TelecomUtilities.logToFile("[TelecomConnectionService] onCreateOutgoingConnection: dialing") + + val uuid = outgoingCallConnection.extras.getString(EXTRA_CALLKIT_ID) ?: "" + setAllOthersOnHold(uuid) + + return outgoingCallConnection + } + + override fun onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest) { + super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request) + TelecomUtilities.logToFile("[TelecomConnectionService] OnCreateOutgoingConnectionFailed FAILED") + } + + private fun createConnection(request: ConnectionRequest): Connection { + TelecomUtilities.logToFile("[TelecomConnectionService] createConnection -- UUID: ${request.extras.getString(EXTRA_CALLKIT_ID)}") + + val extras = request.extras + if (extras.getString(EXTRA_CALLKIT_ID) == null) { + extras.putString(EXTRA_CALLKIT_ID, UUID.randomUUID().toString()) + } + + val extrasMap = bundleToMap(extras) + extrasMap[EXTRA_CALLKIT_HANDLE] = request.address?.toString() ?: "Callkit Incoming Call" + val connection = TelecomConnection(this, extrasMap) + + connection.setInitializing() + connection.extras = extras + currentConnections[extras.getString(EXTRA_CALLKIT_ID)] = connection + + return connection + } + + private fun bundleToMap(extras: Bundle): HashMap { + val extrasMap = HashMap() + val keySet = extras.keySet() + val iterator: Iterator = keySet.iterator() + while (iterator.hasNext()) { + val key = iterator.next() + if (extras[key] != null) { + extrasMap[key] = extras[key].toString() + } + } + return extrasMap + } + + companion object { + + private const val TAG = "TelecomConnectionService" + + + var applicationContext: Context? = null + + var currentConnections: MutableMap = HashMap() + + fun getConnection(connectionId: String?): Connection? { + return if (currentConnections.containsKey(connectionId)) { + currentConnections[connectionId] + } else null + } + + fun deinitConnection(connectionId: String) { + TelecomUtilities.logToFile("[TelecomConnectionService] deinitConnection: $connectionId") + if (currentConnections.containsKey(connectionId)) { + currentConnections.remove(connectionId) + } + } + + // put all other calls on hold + fun setAllOthersOnHold(myUID: String?) { + for ((key, value) in currentConnections) { + if (!key.contentEquals(myUID)) { + value.onHold() + } + } + } + } +} diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt new file mode 100644 index 00000000..725ce97e --- /dev/null +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt @@ -0,0 +1,240 @@ +package com.hiennv.flutter_callkit_incoming.telecom + +import android.Manifest +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.CallAudioState +import android.telecom.Connection +import android.telecom.PhoneAccount +import android.telecom.PhoneAccountHandle +import android.telecom.TelecomManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.annotation.RequiresApi +import com.hiennv.flutter_callkit_incoming.CallkitConstants +import com.hiennv.flutter_callkit_incoming.Data +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.ZoneOffset +import java.util.UUID + +// the most important thing this does is registering the phone account +@RequiresApi(Build.VERSION_CODES.M) +class TelecomUtilities(private val applicationContext : Context) { + + private lateinit var telecomManager: TelecomManager + private lateinit var handle: PhoneAccountHandle + private lateinit var telephonyManager: TelephonyManager + + private var requiredPermissions: Array + + init { + registerPhoneAccount(applicationContext) + + requiredPermissions = arrayOf(Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE, Manifest.permission.RECORD_AUDIO) + if(Build.VERSION.SDK_INT > 29){ + requiredPermissions += Manifest.permission.READ_PHONE_NUMBERS + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun registerPhoneAccount(appContext: Context) { + + val cName = ComponentName(applicationContext, TelecomConnectionService::class.java) + val appName = getApplicationName(appContext) + handle = PhoneAccountHandle(cName, appName) + + val identifier = appContext.resources.getIdentifier("ic_logo", "mipmap", appContext.packageName) + val icon = Icon.createWithResource(appContext, identifier) + + val account = PhoneAccount.Builder(handle, appName) + .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED) + .setIcon(icon) + .build() + + telephonyManager = applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + + telecomManager = applicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + telecomManager.registerPhoneAccount(account) + + logToFile("[TelecomUtilities] REGISTERED PHONE ACCOUNT") + } + + private fun getApplicationName(appContext: Context): String { + val applicationInfo = appContext.applicationInfo + val stringId = applicationInfo.labelRes + return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else appContext.getString(stringId) + } + + // incoming call + @RequiresApi(Build.VERSION_CODES.M) + fun reportIncomingCall(data: Data) { + try { + val extras = Bundle() + + val uuid: String = data.id + extras.putString(CallkitConstants.EXTRA_CALLKIT_ID, uuid) + + // dnc + val name: String = data.nameCaller + extras.putString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, name) + // visible in cars + val handleString: String = name // data.handle + val uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, name, null) + extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri) + + logToFile("[TelecomUtilities] reportIncomingCall number: $handleString, uuid: $uuid") + + telecomManager.addNewIncomingCall(handle, extras) + + } catch (er: Exception) { + Log.e(TAG,"EXCEPTION reportIncomingCall -- $er", er) + + val stackTrace = StringWriter() + er.printStackTrace(PrintWriter(stackTrace)) + + logToFile("[TelecomUtilities] EXCEPTION reportIncomingCall -- $er -- message: ${er.message} -- stack: $stackTrace") + } + } + + // outgoing call + @RequiresApi(Build.VERSION_CODES.M) + @SuppressLint("MissingPermission") + fun startCall(data: Data) { + val extras = Bundle() // has the account handle + val callExtras = Bundle() // has the caller's name/number + + val uuid = UUID.fromString(data.uuid) + + val number : String = data.handle + val uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null) + callExtras.putString(CallkitConstants.EXTRA_CALLKIT_HANDLE, number) + callExtras.putString(CallkitConstants.EXTRA_CALLKIT_ID, uuid.toString()) + + logToFile("[TelecomUtilities] startCall -- number: $number") + + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle) + extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + telecomManager.placeCall(uri, extras) + } + + @RequiresApi(Build.VERSION_CODES.M) + fun endCall(data: Data) { + logToFile("[TelecomUtilities] endCall -- UUID: ${data.uuid}") + + val uuid: String = data.uuid + val connection = TelecomConnectionService.getConnection(uuid) + connection?.onDisconnect() + } + + @RequiresApi(Build.VERSION_CODES.M) + fun holdCall(data: Data) { + logToFile("[TelecomUtilities] holdCall -- UUID = ${data.uuid} | hold = ${data.isOnHold}") + val connection = TelecomConnectionService.getConnection(data.uuid) + + if (data.isOnHold) connection?.onHold() + else connection?.onUnhold() + } + + @RequiresApi(Build.VERSION_CODES.M) + fun unHoldCall(data: Data) { + logToFile("[TelecomUtilities] unHoldCall -- UUID = ${data.uuid} ") + val connection = TelecomConnectionService.getConnection(data.uuid) + connection?.onUnhold() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun setAudioRoute(data: Data) { + val connection = TelecomConnectionService.getConnection(data.uuid) + + logToFile("[TelecomUtilities] setAudioRoute -- UUID = ${data.uuid} | audioRoute = ${data.audioRoute}") + + val route = jsToAndroidRouteMap[data.audioRoute] ?: return + connection?.setAudioRoute(route) + + } + + @RequiresApi(Build.VERSION_CODES.M) + fun muteCall(data: Data) { + logToFile("[TelecomUtilities] muteCall -- UUID = ${data.uuid} | hold = ${data.isMuted}") + val uuid : String = data.uuid + val muted : Boolean = data.isMuted + val connection = TelecomConnectionService.getConnection(uuid) ?: return + + val newAudioState = if (muted) { + CallAudioState(true, connection.callAudioState.route, connection.callAudioState.supportedRouteMask) + } else { + CallAudioState(false, connection.callAudioState.route, connection.callAudioState.supportedRouteMask) + } + + connection.onCallAudioStateChanged(newAudioState) + } + + fun acceptCall(data: Data) { + val uuid : String = data.uuid + + val connection = TelecomConnectionService.getConnection(uuid) + logToFile("[TelecomUtilities] acceptCall -- UUID = $uuid connection exists? ${connection!=null}") + + // avoid infinite loop by not calling onAnswer if the state isn't already ACTIVE + if (connection?.state != Connection.STATE_ACTIVE) connection?.onAnswer() + else logToFile("[TelecomUtilities] acceptCall -- UUID = $uuid is already active") + + logToFile("[TelecomUtilities] acceptCall -- AUDIO ROUTE: ${connection?.callAudioState?.route?.toString()}") + } + + fun endAllActiveCalls() { + Log.d(TAG, "endAllActiveCalls: ${TelecomConnectionService.currentConnections.size}") + TelecomConnectionService.currentConnections.forEach { (_, c) -> c.onDisconnect() } + } + + companion object { + private const val TAG = "TelecomUtilities" + + public var telecomUtilitiesSingleton :TelecomUtilities? = null + + + val androidToJsRouteMap = mapOf( + CallAudioState.ROUTE_EARPIECE to 1, + CallAudioState.ROUTE_BLUETOOTH to 2, + CallAudioState.ROUTE_WIRED_HEADSET to 3, + CallAudioState.ROUTE_SPEAKER to 4, + CallAudioState.ROUTE_WIRED_OR_EARPIECE to 5, + ) + + val jsToAndroidRouteMap = mapOf( + 1 to CallAudioState.ROUTE_EARPIECE, + 2 to CallAudioState.ROUTE_BLUETOOTH, + 3 to CallAudioState.ROUTE_WIRED_HEADSET, + 4 to CallAudioState.ROUTE_SPEAKER, + 5 to CallAudioState.ROUTE_WIRED_OR_EARPIECE, + ) + + private const val logToFile = false // log to file flag + fun logToFile(message: String) { + Log.d("CallkitTelecom", message) + + val context = TelecomConnectionService.applicationContext ?: return + + if (!logToFile) return + try { + val timestamp = LocalDateTime.now(ZoneOffset.UTC) + val path = "${context.cacheDir}/console_logs_${timestamp.format(DateTimeFormatter.ofPattern("yyyyMMdd"))}.txt" + + val file = File(path) + file.appendText("${timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"))} $message") + + } catch (e: Exception) { + Log.e(TAG, e.message ?: "", e) + } + } + } +} diff --git a/android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml b/android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml index 3e5e6d95..f5c6682b 100644 --- a/android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml +++ b/android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml @@ -48,6 +48,7 @@ android:id="@+id/ivAvatar" android:layout_width="@dimen/size_avatar" android:layout_height="@dimen/size_avatar" + android:visibility="invisible" android:layout_centerInParent="true" android:src="@drawable/ic_default_avatar" app:civ_border_color="#80ffffff" diff --git a/android/src/main/res/layout/activity_callkit_incoming.xml b/android/src/main/res/layout/activity_callkit_incoming.xml index 70bf002f..19c06432 100644 --- a/android/src/main/res/layout/activity_callkit_incoming.xml +++ b/android/src/main/res/layout/activity_callkit_incoming.xml @@ -48,6 +48,7 @@ android:id="@+id/ivAvatar" android:layout_width="@dimen/size_avatar" android:layout_height="@dimen/size_avatar" + android:visibility="invisible" android:layout_centerInParent="true" android:src="@drawable/ic_default_avatar" app:civ_border_color="#80ffffff" diff --git a/android/src/main/res/layout/layout_custom_notification.xml b/android/src/main/res/layout/layout_custom_notification.xml index 627ad2e0..6baf5af0 100644 --- a/android/src/main/res/layout/layout_custom_notification.xml +++ b/android/src/main/res/layout/layout_custom_notification.xml @@ -40,7 +40,7 @@ android:layout_height="@dimen/base_margin_x4_8" android:scaleType="centerCrop" android:src="@drawable/ic_default_avatar" - android:visibility="visible" /> + android:visibility="invisible" /> + android:visibility="invisible" /> + android:visibility="invisible" /> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.13.0): + - FirebaseCoreInternal (10.19.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.13.0): + - FirebaseInstallations (10.19.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -36,32 +36,32 @@ PODS: - flutter_callkit_incoming (0.0.1): - CryptoSwift - Flutter - - GoogleDataTransport (9.2.5): + - GoogleDataTransport (9.3.0): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/AppDelegateSwizzler (7.12.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Environment (7.12.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.12.0): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Network (7.12.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.5)" - - GoogleUtilities/Reachability (7.11.5): + - "GoogleUtilities/NSData+zlib (7.12.0)" + - GoogleUtilities/Reachability (7.12.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/UserDefaults (7.12.0): - GoogleUtilities/Logger - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - PromisesObjC (2.3.1) DEPENDENCIES: @@ -94,21 +94,21 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_callkit_incoming/ios" SPEC CHECKSUMS: - CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1 + CryptoSwift: 52aaf3fce7337552863b1d952e408085f0e65030 Firebase: 0219acf760880eeec8ce479895bd7767466d9f81 firebase_core: 312d0d81b346ec20540822c8498e626d6918ef48 firebase_messaging: 8432ce73100171cab1707fad998a89590276bb4d FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 - FirebaseCoreInternal: b342e37cd4f5b4454ec34308f073420e7920858e - FirebaseInstallations: b28af1b9f997f1a799efe818c94695a3728c352f + FirebaseCoreInternal: b444828ea7cfd594fca83046b95db98a2be4f290 + FirebaseInstallations: 033d199474164db20c8350736842a94fe717b960 FirebaseMessaging: ac9062bcc35ed56e15a0241d8fd317022499baf8 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_callkit_incoming: 417dd1b46541cdd5d855ad795ccbe97d1c18155e - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 -PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d +PODFILE CHECKSUM: 505fe807fc9a2ba684f6436bd029b74c8430ab62 COCOAPODS: 1.12.1 diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 84dc7c35..1eb1a24e 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -4,7 +4,7 @@ import Flutter import flutter_callkit_incoming @UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate { +@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -75,10 +75,108 @@ import flutter_callkit_incoming SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) //Make sure call completion() - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { completion() } } + // Func Call api for Accept + func onAccept(_ call: Call) { + let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any] + print("LOG: onAccept") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + // Func Call API for Decline + func onDecline(_ call: Call) { + let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any] + print("LOG: onDecline") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + func onEnd(_ call: Call) { + let json = ["action": "END", "data": call.data.toJSON()] as [String: Any] + print("LOG: onEnd") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + func onTimeOut(_ call: Call) { + let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any] + print("LOG: onTimeOut") + self.performRequest(parameters: json) { result in + switch result { + case .success(let data): + print("Received data: \(data)") + + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } + + func performRequest(parameters: [String: Any], completion: @escaping (Result) -> Void) { + if let url = URL(string: "https://webhook.site/e32a591f-0d17-469d-a70d-33e9f9d60727") { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + //Add header + + do { + let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: []) + request.httpBody = jsonData + } catch { + completion(.failure(error)) + return + } + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data else { + completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Empty data"]))) + return + } + + do { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + completion(.success(jsonObject)) + } catch { + completion(.failure(error)) + } + } + task.resume() + } else { + completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) + } + } + + } diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 3d0b884e..8434412a 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -152,6 +152,7 @@ class HomePageState extends State { backgroundColor: '#0955fa', backgroundUrl: 'assets/test.png', actionColor: '#4CAF50', + textColor: '#ffffff', incomingCallNotificationChannelName: 'Incoming Call', missedCallNotificationChannelName: 'Missed Call', ), @@ -204,8 +205,7 @@ class HomePageState extends State { } Future getDevicePushTokenVoIP() async { - var devicePushTokenVoIP = - await FlutterCallkitIncoming.getDevicePushTokenVoIP(); + var devicePushTokenVoIP = await FlutterCallkitIncoming.getDevicePushTokenVoIP(); print(devicePushTokenVoIP); } @@ -224,8 +224,7 @@ class HomePageState extends State { case Event.actionCallAccept: // TODO: accepted an incoming call // TODO: show screen calling in Flutter - NavigationService.instance - .pushNamedIfNotCurrent(AppRoute.callingPage, args: event.body); + NavigationService.instance.pushNamedIfNotCurrent(AppRoute.callingPage, args: event.body); break; case Event.actionCallDecline: // TODO: declined an incoming call @@ -270,8 +269,7 @@ class HomePageState extends State { //check with https://webhook.site/#!/2748bc41-8599-4093-b8ad-93fd328f1cd2 Future requestHttp(content) async { - get(Uri.parse( - 'https://webhook.site/2748bc41-8599-4093-b8ad-93fd328f1cd2?data=$content')); + get(Uri.parse('https://webhook.site/2748bc41-8599-4093-b8ad-93fd328f1cd2?data=$content')); } void onEvent(CallEvent event) { diff --git a/example/lib/main.dart b/example/lib/main.dart index 9c473b1c..201d3be7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -40,6 +40,7 @@ Future showCallkitIncoming(String uuid) async { backgroundColor: '#0955fa', backgroundUrl: 'assets/test.png', actionColor: '#4CAF50', + textColor: '#ffffff', ), ios: const IOSParams( iconName: 'CallKitLogo', @@ -107,8 +108,7 @@ class MyAppState extends State with WidgetsBindingObserver { Future checkAndNavigationCallingPage() async { var currentCall = await getCurrentCall(); if (currentCall != null) { - NavigationService.instance - .pushNamedIfNotCurrent(AppRoute.callingPage, args: currentCall); + NavigationService.instance.pushNamedIfNotCurrent(AppRoute.callingPage, args: currentCall); } } @@ -132,8 +132,7 @@ class MyAppState extends State with WidgetsBindingObserver { _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onMessage.listen((RemoteMessage message) async { - print( - 'Message title: ${message.notification?.title}, body: ${message.notification?.body}, data: ${message.data}'); + print('Message title: ${message.notification?.title}, body: ${message.notification?.body}, data: ${message.data}'); _currentUuid = _uuid.v4(); showCallkitIncoming(_currentUuid!); }); @@ -149,15 +148,12 @@ class MyAppState extends State with WidgetsBindingObserver { onGenerateRoute: AppRoute.generateRoute, initialRoute: AppRoute.homePage, navigatorKey: NavigationService.instance.navigationKey, - navigatorObservers: [ - NavigationService.instance.routeObserver - ], + navigatorObservers: [NavigationService.instance.routeObserver], ); } Future getDevicePushTokenVoIP() async { - var devicePushTokenVoIP = - await FlutterCallkitIncoming.getDevicePushTokenVoIP(); + var devicePushTokenVoIP = await FlutterCallkitIncoming.getDevicePushTokenVoIP(); print(devicePushTokenVoIP); } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 23350d9d..c261ffd7 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" crypto: dependency: transitive description: @@ -124,7 +124,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0+1" + version: "2.0.0+2" flutter_test: dependency: "direct dev" description: flutter @@ -171,18 +171,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -216,10 +216,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -256,10 +256,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" typed_data: dependency: transitive description: @@ -284,6 +284,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" sdks: - dart: ">=3.0.0-0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.3.0" diff --git a/ios/Classes/Call.swift b/ios/Classes/Call.swift index 8228547e..4908efda 100644 --- a/ios/Classes/Call.swift +++ b/ios/Classes/Call.swift @@ -10,11 +10,11 @@ import AVFoundation public class Call: NSObject { - let uuid: UUID - let data: Data - let isOutGoing: Bool + public var uuid: UUID + public var data: Data + public var isOutGoing: Bool - var handle: String? + public var handle: String? var stateDidChange: (() -> Void)? var hasStartedConnectDidChange: (() -> Void)? @@ -133,6 +133,7 @@ public class Call: NSObject { @objc public var handle: String @objc public var avatar: String @objc public var type: Int + @objc public var normalHandle: Int @objc public var duration: Int @objc public var extra: NSDictionary @@ -161,6 +162,7 @@ public class Call: NSObject { self.handle = handle self.avatar = "" self.type = type + self.normalHandle = 0 self.duration = 30000 self.extra = [:] self.iconName = "CallKitLogo" @@ -196,6 +198,7 @@ public class Call: NSObject { self.handle = args["handle"] as? String ?? "" self.avatar = args["avatar"] as? String ?? "" self.type = args["type"] as? Int ?? 0 + self.normalHandle = args["normalHandle"] as? Int ?? 0 self.duration = args["duration"] as? Int ?? 30000 self.extra = args["extra"] as? NSDictionary ?? [:] @@ -237,7 +240,7 @@ public class Call: NSObject { } } - public func toJSON() -> [String: Any] { + open func toJSON() -> [String: Any] { let ios: [String : Any] = [ "iconName": iconName, "handleType": handleType, @@ -264,6 +267,7 @@ public class Call: NSObject { "handle": handle, "avatar": avatar, "type": type, + "normalHandle": normalHandle, "duration": duration, "extra": extra, "ios": ios @@ -272,7 +276,36 @@ public class Call: NSObject { } func getEncryptHandle() -> String { - return String(format: "{\"nameCaller\":\"%@\", \"handle\":\"%@\"}", nameCaller, handle).encryptHandle() + if (normalHandle > 0) { + return handle + } + do { + var map: [String: Any] = [:] + + map["nameCaller"] = nameCaller + map["handle"] = handle + + var mapExtras = extra as? [String: Any] + + if (mapExtras == nil) { + print("error casting dictionary to [String: Any]") + return String(format: "{\"nameCaller\":\"%@\", \"handle\":\"%@\"}", nameCaller, handle).encryptHandle() + } + + for (key, value) in mapExtras! { + map[key] = value + } + + let mapData = try JSONSerialization.data(withJSONObject: map, options: .prettyPrinted) + + let mapString: String = String(data: mapData, encoding: .utf8) ?? "" + + return mapString.encryptHandle() + } catch { + print("error encrypting call data") + return String(format: "{\"nameCaller\":\"%@\", \"handle\":\"%@\"}", nameCaller, handle).encryptHandle() + } + } diff --git a/ios/Classes/CallkitIncomingAppDelegate.swift b/ios/Classes/CallkitIncomingAppDelegate.swift new file mode 100644 index 00000000..c9aa5a86 --- /dev/null +++ b/ios/Classes/CallkitIncomingAppDelegate.swift @@ -0,0 +1,21 @@ +// +// CallkitIncomingAppDelegate.swift +// flutter_callkit_incoming +// +// Created by Hien Nguyen on 05/01/2024. +// + +import Foundation + + +public protocol CallkitIncomingAppDelegate : NSObjectProtocol { + + func onAccept(_ call: Call); + + func onDecline(_ call: Call); + + func onEnd(_ call: Call); + + func onTimeOut(_ call: Call); + +} diff --git a/ios/Classes/StringUtils.swift b/ios/Classes/StringUtils.swift index f0d9058c..164205f2 100644 --- a/ios/Classes/StringUtils.swift +++ b/ios/Classes/StringUtils.swift @@ -30,8 +30,7 @@ extension String { guard let data = Foundation.Data(base64Encoded: self) else { return "" } - - return String(data: data, encoding: .utf8)! + return String(data: data, encoding: .utf8) ?? "" } func toBase64() -> String { @@ -48,6 +47,11 @@ extension String { } public func getDecryptHandle() -> [String: Any] { + if (!self.isBase64Encoded()) { + var map: [String: Any] = [:] + map["handle"] = self + return map + } if let data = self.decryptHandle().data(using: .utf8) { do { return try (JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])! @@ -58,4 +62,31 @@ extension String { return [:] } + public func getHandleType() -> String { + if (!self.isBase64Encoded()) { + if (!self.isPhoneNumber()) { + return "email" + } else { + return "number" + } + } + return "generic" + } + + public func isBase64Encoded() -> Bool { + let value = self.fromBase64() + return !value.isEmpty + } + + func isPhoneNumber() -> Bool { + let cleanedValue = self + .replacingOccurrences(of: "[+-]", with: "", options: .regularExpression) + .replacingOccurrences(of: "[ ]", with: "", options: .regularExpression) + + + let decimalCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: cleanedValue) + return decimalCharacters.isSuperset(of: characterSet) + } + } diff --git a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift index 11c04d93..97b3860c 100644 --- a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift +++ b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift @@ -35,14 +35,20 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi private var data: Data? private var isFromPushKit: Bool = false + private var silenceEvents: Bool = false private let devicePushTokenVoIP = "DevicePushTokenVoIP" - private var answerAction: CXAnswerCallAction? private func sendEvent(_ event: String, _ body: [String : Any?]?) { - streamHandlers.reap().forEach { handler in - handler?.send(event, body ?? [:]) + if silenceEvents { + print(event, " silenced") + return + } else { + streamHandlers.reap().forEach { handler in + handler?.send(event, body ?? [:]) + } } + } @objc public func sendEventCustom(_ event: String, body: NSDictionary?) { @@ -182,8 +188,25 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi case "getDevicePushTokenVoIP": result(self.getDevicePushTokenVoIP()) break; - case "startCallIncoming": - self.answerAction?.fulfill() + case "silenceEvents": + guard let silence = call.arguments as? Bool else { + result("OK") + return + } + + self.silenceEvents = silence + result("OK") + break; + case "requestNotificationPermission": + result("OK") + break + case "hideCallkitIncoming": + result("OK") + break + case "endNativeSubsystemOnly": + result("OK") + break + case "setAudioRoute": result("OK") break default: @@ -344,7 +367,13 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi func callEndTimeout(_ data: Data) { self.saveEndCall(data.uuid, 3) + guard let call = self.callManager.callWithUUID(uuid: UUID(uuidString: data.uuid)!) else { + return + } sendEvent(SwiftFlutterCallkitIncomingPlugin.ACTION_CALL_TIMEOUT, data.toJSON()) + if let appDelegate = UIApplication.shared.delegate as? CallkitIncomingAppDelegate { + appDelegate.onTimeOut(call) + } } func getHandleType(_ handleType: String?) -> CXHandle.HandleType { @@ -488,6 +517,7 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi action.fail() return } + self.configurAudioSession() DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) { self.configurAudioSession() } @@ -495,8 +525,11 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi self?.sharedProvider?.reportOutgoingCall(with: call.uuid, connectedAt: call.connectedData) } self.answerCall = call - self.answerAction = action sendEvent(SwiftFlutterCallkitIncomingPlugin.ACTION_CALL_ACCEPT, self.data?.toJSON()) + if let appDelegate = UIApplication.shared.delegate as? CallkitIncomingAppDelegate { + appDelegate.onAccept(call) + } + action.fulfill() } @@ -514,11 +547,15 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi self.callManager.removeCall(call) if (self.answerCall == nil && self.outgoingCall == nil) { sendEvent(SwiftFlutterCallkitIncomingPlugin.ACTION_CALL_DECLINE, self.data?.toJSON()) - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - action.fulfill() + if let appDelegate = UIApplication.shared.delegate as? CallkitIncomingAppDelegate { + appDelegate.onDecline(call) } + action.fulfill() }else { sendEvent(SwiftFlutterCallkitIncomingPlugin.ACTION_CALL_ENDED, call.data.toJSON()) + if let appDelegate = UIApplication.shared.delegate as? CallkitIncomingAppDelegate { + appDelegate.onEnd(call) + } action.fulfill() } } @@ -566,7 +603,15 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { + guard let call = self.callManager.callWithUUID(uuid: action.uuid) else { + action.fail() + return + } sendEvent(SwiftFlutterCallkitIncomingPlugin.ACTION_CALL_TIMEOUT, self.data?.toJSON()) + if let appDelegate = UIApplication.shared.delegate as? CallkitIncomingAppDelegate { + appDelegate.onTimeOut(call) + } + action.fulfill() } public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { diff --git a/lib/entities/android_params.dart b/lib/entities/android_params.dart index a47d3183..9524e113 100644 --- a/lib/entities/android_params.dart +++ b/lib/entities/android_params.dart @@ -13,6 +13,7 @@ class AndroidParams { this.backgroundColor, this.backgroundUrl, this.actionColor, + this.textColor, this.incomingCallNotificationChannelName, this.missedCallNotificationChannelName, }); @@ -38,14 +39,16 @@ class AndroidParams { /// Color used in button/text on notification. final String? actionColor; + /// Color used for the text in the full screen notification + final String? textColor; + /// Notification channel name of incoming call. final String? incomingCallNotificationChannelName; /// Notification channel name of missed call. final String? missedCallNotificationChannelName; - factory AndroidParams.fromJson(Map json) => - _$AndroidParamsFromJson(json); + factory AndroidParams.fromJson(Map json) => _$AndroidParamsFromJson(json); Map toJson() => _$AndroidParamsToJson(this); } diff --git a/lib/entities/android_params.g.dart b/lib/entities/android_params.g.dart index 8a1b1b00..bc3b7bda 100644 --- a/lib/entities/android_params.g.dart +++ b/lib/entities/android_params.g.dart @@ -15,6 +15,7 @@ AndroidParams _$AndroidParamsFromJson(Map json) => backgroundColor: json['backgroundColor'] as String?, backgroundUrl: json['backgroundUrl'] as String?, actionColor: json['actionColor'] as String?, + textColor: json['textColor'] as String?, incomingCallNotificationChannelName: json['incomingCallNotificationChannelName'] as String?, missedCallNotificationChannelName: @@ -30,6 +31,7 @@ Map _$AndroidParamsToJson(AndroidParams instance) => 'backgroundColor': instance.backgroundColor, 'backgroundUrl': instance.backgroundUrl, 'actionColor': instance.actionColor, + 'textColor': instance.textColor, 'incomingCallNotificationChannelName': instance.incomingCallNotificationChannelName, 'missedCallNotificationChannelName': diff --git a/lib/entities/call_kit_params.dart b/lib/entities/call_kit_params.dart index 824af512..e0e4e4f7 100644 --- a/lib/entities/call_kit_params.dart +++ b/lib/entities/call_kit_params.dart @@ -16,6 +16,7 @@ class CallKitParams { this.avatar, this.handle, this.type, + this.normalHandle, this.duration, this.textAccept, this.textDecline, @@ -32,6 +33,7 @@ class CallKitParams { final String? avatar; final String? handle; final int? type; + final int? normalHandle; final int? duration; final String? textAccept; final String? textDecline; diff --git a/lib/entities/call_kit_params.g.dart b/lib/entities/call_kit_params.g.dart index 84351895..a2774cc1 100644 --- a/lib/entities/call_kit_params.g.dart +++ b/lib/entities/call_kit_params.g.dart @@ -14,6 +14,7 @@ CallKitParams _$CallKitParamsFromJson(Map json) => avatar: json['avatar'] as String?, handle: json['handle'] as String?, type: json['type'] as int?, + normalHandle: json['normalHandle'] as int?, duration: json['duration'] as int?, textAccept: json['textAccept'] as String?, textDecline: json['textDecline'] as String?, @@ -39,6 +40,7 @@ Map _$CallKitParamsToJson(CallKitParams instance) => 'avatar': instance.avatar, 'handle': instance.handle, 'type': instance.type, + 'normalHandle': instance.normalHandle, 'duration': instance.duration, 'textAccept': instance.textAccept, 'textDecline': instance.textDecline, diff --git a/lib/flutter_callkit_incoming.dart b/lib/flutter_callkit_incoming.dart index a58d40c3..064d8e9c 100644 --- a/lib/flutter_callkit_incoming.dart +++ b/lib/flutter_callkit_incoming.dart @@ -111,12 +111,14 @@ class FlutterCallkitIncoming { return await _channel.invokeMethod("getDevicePushTokenVoIP"); } + /// Silence CallKit events + static Future silenceEvents() async { + return await _channel.invokeMethod("silenceEvents", true); + } - /// Start incoming call - /// On iOS: start connection timer - /// On Android: not implemented - static Future startIncomingCall() async { - await _channel.invokeMethod("startCallIncoming"); + /// Unsilence CallKit events + static Future unsilenceEvents() async { + return await _channel.invokeMethod("silenceEvents", false); } /// Request permisstion show notification for Android(13) diff --git a/pubspec.yaml b/pubspec.yaml index d92cd618..06d0ac0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_callkit_incoming description: Flutter Callkit Incoming to show callkit screen in your Flutter app. -version: 2.0.0+2 +version: 2.0.1 homepage: https://github.com/hiennguyen92/flutter_callkit_incoming repository: https://github.com/hiennguyen92/flutter_callkit_incoming issue_tracker: https://github.com/hiennguyen92/flutter_callkit_incoming/issues