From c3375c45ff9e82f6dd0ab45cae5c9e4e6b3e873a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Tue, 31 Oct 2023 15:05:14 +0100 Subject: [PATCH 01/15] avatar visibility fix --- .../main/res/layout-w600dp-land/activity_callkit_incoming.xml | 1 + android/src/main/res/layout/activity_callkit_incoming.xml | 1 + android/src/main/res/layout/layout_custom_notification.xml | 2 +- .../src/main/res/layout/layout_custom_small_ex_notification.xml | 2 +- .../src/main/res/layout/layout_custom_small_notification.xml | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) 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" /> Date: Tue, 7 Nov 2023 17:36:34 +0100 Subject: [PATCH 02/15] feature(): add custom text color for full screen notitication --- README.md | 26 +- .../hiennv/flutter_callkit_incoming/Call.kt | 8 + .../CallkitConstants.kt | 1 + .../CallkitIncomingActivity.kt | 14 + .../CallkitIncomingBroadcastReceiver.kt | 471 ++++++++++++------ example/lib/home_page.dart | 10 +- example/lib/main.dart | 14 +- example/pubspec.lock | 32 +- lib/entities/android_params.dart | 7 +- lib/entities/android_params.g.dart | 2 + 10 files changed, 379 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index 07fab3b0..973d1015 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 @@ -366,11 +367,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 | @@ -391,7 +393,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca | **`ringtonePath`** | Add file to root project xcode `/ios/Runner/Ringtone.caf` and Copy Bundle Resources(Build Phases) |`Ringtone.caf`
`system_ringtone_default`
using ringtone default of the phone| -5. Source code +1. Source code ``` please checkout repo github @@ -415,7 +417,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/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt index 7c6989d3..fdbd2f25 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") @@ -81,6 +83,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 +181,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 +260,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..a9ba770f 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 @@ -52,6 +52,7 @@ 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_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 = 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 aeda78cb..d5b5a409 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 @@ -158,9 +158,17 @@ class CallkitIncomingActivity : Activity() { val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) if (data == null) finish() + val textColor = data?.getString(CallkitConstants.EXTRA_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 @@ -189,6 +197,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..d5b5a409 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 @@ -1,195 +1,336 @@ package com.hiennv.flutter_callkit_incoming -import android.annotation.SuppressLint +import android.app.Activity +import android.app.ActivityManager +import android.app.KeyguardManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ActivityInfo +import android.graphics.Color import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.view.animation.AnimationUtils +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout +import com.squareup.picasso.Picasso +import de.hdodenhof.circleimageview.CircleImageView +import kotlin.math.abs +import okhttp3.OkHttpClient +import com.squareup.picasso.OkHttp3Downloader +import android.view.ViewGroup.MarginLayoutParams +import android.os.PowerManager +import android.text.TextUtils import android.util.Log -class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { + +class CallkitIncomingActivity : Activity() { companion object { - private const val TAG = "CallkitIncomingReceiver" - 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) - } + private const val ACTION_ENDED_CALL_INCOMING = + "com.hiennv.flutter_callkit_incoming.ACTION_ENDED_CALL_INCOMING" - 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) - } + fun getIntent(context: Context, data: Bundle) = Intent(CallkitConstants.ACTION_CALL_INCOMING).apply { + action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" + putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } - 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) - } + fun getIntentEnded(context: Context, isAccepted: Boolean): Intent { + val intent = Intent("${context.packageName}.${ACTION_ENDED_CALL_INCOMING}") + intent.putExtra("ACCEPTED", isAccepted) + return intent + } + } - 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) + inner class EndedCallkitIncomingBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (!isFinishing) { + val isAccepted = intent.getBooleanExtra("ACCEPTED", false) + if (isAccepted) { + finishDelayed() + } else { + finishTask() + } } + } + } - 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) - } + private var endedCallkitIncomingBroadcastReceiver = EndedCallkitIncomingBroadcastReceiver() - 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) - } + private lateinit var ivBackground: ImageView + private lateinit var llBackgroundAnimation: RippleRelativeLayout - 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) - } + private lateinit var tvNameCaller: TextView + private lateinit var tvNumber: TextView + private lateinit var ivLogo: ImageView + private lateinit var ivAvatar: CircleImageView - 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) - } + private lateinit var llAction: LinearLayout + private lateinit var ivAcceptCall: ImageView + private lateinit var tvAccept: TextView + + private lateinit var ivDeclineCall: ImageView + private lateinit var tvDecline: TextView + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestedOrientation = if (!Utils.isTablet(this@CallkitIncomingActivity)) { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + }else { + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + setTurnScreenOn(true) + setShowWhenLocked(true) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) + } + transparentStatusAndNavigation() + setContentView(R.layout.activity_callkit_incoming) + initView() + incomingData(intent) + registerReceiver( + endedCallkitIncomingBroadcastReceiver, + IntentFilter("${packageName}.${ACTION_ENDED_CALL_INCOMING}") + ) } + private fun wakeLockRequest(duration: Long) { - @SuppressLint("MissingPermission") - override fun onReceive(context: Context, intent: Intent) { - val callkitNotificationManager = CallkitNotificationManager(context) - val action = intent.action ?: return - val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) ?: return - when (action) { - "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" -> { - try { - callkitNotificationManager.showIncomingNotification(data) - sendEventFlutter(CallkitConstants.ACTION_CALL_INCOMING, data) - addCall(context, Data.fromBundle(data)) - if (callkitNotificationManager.incomingChannelEnabled()) { - val soundPlayerServiceIntent = - Intent(context, CallkitSoundPlayerService::class.java) - soundPlayerServiceIntent.putExtras(data) - context.startService(soundPlayerServiceIntent) - } - } catch (error: Exception) { - Log.e(TAG, null, error) - } - } - "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" -> { - try { - sendEventFlutter(CallkitConstants.ACTION_CALL_START, data) - addCall(context, Data.fromBundle(data), true) - } catch (error: Exception) { - Log.e(TAG, null, error) - } - } - "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" -> { - try { - sendEventFlutter(CallkitConstants.ACTION_CALL_ACCEPT, data) - context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) - callkitNotificationManager.clearIncomingNotification(data, true) - addCall(context, Data.fromBundle(data), true) - } catch (error: Exception) { - Log.e(TAG, null, error) - } - } - "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" -> { - try { - sendEventFlutter(CallkitConstants.ACTION_CALL_DECLINE, data) - context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) - callkitNotificationManager.clearIncomingNotification(data, false) - removeCall(context, Data.fromBundle(data)) - } catch (error: Exception) { - Log.e(TAG, null, error) - } - } - "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" -> { - try { - sendEventFlutter(CallkitConstants.ACTION_CALL_ENDED, data) - context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) - callkitNotificationManager.clearIncomingNotification(data, false) - removeCall(context, Data.fromBundle(data)) - } catch (error: Exception) { - Log.e(TAG, null, error) - } + val pm = applicationContext.getSystemService(POWER_SERVICE) as PowerManager + val wakeLock = pm.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, + "Callkit:PowerManager" + ) + wakeLock.acquire(duration) + } + + private fun transparentStatusAndNavigation() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + setWindowFlag( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, true + ) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setWindowFlag( + (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION), false + ) + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + } + } + + private fun setWindowFlag(bits: Int, on: Boolean) { + val win: Window = window + val winParams: WindowManager.LayoutParams = win.attributes + if (on) { + winParams.flags = winParams.flags or bits + } else { + winParams.flags = winParams.flags and bits.inv() + } + win.attributes = winParams + } + + + private fun incomingData(intent: Intent) { + val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) + if (data == null) finish() + + val textColor = data?.getString(CallkitConstants.EXTRA_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 + + val avatarUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") + if (avatarUrl != null && avatarUrl.isNotEmpty()) { + ivAvatar.visibility = View.VISIBLE + val headers = data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + getPicassoInstance(this@CallkitIncomingActivity, headers) + .load(avatarUrl) + .placeholder(R.drawable.ic_default_avatar) + .error(R.drawable.ic_default_avatar) + .into(ivAvatar) + } + + val callType = data?.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0) ?: 0 + if (callType > 0) { + ivAcceptCall.setImageResource(R.drawable.ic_video) + } + val duration = data?.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L) ?: 0L + wakeLockRequest(duration) + + finishTimeout(data, duration) + + val textAccept = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "") + tvAccept.text = if (TextUtils.isEmpty(textAccept)) getString(R.string.text_accept) else textAccept + 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)) + } catch (error: Exception) { + } + var backgroundUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, "") + if (backgroundUrl != null && backgroundUrl.isNotEmpty()) { + if (!backgroundUrl.startsWith("http://", true) && !backgroundUrl.startsWith("https://", true)){ + backgroundUrl = String.format("file:///android_asset/flutter_assets/%s", backgroundUrl) } - "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" -> { - try { - sendEventFlutter(CallkitConstants.ACTION_CALL_TIMEOUT, data) - context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) - if (data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, true)) { - callkitNotificationManager.showMissCallNotification(data) - } - removeCall(context, Data.fromBundle(data)) - } catch (error: Exception) { - Log.e(TAG, null, error) - } + val headers = data?.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + getPicassoInstance(this@CallkitIncomingActivity, headers) + .load(backgroundUrl) + .placeholder(R.drawable.transparent) + .error(R.drawable.transparent) + .into(ivBackground) + } + } + + private fun finishTimeout(data: Bundle?, duration: Long) { + val currentSystemTime = System.currentTimeMillis() + val timeStartCall = + data?.getLong(CallkitNotificationManager.EXTRA_TIME_START_CALL, currentSystemTime) + ?: currentSystemTime + + val timeOut = duration - abs(currentSystemTime - timeStartCall) + Handler(Looper.getMainLooper()).postDelayed({ + if (!isFinishing) { + finishTask() } - "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" -> { - try { - callkitNotificationManager.clearMissCallNotification(data) - sendEventFlutter(CallkitConstants.ACTION_CALL_CALLBACK, data) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - val closeNotificationPanel = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) - context.sendBroadcast(closeNotificationPanel) + }, timeOut) + } + + private fun initView() { + ivBackground = findViewById(R.id.ivBackground) + llBackgroundAnimation = findViewById(R.id.llBackgroundAnimation) + llBackgroundAnimation.layoutParams.height = + Utils.getScreenWidth() + Utils.getStatusBarHeight(this@CallkitIncomingActivity) + llBackgroundAnimation.startRippleAnimation() + + tvNameCaller = findViewById(R.id.tvNameCaller) + tvNumber = findViewById(R.id.tvNumber) + ivLogo = findViewById(R.id.ivLogo) + ivAvatar = findViewById(R.id.ivAvatar) + + llAction = findViewById(R.id.llAction) + + val params = llAction.layoutParams as MarginLayoutParams + params.setMargins(0, 0, 0, Utils.getNavigationBarHeight(this@CallkitIncomingActivity)) + llAction.layoutParams = params + + ivAcceptCall = findViewById(R.id.ivAcceptCall) + tvAccept = findViewById(R.id.tvAccept) + ivDeclineCall = findViewById(R.id.ivDeclineCall) + tvDecline = findViewById(R.id.tvDecline) + animateAcceptCall() + + ivAcceptCall.setOnClickListener { + onAcceptClick() + } + ivDeclineCall.setOnClickListener { + onDeclineClick() + } + } + + private fun animateAcceptCall() { + val shakeAnimation = + AnimationUtils.loadAnimation(this@CallkitIncomingActivity, R.anim.shake_anim) + ivAcceptCall.animation = shakeAnimation + } + + + private fun onAcceptClick() { + val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) + val acceptIntent = TransparentActivity.getIntent(this, CallkitConstants.ACTION_CALL_ACCEPT, data) + startActivity(acceptIntent) + + dismissKeyguard() + finish() + } + + private fun dismissKeyguard() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + keyguardManager.requestDismissKeyguard(this, null) + } + } + + private fun onDeclineClick() { + val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) + val intent = CallkitIncomingBroadcastReceiver.getIntentDecline(this@CallkitIncomingActivity, data) + sendBroadcast(intent) + finishTask() + } + + private fun finishDelayed() { + Handler(Looper.getMainLooper()).postDelayed({ + finishTask() + }, 1000) + } + + private fun finishTask() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + finishAndRemoveTask() + } else { + finish() + } + } + + private fun getPicassoInstance(context: Context, headers: HashMap): Picasso { + val client = OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder() + for ((key, value) in headers) { + newRequestBuilder.addHeader(key, value.toString()) } - } catch (error: Exception) { - Log.e(TAG, null, error) + chain.proceed(newRequestBuilder.build()) } - } - } + .build() + return Picasso.Builder(context) + .downloader(OkHttp3Downloader(client)) + .build() } - private fun sendEventFlutter(event: String, data: Bundle) { - 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, - "" - ), - ) - 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), - ) - 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 - ) - FlutterCallkitIncomingPlugin.sendEvent(event, forwardData) + override fun onDestroy() { + unregisterReceiver(endedCallkitIncomingBroadcastReceiver) + super.onDestroy() } -} \ No newline at end of file + + override fun onBackPressed() {} +} 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/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': From 5b4e98970a87b3265755a9a13cdcbe96e9c72441 Mon Sep 17 00:00:00 2001 From: Diego Nuez Date: Tue, 7 Nov 2023 17:52:32 +0100 Subject: [PATCH 03/15] fix(): fix typos --- .../hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt | 2 +- .../CallkitIncomingBroadcastReceiver.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d5b5a409..1a171a91 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 @@ -158,7 +158,7 @@ class CallkitIncomingActivity : Activity() { val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) if (data == null) finish() - val textColor = data?.getString(CallkitConstants.EXTRA_TEXT_COLOR, "#ffffff") + 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, "") 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 d5b5a409..1a171a91 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 @@ -158,7 +158,7 @@ class CallkitIncomingActivity : Activity() { val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) if (data == null) finish() - val textColor = data?.getString(CallkitConstants.EXTRA_TEXT_COLOR, "#ffffff") + 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, "") From bc5bd6eaf2e0b227770cc3996810bcdc41ca1a4f Mon Sep 17 00:00:00 2001 From: Diego Nuez Date: Tue, 7 Nov 2023 17:57:52 +0100 Subject: [PATCH 04/15] feat(): add logs --- .../hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1a171a91..82e2a7c6 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 @@ -159,7 +159,7 @@ class CallkitIncomingActivity : Activity() { if (data == null) finish() val textColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, "#ffffff") - + println("TEXT COLOR = ${textColor}"); tvNameCaller.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") tvNumber.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "") @@ -201,6 +201,7 @@ class CallkitIncomingActivity : Activity() { tvAccept.setTextColor(Color.parseColor(textColor)) tvDecline.setTextColor(Color.parseColor(textColor)) } catch (error: Exception) { + println(error.message); } val backgroundColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, "#0955fa") From 1da643437314c7e9e4f755834580f488c84b7633 Mon Sep 17 00:00:00 2001 From: Diego Nuez Date: Tue, 7 Nov 2023 18:11:58 +0100 Subject: [PATCH 05/15] fix(): fix typos and restore CallkitIncomingBroadcastReceiver.kt --- .idea/misc.xml | 1 - .../CallkitIncomingActivity.kt | 2 - .../CallkitIncomingBroadcastReceiver.kt | 472 ++++++------------ 3 files changed, 166 insertions(+), 309 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index f4f6d798..bea9a163 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - 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 82e2a7c6..c44181cc 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 @@ -159,7 +159,6 @@ class CallkitIncomingActivity : Activity() { if (data == null) finish() val textColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, "#ffffff") - println("TEXT COLOR = ${textColor}"); tvNameCaller.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") tvNumber.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "") @@ -201,7 +200,6 @@ class CallkitIncomingActivity : Activity() { tvAccept.setTextColor(Color.parseColor(textColor)) tvDecline.setTextColor(Color.parseColor(textColor)) } catch (error: Exception) { - println(error.message); } val backgroundColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, "#0955fa") 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 1a171a91..dc69917b 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 @@ -1,336 +1,196 @@ package com.hiennv.flutter_callkit_incoming -import android.app.Activity -import android.app.ActivityManager -import android.app.KeyguardManager +import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ActivityInfo -import android.graphics.Color import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.View -import android.view.Window -import android.view.WindowManager -import android.view.animation.AnimationUtils -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout -import com.squareup.picasso.Picasso -import de.hdodenhof.circleimageview.CircleImageView -import kotlin.math.abs -import okhttp3.OkHttpClient -import com.squareup.picasso.OkHttp3Downloader -import android.view.ViewGroup.MarginLayoutParams -import android.os.PowerManager -import android.text.TextUtils import android.util.Log - -class CallkitIncomingActivity : Activity() { +class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { companion object { + private const val TAG = "CallkitIncomingReceiver" - private const val ACTION_ENDED_CALL_INCOMING = - "com.hiennv.flutter_callkit_incoming.ACTION_ENDED_CALL_INCOMING" - - fun getIntent(context: Context, data: Bundle) = Intent(CallkitConstants.ACTION_CALL_INCOMING).apply { - action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" - putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - fun getIntentEnded(context: Context, isAccepted: Boolean): Intent { - val intent = Intent("${context.packageName}.${ACTION_ENDED_CALL_INCOMING}") - intent.putExtra("ACCEPTED", isAccepted) - return intent - } - } - - inner class EndedCallkitIncomingBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (!isFinishing) { - val isAccepted = intent.getBooleanExtra("ACCEPTED", false) - if (isAccepted) { - finishDelayed() - } else { - finishTask() - } + 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) } - } - } - - private var endedCallkitIncomingBroadcastReceiver = EndedCallkitIncomingBroadcastReceiver() - - private lateinit var ivBackground: ImageView - private lateinit var llBackgroundAnimation: RippleRelativeLayout - - private lateinit var tvNameCaller: TextView - private lateinit var tvNumber: TextView - private lateinit var ivLogo: ImageView - private lateinit var ivAvatar: CircleImageView - - private lateinit var llAction: LinearLayout - private lateinit var ivAcceptCall: ImageView - private lateinit var tvAccept: TextView - - private lateinit var ivDeclineCall: ImageView - private lateinit var tvDecline: TextView - - @Suppress("DEPRECATION") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - requestedOrientation = if (!Utils.isTablet(this@CallkitIncomingActivity)) { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - }else { - ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - setTurnScreenOn(true) - setShowWhenLocked(true) - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) - window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) - window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) - } - transparentStatusAndNavigation() - setContentView(R.layout.activity_callkit_incoming) - initView() - incomingData(intent) - registerReceiver( - endedCallkitIncomingBroadcastReceiver, - IntentFilter("${packageName}.${ACTION_ENDED_CALL_INCOMING}") - ) - } - - private fun wakeLockRequest(duration: Long) { - - val pm = applicationContext.getSystemService(POWER_SERVICE) as PowerManager - val wakeLock = pm.newWakeLock( - PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, - "Callkit:PowerManager" - ) - wakeLock.acquire(duration) - } - private fun transparentStatusAndNavigation() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - setWindowFlag( - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, true - ) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setWindowFlag( - (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION), false - ) - window.statusBarColor = Color.TRANSPARENT - window.navigationBarColor = Color.TRANSPARENT - } - } - - private fun setWindowFlag(bits: Int, on: Boolean) { - val win: Window = window - val winParams: WindowManager.LayoutParams = win.attributes - if (on) { - winParams.flags = winParams.flags or bits - } else { - winParams.flags = winParams.flags and bits.inv() - } - win.attributes = winParams - } - - - private fun incomingData(intent: Intent) { - 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 - - val avatarUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") - if (avatarUrl != null && avatarUrl.isNotEmpty()) { - ivAvatar.visibility = View.VISIBLE - val headers = data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap - getPicassoInstance(this@CallkitIncomingActivity, headers) - .load(avatarUrl) - .placeholder(R.drawable.ic_default_avatar) - .error(R.drawable.ic_default_avatar) - .into(ivAvatar) - } - - val callType = data?.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0) ?: 0 - if (callType > 0) { - ivAcceptCall.setImageResource(R.drawable.ic_video) - } - val duration = data?.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L) ?: 0L - wakeLockRequest(duration) - - finishTimeout(data, duration) - - val textAccept = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "") - tvAccept.text = if (TextUtils.isEmpty(textAccept)) getString(R.string.text_accept) else textAccept - 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)) - } catch (error: Exception) { - } - var backgroundUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, "") - if (backgroundUrl != null && backgroundUrl.isNotEmpty()) { - if (!backgroundUrl.startsWith("http://", true) && !backgroundUrl.startsWith("https://", true)){ - backgroundUrl = String.format("file:///android_asset/flutter_assets/%s", backgroundUrl) + 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) } - val headers = data?.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap - getPicassoInstance(this@CallkitIncomingActivity, headers) - .load(backgroundUrl) - .placeholder(R.drawable.transparent) - .error(R.drawable.transparent) - .into(ivBackground) - } - } - - private fun finishTimeout(data: Bundle?, duration: Long) { - val currentSystemTime = System.currentTimeMillis() - val timeStartCall = - data?.getLong(CallkitNotificationManager.EXTRA_TIME_START_CALL, currentSystemTime) - ?: currentSystemTime - val timeOut = duration - abs(currentSystemTime - timeStartCall) - Handler(Looper.getMainLooper()).postDelayed({ - if (!isFinishing) { - finishTask() + 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) } - }, timeOut) - } - - private fun initView() { - ivBackground = findViewById(R.id.ivBackground) - llBackgroundAnimation = findViewById(R.id.llBackgroundAnimation) - llBackgroundAnimation.layoutParams.height = - Utils.getScreenWidth() + Utils.getStatusBarHeight(this@CallkitIncomingActivity) - llBackgroundAnimation.startRippleAnimation() - - tvNameCaller = findViewById(R.id.tvNameCaller) - tvNumber = findViewById(R.id.tvNumber) - ivLogo = findViewById(R.id.ivLogo) - ivAvatar = findViewById(R.id.ivAvatar) - - llAction = findViewById(R.id.llAction) - - val params = llAction.layoutParams as MarginLayoutParams - params.setMargins(0, 0, 0, Utils.getNavigationBarHeight(this@CallkitIncomingActivity)) - llAction.layoutParams = params - - ivAcceptCall = findViewById(R.id.ivAcceptCall) - tvAccept = findViewById(R.id.tvAccept) - ivDeclineCall = findViewById(R.id.ivDeclineCall) - tvDecline = findViewById(R.id.tvDecline) - animateAcceptCall() - - ivAcceptCall.setOnClickListener { - onAcceptClick() - } - ivDeclineCall.setOnClickListener { - onDeclineClick() - } - } - private fun animateAcceptCall() { - val shakeAnimation = - AnimationUtils.loadAnimation(this@CallkitIncomingActivity, R.anim.shake_anim) - ivAcceptCall.animation = shakeAnimation - } - - - private fun onAcceptClick() { - val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) - val acceptIntent = TransparentActivity.getIntent(this, CallkitConstants.ACTION_CALL_ACCEPT, data) - startActivity(acceptIntent) + 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) + } - dismissKeyguard() - finish() - } + 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) + } - private fun dismissKeyguard() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - keyguardManager.requestDismissKeyguard(this, null) - } - } + 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) + } - private fun onDeclineClick() { - val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) - val intent = CallkitIncomingBroadcastReceiver.getIntentDecline(this@CallkitIncomingActivity, data) - sendBroadcast(intent) - finishTask() - } + 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) + } - private fun finishDelayed() { - Handler(Looper.getMainLooper()).postDelayed({ - finishTask() - }, 1000) + 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) + } } - private fun finishTask() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - finishAndRemoveTask() - } else { - finish() - } - } - private fun getPicassoInstance(context: Context, headers: HashMap): Picasso { - val client = OkHttpClient.Builder() - .addNetworkInterceptor { chain -> - val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder() - for ((key, value) in headers) { - newRequestBuilder.addHeader(key, value.toString()) + @SuppressLint("MissingPermission") + override fun onReceive(context: Context, intent: Intent) { + val callkitNotificationManager = CallkitNotificationManager(context) + val action = intent.action ?: return + val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) ?: return + when (action) { + "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" -> { + try { + callkitNotificationManager.showIncomingNotification(data) + sendEventFlutter(CallkitConstants.ACTION_CALL_INCOMING, data) + addCall(context, Data.fromBundle(data)) + if (callkitNotificationManager.incomingChannelEnabled()) { + val soundPlayerServiceIntent = + Intent(context, CallkitSoundPlayerService::class.java) + soundPlayerServiceIntent.putExtras(data) + context.startService(soundPlayerServiceIntent) } - chain.proceed(newRequestBuilder.build()) + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" -> { + try { + sendEventFlutter(CallkitConstants.ACTION_CALL_START, data) + addCall(context, Data.fromBundle(data), true) + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" -> { + try { + sendEventFlutter(CallkitConstants.ACTION_CALL_ACCEPT, data) + context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) + callkitNotificationManager.clearIncomingNotification(data, true) + addCall(context, Data.fromBundle(data), true) + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" -> { + try { + sendEventFlutter(CallkitConstants.ACTION_CALL_DECLINE, data) + context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) + callkitNotificationManager.clearIncomingNotification(data, false) + removeCall(context, Data.fromBundle(data)) + } catch (error: Exception) { + Log.e(TAG, null, error) } - .build() - return Picasso.Builder(context) - .downloader(OkHttp3Downloader(client)) - .build() + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" -> { + try { + sendEventFlutter(CallkitConstants.ACTION_CALL_ENDED, data) + context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) + callkitNotificationManager.clearIncomingNotification(data, false) + removeCall(context, Data.fromBundle(data)) + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" -> { + try { + sendEventFlutter(CallkitConstants.ACTION_CALL_TIMEOUT, data) + context.stopService(Intent(context, CallkitSoundPlayerService::class.java)) + if (data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, true)) { + callkitNotificationManager.showMissCallNotification(data) + } + removeCall(context, Data.fromBundle(data)) + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" -> { + try { + callkitNotificationManager.clearMissCallNotification(data) + sendEventFlutter(CallkitConstants.ACTION_CALL_CALLBACK, data) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + val closeNotificationPanel = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) + context.sendBroadcast(closeNotificationPanel) + } + } catch (error: Exception) { + Log.e(TAG, null, error) + } + } + } } - override fun onDestroy() { - unregisterReceiver(endedCallkitIncomingBroadcastReceiver) - super.onDestroy() + private fun sendEventFlutter(event: String, data: Bundle) { + 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, ""), + "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), + ) + 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 + ) + FlutterCallkitIncomingPlugin.sendEvent(event, forwardData) } - - override fun onBackPressed() {} -} +} \ No newline at end of file From 0a07804d1af15ef8a6d17cbd9f5820305db03357 Mon Sep 17 00:00:00 2001 From: Diego Nuez Date: Tue, 7 Nov 2023 18:19:02 +0100 Subject: [PATCH 06/15] fix(README.md): fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 973d1015..f06eb52f 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Ca | **`ringtonePath`** | Add file to root project xcode `/ios/Runner/Ringtone.caf` and Copy Bundle Resources(Build Phases) |`Ringtone.caf`
`system_ringtone_default`
using ringtone default of the phone| -1. Source code +5. Source code ``` please checkout repo github From a2624fdc3a7b9a9a2afec0d1cf4dfeae62883872 Mon Sep 17 00:00:00 2001 From: diegonuja Date: Wed, 8 Nov 2023 09:08:29 +0100 Subject: [PATCH 07/15] Update CallkitConstants.kt --- .../com/hiennv/flutter_callkit_incoming/CallkitConstants.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a9ba770f..842eadba 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 @@ -52,11 +52,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_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 +} From 90a8a7cfb887f5c8c745720878a7e7c1a5bbc6b0 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sat, 25 Nov 2023 21:58:12 +0100 Subject: [PATCH 08/15] feature: extras included in handle object --- ios/Classes/Call.swift | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/ios/Classes/Call.swift b/ios/Classes/Call.swift index 8228547e..b53426d6 100644 --- a/ios/Classes/Call.swift +++ b/ios/Classes/Call.swift @@ -272,7 +272,36 @@ public class Call: NSObject { } func getEncryptHandle() -> String { - return String(format: "{\"nameCaller\":\"%@\", \"handle\":\"%@\"}", nameCaller, handle).encryptHandle() + 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) + + // Convert the JSON data to a string + let mapString: String = String(data: mapData, encoding: .utf8) ?? "" + + print("encrypting call data -> " + mapString) + + return mapString.encryptHandle() + } catch { + print("error encrypting call data") + return String(format: "{\"nameCaller\":\"%@\", \"handle\":\"%@\"}", nameCaller, handle).encryptHandle() + } + } From 5a9c979e03929d21e65f5f701d507544b50b33bf Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sat, 25 Nov 2023 22:11:03 +0100 Subject: [PATCH 09/15] feature: not needed print --- ios/Classes/Call.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/Classes/Call.swift b/ios/Classes/Call.swift index b53426d6..b3b30c70 100644 --- a/ios/Classes/Call.swift +++ b/ios/Classes/Call.swift @@ -291,11 +291,8 @@ public class Call: NSObject { let mapData = try JSONSerialization.data(withJSONObject: map, options: .prettyPrinted) - // Convert the JSON data to a string let mapString: String = String(data: mapData, encoding: .utf8) ?? "" - - print("encrypting call data -> " + mapString) - + return mapString.encryptHandle() } catch { print("error encrypting call data") From 45680a49ed639ae906c0d2f8b8b99ba70dbced97 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 26 Nov 2023 05:10:43 +0100 Subject: [PATCH 10/15] feature: normal handle, no base64 values --- ios/Classes/Call.swift | 7 ++++++ ios/Classes/StringUtils.swift | 34 +++++++++++++++++++++++++++++ lib/entities/call_kit_params.dart | 2 ++ lib/entities/call_kit_params.g.dart | 2 ++ 4 files changed, 45 insertions(+) diff --git a/ios/Classes/Call.swift b/ios/Classes/Call.swift index b3b30c70..d32933b1 100644 --- a/ios/Classes/Call.swift +++ b/ios/Classes/Call.swift @@ -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 ?? [:] @@ -264,6 +267,7 @@ public class Call: NSObject { "handle": handle, "avatar": avatar, "type": type, + "normalHandle": normalHandle, "duration": duration, "extra": extra, "ios": ios @@ -272,6 +276,9 @@ public class Call: NSObject { } func getEncryptHandle() -> String { + if (normalHandle > 0) { + return handle + } do { var map: [String: Any] = [:] diff --git a/ios/Classes/StringUtils.swift b/ios/Classes/StringUtils.swift index f0d9058c..b753ba14 100644 --- a/ios/Classes/StringUtils.swift +++ b/ios/Classes/StringUtils.swift @@ -48,6 +48,13 @@ extension String { } public func getDecryptHandle() -> [String: Any] { + if (!self.isBase64Encoded()) { + print("normal value") + var map: [String: Any] = [:] + map["handle"] = self + return map + } + print("base64 value") if let data = self.decryptHandle().data(using: .utf8) { do { return try (JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])! @@ -58,4 +65,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/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, From 6256f8cc3292345c8fdd282fd820da28cde2444b Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 26 Nov 2023 13:18:39 +0100 Subject: [PATCH 11/15] feature: fix optional value --- ios/Classes/StringUtils.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/Classes/StringUtils.swift b/ios/Classes/StringUtils.swift index b753ba14..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 { @@ -49,12 +48,10 @@ extension String { public func getDecryptHandle() -> [String: Any] { if (!self.isBase64Encoded()) { - print("normal value") var map: [String: Any] = [:] map["handle"] = self return map } - print("base64 value") if let data = self.decryptHandle().data(using: .utf8) { do { return try (JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])! From ea4920f30302389b05f5eac3dace53bc4534fd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Thu, 14 Dec 2023 12:48:06 +0100 Subject: [PATCH 12/15] silence events platform method added --- .../CallkitIncomingBroadcastReceiver.kt | 3 +++ .../FlutterCallkitIncomingPlugin.kt | 5 +++++ .../SwiftFlutterCallkitIncomingPlugin.swift | 21 +++++++++++++++++-- lib/flutter_callkit_incoming.dart | 10 +++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) 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..dc0b9637 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,6 +12,7 @@ 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 { @@ -150,6 +151,8 @@ 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( 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..882eaf55 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 @@ -243,6 +243,11 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA "getDevicePushTokenVoIP" -> { result.success("") } + "silenceEvents" -> { + val silence = call.arguments as? Boolean ?: false + CallkitIncomingBroadcastReceiver.silenceEvents = silence + result.success("") + } "requestNotificationPermission" -> { val map = buildMap { val args = call.arguments diff --git a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift index 5c45ba69..dcaa48ef 100644 --- a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift +++ b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift @@ -35,11 +35,17 @@ 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 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 ?? [:]) + } } } @@ -180,6 +186,17 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi case "getDevicePushTokenVoIP": result(self.getDevicePushTokenVoIP()) break; + case "silenceEvents": + guard let silence = call.arguments as? Bool else { + result("OK") + return + } + + print("silence events") + print(silence) + self.silenceEvents = silence + result("OK") + break; default: result(FlutterMethodNotImplemented) } diff --git a/lib/flutter_callkit_incoming.dart b/lib/flutter_callkit_incoming.dart index bd7625c3..064d8e9c 100644 --- a/lib/flutter_callkit_incoming.dart +++ b/lib/flutter_callkit_incoming.dart @@ -111,6 +111,16 @@ class FlutterCallkitIncoming { return await _channel.invokeMethod("getDevicePushTokenVoIP"); } + /// Silence CallKit events + static Future silenceEvents() async { + return await _channel.invokeMethod("silenceEvents", true); + } + + /// Unsilence CallKit events + static Future unsilenceEvents() async { + return await _channel.invokeMethod("silenceEvents", false); + } + /// Request permisstion show notification for Android(13) /// Only Android: show request permission post notification for Android 13+ static Future requestNotificationPermission(dynamic data) async { From 360129614e3dad6b23e57058949677c480f66ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Mon, 18 Dec 2023 11:01:25 +0100 Subject: [PATCH 13/15] tweak --- ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift index dcaa48ef..caad4dda 100644 --- a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift +++ b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift @@ -47,6 +47,7 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi handler?.send(event, body ?? [:]) } } + } @objc public func sendEventCustom(_ event: String, body: NSDictionary?) { @@ -192,8 +193,6 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi return } - print("silence events") - print(silence) self.silenceEvents = silence result("OK") break; From 460143d45dd733cfcba7cce8d1a15ba3610617b9 Mon Sep 17 00:00:00 2001 From: Hien Nguyen Date: Fri, 5 Jan 2024 14:43:22 +0700 Subject: [PATCH 14/15] Android update Telecom --- .idea/misc.xml | 4 + android/src/main/AndroidManifest.xml | 11 + .../hiennv/flutter_callkit_incoming/Call.kt | 7 + .../CallkitConstants.kt | 3 + .../CallkitIncomingBroadcastReceiver.kt | 156 +++++++----- .../CallkitSoundPlayerService.kt | 5 +- .../FlutterCallkitIncomingPlugin.kt | 113 ++++++++- .../telecom/TelecomConnection.kt | 199 +++++++++++++++ .../telecom/TelecomConnectionService.kt | 149 +++++++++++ .../telecom/TelecomUtils.kt | 240 ++++++++++++++++++ example/ios/Podfile | 6 + example/ios/Podfile.lock | 46 ++-- example/ios/Runner/AppDelegate.swift | 2 +- .../SwiftFlutterCallkitIncomingPlugin.swift | 14 +- 14 files changed, 848 insertions(+), 107 deletions(-) create mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt create mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt create mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index bea9a163..f1fbd79c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + @@ -5,4 +6,7 @@ + + \ No newline at end of file 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 fdbd2f25..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 @@ -73,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 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 842eadba..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" 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 76a65d92..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 @@ -15,52 +15,64 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { 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) + } } @@ -77,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) } @@ -85,6 +97,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_START, data) @@ -93,6 +106,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_ACCEPT, data) @@ -103,6 +117,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_DECLINE, data) @@ -113,6 +128,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_ENDED, data) @@ -123,6 +139,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" -> { try { sendEventFlutter(CallkitConstants.ACTION_CALL_TIMEOUT, data) @@ -135,6 +152,7 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { Log.e(TAG, null, error) } } + "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" -> { try { callkitNotificationManager.clearMissCallNotification(data) @@ -151,48 +169,48 @@ class CallkitIncomingBroadcastReceiver : BroadcastReceiver() { } private fun sendEventFlutter(event: String, data: Bundle) { - if(silenceEvents) return + 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, ""), - "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, - "" - ), + "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 882eaf55..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,19 +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 @@ -257,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, "") @@ -283,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 { @@ -314,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/example/ios/Podfile b/example/ios/Podfile index 313ea4a1..99fe5280 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -37,5 +37,11 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + xcconfig_path = config.base_configuration_reference.real_path + xcconfig = File.read(xcconfig_path) + xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") + File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7a3b3c1d..256b356e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - CryptoSwift (1.7.1) + - CryptoSwift (1.8.0) - Firebase/CoreOnly (10.7.0): - FirebaseCore (= 10.7.0) - Firebase/Messaging (10.7.0): @@ -16,9 +16,9 @@ PODS: - FirebaseCoreInternal (~> 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..3cafef9a 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -75,7 +75,7 @@ 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() } } diff --git a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift index caad4dda..02d721ea 100644 --- a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift +++ b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift @@ -196,6 +196,18 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi 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: result(FlutterMethodNotImplemented) } @@ -494,7 +506,7 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi action.fail() return } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { self.configurAudioSession() } call.hasConnectDidChange = { [weak self] in From c319c8ed5203d62b25335187e27ba62addd2622b Mon Sep 17 00:00:00 2001 From: Hien Nguyen Date: Fri, 5 Jan 2024 18:11:30 +0700 Subject: [PATCH 15/15] Version 2.0.1 --- CHANGELOG.md | 10 +++ README.md | 69 +++++++++++++++++++ example/ios/Runner/AppDelegate.swift | 16 ++--- ios/Classes/Call.swift | 10 +-- .../SwiftFlutterCallkitIncomingPlugin.swift | 3 +- pubspec.yaml | 2 +- 6 files changed, 95 insertions(+), 15 deletions(-) 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 f06eb52f..a2679a3e 100644 --- a/README.md +++ b/README.md @@ -326,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 diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index aa05f104..1eb1a24e 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -83,8 +83,8 @@ import flutter_callkit_incoming // Func Call api for Accept func onAccept(_ call: Call) { - let json = ["action": "ACCEPT", "uuid": "XXX-XXX-XXX-XXX-ACCEPT"] - print("CCCC: onAccept") + 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): @@ -98,8 +98,8 @@ import flutter_callkit_incoming // Func Call API for Decline func onDecline(_ call: Call) { - let json = ["action": "DECLINE", "uuid": "XXX-XXX-XXX-XXX-DECLINE"] - print("CCCC: onDecline") + 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): @@ -112,8 +112,8 @@ import flutter_callkit_incoming } func onEnd(_ call: Call) { - let json = ["action": "END", "uuid": "XXX-XXX-XXX-XXX-END"] - print("CCCC: onEnd") + 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): @@ -126,8 +126,8 @@ import flutter_callkit_incoming } func onTimeOut(_ call: Call) { - let json = ["action": "TIMEOUT", "uuid": "XXX-XXX-XXX-XXX-TIMEOUT"] - print("CCCC: onTimeOut") + 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): diff --git a/ios/Classes/Call.swift b/ios/Classes/Call.swift index d32933b1..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)? @@ -240,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, diff --git a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift index c1e8d5f0..97b3860c 100644 --- a/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift +++ b/ios/Classes/SwiftFlutterCallkitIncomingPlugin.swift @@ -517,7 +517,8 @@ public class SwiftFlutterCallkitIncomingPlugin: NSObject, FlutterPlugin, CXProvi action.fail() return } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { + self.configurAudioSession() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) { self.configurAudioSession() } call.hasConnectDidChange = { [weak self] in 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