From bfe0b6d1e6c660a3592aff0cc027d7087d7b5c50 Mon Sep 17 00:00:00 2001 From: Hien Nguyen Date: Wed, 17 Apr 2024 16:58:26 +0700 Subject: [PATCH 1/2] feat: Remove TelecomConnection Framework --- .../CallkitNotificationManager.kt | 332 +++++++++++------- .../FlutterCallkitIncomingPlugin.kt | 63 +--- .../telecom/TelecomConnection.kt | 199 ----------- .../telecom/TelecomConnectionService.kt | 149 -------- .../telecom/TelecomUtils.kt | 240 ------------- 5 files changed, 199 insertions(+), 784 deletions(-) delete mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt delete mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt delete mode 100644 android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt diff --git a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt index e7bf8547..1c9388a0 100644 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt +++ b/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt @@ -1,6 +1,7 @@ package com.hiennv.flutter_callkit_incoming import android.Manifest +import android.annotation.SuppressLint import android.app.Activity import android.app.Notification import android.app.NotificationChannel @@ -21,6 +22,7 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.text.TextUtils +import android.util.Log import android.view.View import android.widget.RemoteViews import androidx.appcompat.app.AlertDialog @@ -51,7 +53,9 @@ class CallkitNotificationManager(private val context: Context) { private var notificationId: Int = 9696 private var dataNotificationPermission: Map = HashMap() + @SuppressLint("MissingPermission") private var targetLoadAvatarDefault = object : Target { + override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) { notificationBuilder.setLargeIcon(bitmap) getNotificationManager().notify(notificationId, notificationBuilder.build()) @@ -64,6 +68,7 @@ class CallkitNotificationManager(private val context: Context) { } } + @SuppressLint("MissingPermission") private var targetLoadAvatarCustomize = object : Target { override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) { notificationViews?.setImageViewBitmap(R.id.ivAvatar, bitmap) @@ -81,19 +86,21 @@ class CallkitNotificationManager(private val context: Context) { } + @SuppressLint("MissingPermission") fun showIncomingNotification(data: Bundle) { data.putLong(EXTRA_TIME_START_CALL, System.currentTimeMillis()) - notificationId = data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + notificationId = + data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() createNotificationChanel( - data.getString( - CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, - "Incoming Call" - ), - data.getString( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, - "Missed Call" - ), + data.getString( + CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, + "Incoming Call" + ), + data.getString( + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, + "Missed Call" + ), ) notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_INCOMING) @@ -107,11 +114,16 @@ class CallkitNotificationManager(private val context: Context) { notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notificationBuilder.setOngoing(true) notificationBuilder.setWhen(0) - notificationBuilder.setTimeoutAfter(data.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L)) + notificationBuilder.setTimeoutAfter( + data.getLong( + CallkitConstants.EXTRA_CALLKIT_DURATION, + 0L + ) + ) notificationBuilder.setOnlyAlertOnce(true) notificationBuilder.setSound(null) notificationBuilder.setFullScreenIntent( - getActivityPendingIntent(notificationId, data), true + getActivityPendingIntent(notificationId, data), true ) notificationBuilder.setContentIntent(getActivityPendingIntent(notificationId, data)) notificationBuilder.setDeleteIntent(getTimeOutPendingIntent(notificationId, data)) @@ -128,30 +140,30 @@ class CallkitNotificationManager(private val context: Context) { val actionColor = data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, "#4CAF50") try { notificationBuilder.color = Color.parseColor(actionColor) - } catch (error: Exception) { + } catch (_: Exception) { } notificationBuilder.setChannelId(NOTIFICATION_CHANNEL_ID_INCOMING) notificationBuilder.priority = NotificationCompat.PRIORITY_MAX val isCustomNotification = - data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false) + data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false) val isCustomSmallExNotification = - data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, false) + data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, false) if (isCustomNotification) { notificationViews = - RemoteViews(context.packageName, R.layout.layout_custom_notification) + RemoteViews(context.packageName, R.layout.layout_custom_notification) initNotificationViews(notificationViews!!, data) if ((Build.MANUFACTURER.equals( - "Samsung", - ignoreCase = true - ) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) || isCustomSmallExNotification + "Samsung", + ignoreCase = true + ) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) || isCustomSmallExNotification ) { notificationSmallViews = - RemoteViews(context.packageName, R.layout.layout_custom_small_ex_notification) + RemoteViews(context.packageName, R.layout.layout_custom_small_ex_notification) initNotificationViews(notificationSmallViews!!, data) } else { notificationSmallViews = - RemoteViews(context.packageName, R.layout.layout_custom_small_notification) + RemoteViews(context.packageName, R.layout.layout_custom_small_notification) initNotificationViews(notificationSmallViews!!, data) } @@ -163,29 +175,34 @@ class CallkitNotificationManager(private val context: Context) { val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") if (avatarUrl != null && avatarUrl.isNotEmpty()) { val headers = - data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap getPicassoInstance(context, headers).load(avatarUrl) - .into(targetLoadAvatarDefault) + .into(targetLoadAvatarDefault) } notificationBuilder.setContentTitle( - data.getString( - CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, - "" - ) + data.getString( + CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, + "" + ) + ) + notificationBuilder.setContentText( + data.getString( + CallkitConstants.EXTRA_CALLKIT_HANDLE, + "" + ) ) - notificationBuilder.setContentText(data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")) val textDecline = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "") val declineAction: NotificationCompat.Action = NotificationCompat.Action.Builder( - R.drawable.ic_decline, - if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline, - getDeclinePendingIntent(notificationId, data) + R.drawable.ic_decline, + if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline, + getDeclinePendingIntent(notificationId, data) ).build() notificationBuilder.addAction(declineAction) val textAccept = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "") val acceptAction: NotificationCompat.Action = NotificationCompat.Action.Builder( - R.drawable.ic_accept, - if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_accept) else textAccept, - getAcceptPendingIntent(notificationId, data) + R.drawable.ic_accept, + if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_accept) else textAccept, + getAcceptPendingIntent(notificationId, data) ).build() notificationBuilder.addAction(acceptAction) } @@ -196,8 +213,8 @@ class CallkitNotificationManager(private val context: Context) { private fun initNotificationViews(remoteViews: RemoteViews, data: Bundle) { remoteViews.setTextViewText( - R.id.tvNameCaller, - data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") + R.id.tvNameCaller, + data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") ) val isShowCallID = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false) if (isShowCallID == true) { @@ -207,47 +224,48 @@ class CallkitNotificationManager(private val context: Context) { ) } remoteViews.setOnClickPendingIntent( - R.id.llDecline, - getDeclinePendingIntent(notificationId, data) + R.id.llDecline, + getDeclinePendingIntent(notificationId, data) ) val textDecline = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "") remoteViews.setTextViewText( - R.id.tvDecline, - if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline + R.id.tvDecline, + if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline ) remoteViews.setOnClickPendingIntent( - R.id.llAccept, - getAcceptPendingIntent(notificationId, data) + R.id.llAccept, + getAcceptPendingIntent(notificationId, data) ) val textAccept = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "") remoteViews.setTextViewText( - R.id.tvAccept, - if (TextUtils.isEmpty(textAccept)) context.getString(R.string.text_accept) else textAccept + R.id.tvAccept, + if (TextUtils.isEmpty(textAccept)) context.getString(R.string.text_accept) else textAccept ) val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") if (avatarUrl != null && avatarUrl.isNotEmpty()) { val headers = - data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap getPicassoInstance(context, headers).load(avatarUrl) - .transform(CircleTransform()) - .into(targetLoadAvatarCustomize) + .transform(CircleTransform()) + .into(targetLoadAvatarCustomize) } } + @SuppressLint("MissingPermission") fun showMissCallNotification(data: Bundle) { - notificationId = data.getInt( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID, - data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 1 + val missedNotificationId = data.getInt( + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID, + data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 1 ) createNotificationChanel( - data.getString( - CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, - "Incoming Call" - ), - data.getString( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, - "Missed Call" - ), + data.getString( + CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME, + "Incoming Call" + ), + data.getString( + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME, + "Missed Call" + ), ) val missedCallSound: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val typeCall = data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, -1) @@ -270,19 +288,20 @@ class CallkitNotificationManager(private val context: Context) { notificationBuilder.setSubText(if (TextUtils.isEmpty(textMissedCall)) context.getString(R.string.text_missed_call) else textMissedCall) notificationBuilder.setSmallIcon(smallIcon) val isCustomNotification = - data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false) + data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false) val count = data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT, 1) if (count > 1) { notificationBuilder.setNumber(count) } if (isCustomNotification) { notificationViews = - RemoteViews(context.packageName, R.layout.layout_custom_miss_notification) + RemoteViews(context.packageName, R.layout.layout_custom_miss_notification) notificationViews?.setTextViewText( - R.id.tvNameCaller, - data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") + R.id.tvNameCaller, + data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "") ) - val isShowCallID = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false) + val isShowCallID = + data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false) if (isShowCallID == true) { notificationViews?.setTextViewText( R.id.tvNumber, @@ -290,61 +309,67 @@ class CallkitNotificationManager(private val context: Context) { ) } notificationViews?.setOnClickPendingIntent( - R.id.llCallback, - getCallbackPendingIntent(notificationId, data) + R.id.llCallback, + getCallbackPendingIntent(notificationId, data) ) val isShowCallback = data.getBoolean( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW, - true + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW, + true ) notificationViews?.setViewVisibility( - R.id.llCallback, - if (isShowCallback) View.VISIBLE else View.GONE + R.id.llCallback, + if (isShowCallback) View.VISIBLE else View.GONE ) - val textCallback = data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "") + val textCallback = + data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "") notificationViews?.setTextViewText( - R.id.tvCallback, - if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback + R.id.tvCallback, + if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback ) val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") if (avatarUrl != null && avatarUrl.isNotEmpty()) { val headers = - data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap getPicassoInstance(context, headers).load(avatarUrl) - .transform(CircleTransform()).into(targetLoadAvatarCustomize) + .transform(CircleTransform()).into(targetLoadAvatarCustomize) } notificationBuilder.setStyle(NotificationCompat.DecoratedCustomViewStyle()) notificationBuilder.setCustomContentView(notificationViews) notificationBuilder.setCustomBigContentView(notificationViews) } else { notificationBuilder.setContentTitle( - data.getString( - CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, - "" - ) + data.getString( + CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, + "" + ) + ) + notificationBuilder.setContentText( + data.getString( + CallkitConstants.EXTRA_CALLKIT_HANDLE, + "" + ) ) - notificationBuilder.setContentText(data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")) val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "") if (avatarUrl != null && avatarUrl.isNotEmpty()) { val headers = - data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap + data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap getPicassoInstance(context, headers).load(avatarUrl) - .into(targetLoadAvatarDefault) + .into(targetLoadAvatarDefault) } val isShowCallback = data.getBoolean( - CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW, - true + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW, + true ) if (isShowCallback) { val textCallback = - data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "") + data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "") val callbackAction: NotificationCompat.Action = NotificationCompat.Action.Builder( - R.drawable.ic_accept, - if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback, - getCallbackPendingIntent(notificationId, data) + R.drawable.ic_accept, + if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback, + getCallbackPendingIntent(notificationId, data) ).build() notificationBuilder.addAction(callbackAction) } @@ -362,10 +387,10 @@ class CallkitNotificationManager(private val context: Context) { } catch (_: Exception) { } val notification = notificationBuilder.build() - getNotificationManager().notify(notificationId, notification) + getNotificationManager().notify(missedNotificationId, notification) Handler(Looper.getMainLooper()).postDelayed({ try { - getNotificationManager().notify(notificationId, notification) + getNotificationManager().notify(missedNotificationId, notification) } catch (_: Exception) { } }, 1000) @@ -374,16 +399,20 @@ class CallkitNotificationManager(private val context: Context) { fun clearIncomingNotification(data: Bundle, isAccepted: Boolean) { context.sendBroadcast(CallkitIncomingActivity.getIntentEnded(context, isAccepted)) - notificationId = data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + notificationId = + data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() getNotificationManager().cancel(notificationId) } fun clearMissCallNotification(data: Bundle) { - notificationId = data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() - getNotificationManager().cancel(notificationId) + val missedNotificationId = data.getInt( + CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID, + data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 1 + ) + getNotificationManager().cancel(missedNotificationId) Handler(Looper.getMainLooper()).postDelayed({ try { - getNotificationManager().cancel(notificationId) + getNotificationManager().cancel(missedNotificationId) } catch (_: Exception) { } }, 1000) @@ -400,8 +429,8 @@ class CallkitNotificationManager(private val context: Context) { } private fun createNotificationChanel( - incomingCallChannelName: String, - missedCallChannelName: String, + incomingCallChannelName: String, + missedCallChannelName: String, ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getNotificationManager().apply { @@ -410,13 +439,13 @@ class CallkitNotificationManager(private val context: Context) { channelCall.setSound(null, null) } else { channelCall = NotificationChannel( - NOTIFICATION_CHANNEL_ID_INCOMING, - incomingCallChannelName, - NotificationManager.IMPORTANCE_HIGH + NOTIFICATION_CHANNEL_ID_INCOMING, + incomingCallChannelName, + NotificationManager.IMPORTANCE_HIGH ).apply { description = "" vibrationPattern = - longArrayOf(0, 1000, 500, 1000, 500) + longArrayOf(0, 1000, 500, 1000, 500) lightColor = Color.RED enableLights(true) enableVibration(true) @@ -430,9 +459,9 @@ class CallkitNotificationManager(private val context: Context) { createNotificationChannel(channelCall) val channelMissedCall = NotificationChannel( - NOTIFICATION_CHANNEL_ID_MISSED, - missedCallChannelName, - NotificationManager.IMPORTANCE_DEFAULT + NOTIFICATION_CHANNEL_ID_MISSED, + missedCallChannelName, + NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "" vibrationPattern = longArrayOf(0, 1000) @@ -448,9 +477,9 @@ class CallkitNotificationManager(private val context: Context) { private fun getAcceptPendingIntent(id: Int, data: Bundle): PendingIntent { val intentTransparent = TransparentActivity.getIntent( - context, - CallkitConstants.ACTION_CALL_ACCEPT, - data + context, + CallkitConstants.ACTION_CALL_ACCEPT, + data ) return PendingIntent.getActivity(context, id, intentTransparent, getFlagPendingIntent()) } @@ -467,9 +496,9 @@ class CallkitNotificationManager(private val context: Context) { private fun getCallbackPendingIntent(id: Int, data: Bundle): PendingIntent { val intentTransparent = TransparentActivity.getIntent( - context, - CallkitConstants.ACTION_CALL_CALLBACK, - data + context, + CallkitConstants.ACTION_CALL_CALLBACK, + data ) return PendingIntent.getActivity(context, id, intentTransparent, getFlagPendingIntent()) } @@ -492,6 +521,7 @@ class CallkitNotificationManager(private val context: Context) { } } + private fun getNotificationManager(): NotificationManagerCompat { return NotificationManagerCompat.from(context) } @@ -499,17 +529,17 @@ class CallkitNotificationManager(private val context: Context) { 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()) - } - chain.proceed(newRequestBuilder.build()) + .addNetworkInterceptor { chain -> + val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder() + for ((key, value) in headers) { + newRequestBuilder.addHeader(key, value.toString()) } - .build() + chain.proceed(newRequestBuilder.build()) + } + .build() return Picasso.Builder(context) - .downloader(OkHttp3Downloader(client)) - .build() + .downloader(OkHttp3Downloader(client)) + .build() } @@ -517,9 +547,11 @@ class CallkitNotificationManager(private val context: Context) { this.dataNotificationPermission = map if (Build.VERSION.SDK_INT > 32) { activity?.let { - ActivityCompat.requestPermissions(it, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - PERMISSION_NOTIFICATION_REQUEST_CODE) + ActivityCompat.requestPermissions( + it, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + PERMISSION_NOTIFICATION_REQUEST_CODE + ) } } } @@ -528,36 +560,60 @@ class CallkitNotificationManager(private val context: Context) { when (requestCode) { PERMISSION_NOTIFICATION_REQUEST_CODE -> { if (grantResults.isNotEmpty() && - grantResults[0] === PackageManager.PERMISSION_GRANTED) { + grantResults[0] === PackageManager.PERMISSION_GRANTED + ) { // allow } else { //deny activity?.let { - if (ActivityCompat.shouldShowRequestPermissionRationale(it, Manifest.permission.POST_NOTIFICATIONS)) { + if (ActivityCompat.shouldShowRequestPermissionRationale( + it, + Manifest.permission.POST_NOTIFICATIONS + ) + ) { //showDialogPermissionRationale() if (this.dataNotificationPermission["rationaleMessagePermission"] != null) { - showDialogMessage(it, this.dataNotificationPermission["rationaleMessagePermission"] as String) { dialog, _ -> + showDialogMessage( + it, + this.dataNotificationPermission["rationaleMessagePermission"] as String + ) { dialog, _ -> dialog?.dismiss() - requestNotificationPermission(activity, this.dataNotificationPermission) + requestNotificationPermission( + activity, + this.dataNotificationPermission + ) } } else { - requestNotificationPermission(activity, this.dataNotificationPermission) + requestNotificationPermission( + activity, + this.dataNotificationPermission + ) } } else { //Open Setting if (this.dataNotificationPermission["postNotificationMessageRequired"] != null) { - showDialogMessage(it, this.dataNotificationPermission["postNotificationMessageRequired"] as String) { dialog, _ -> + showDialogMessage( + it, + this.dataNotificationPermission["postNotificationMessageRequired"] as String + ) { dialog, _ -> dialog?.dismiss() - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", it.packageName, null)) + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", it.packageName, null) + ) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) it.startActivity(intent) } } else { - showDialogMessage(it, it.resources.getString(R.string.text_post_notification_message_required)) { dialog, _ -> + showDialogMessage( + it, + it.resources.getString(R.string.text_post_notification_message_required) + ) { dialog, _ -> dialog?.dismiss() - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", it.packageName, null)) + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", it.packageName, null) + ) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) it.startActivity(intent) } @@ -569,14 +625,18 @@ class CallkitNotificationManager(private val context: Context) { } } - private fun showDialogMessage(activity: Activity?, message: String, okListener: DialogInterface.OnClickListener) { + private fun showDialogMessage( + activity: Activity?, + message: String, + okListener: DialogInterface.OnClickListener + ) { activity?.let { AlertDialog.Builder(it, R.style.DialogTheme) - .setMessage(message) - .setPositiveButton(android.R.string.ok, okListener) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show() + .setMessage(message) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show() } } 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 1478a2d8..9884c468 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 @@ -10,7 +10,6 @@ import android.os.Looper import android.util.Log import androidx.annotation.NonNull 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 @@ -29,9 +28,6 @@ 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 } @@ -78,8 +74,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA eventHandlers.add(WeakReference(handler)) events.setStreamHandler(handler) - telecomUtilities = TelecomUtilities(context) - TelecomUtilities.telecomUtilitiesSingleton = telecomUtilities } } @@ -163,11 +157,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA ) ) - // 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") } @@ -175,11 +164,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA 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") } @@ -199,10 +183,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA ) ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - telecomUtilities.startCall(data) - } - result.success("OK") } @@ -215,11 +195,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } 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") } @@ -232,15 +207,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } 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") } @@ -257,18 +223,10 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA ) ) - 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") } @@ -292,12 +250,6 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } } removeAllCalls(context) - - //Additional safety net - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - telecomUtilities.endAllActiveCalls() - } - result.success("OK") } @@ -332,17 +284,11 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } "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) { @@ -371,10 +317,7 @@ class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityA } 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 { 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 deleted file mode 100644 index 09ca3f84..00000000 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnection.kt +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index 8b3fe766..00000000 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomConnectionService.kt +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index 725ce97e..00000000 --- a/android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/telecom/TelecomUtils.kt +++ /dev/null @@ -1,240 +0,0 @@ -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) - } - } - } -} From a1cfd0eabe1b3bda28c60b27e9b52e5c62577824 Mon Sep 17 00:00:00 2001 From: Hien Nguyen Date: Wed, 17 Apr 2024 17:01:37 +0700 Subject: [PATCH 2/2] ver: 2.0.4 --- CHANGELOG.md | 4 ++++ lib/entities/android_params.g.dart | 3 +-- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8828df..9d7a4ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 +* Removed `Telecom Framework` (Android) +* Fixed hide notification for action `CallBack` (Android) + ## 2.0.3 * Fixed linked func `hideCallkitIncoming` diff --git a/lib/entities/android_params.g.dart b/lib/entities/android_params.g.dart index 223e3e52..0df21f1c 100644 --- a/lib/entities/android_params.g.dart +++ b/lib/entities/android_params.g.dart @@ -21,8 +21,7 @@ AndroidParams _$AndroidParamsFromJson(Map json) => json['incomingCallNotificationChannelName'] as String?, missedCallNotificationChannelName: json['missedCallNotificationChannelName'] as String?, - isShowFullLockedScreen: - json['isShowFullLockedScreen'] as bool?, + isShowFullLockedScreen: json['isShowFullLockedScreen'] as bool?, ); Map _$AndroidParamsToJson(AndroidParams instance) => diff --git a/pubspec.yaml b/pubspec.yaml index 27edd32f..7766bcf6 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.3 +version: 2.0.4 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