Skip to content

Commit 0da822c

Browse files
space auto
1 parent c9a1021 commit 0da822c

29 files changed

+2871
-129
lines changed

android/src/main/AndroidManifest.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@
99
<uses-permission android:name="android.permission.CALL_PHONE" />
1010
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
1111
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
12+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
1213

1314
<application>
1415
<service
1516
android:name=".service.TVConnectionService"
1617
android:exported="true"
17-
android:foregroundServiceType="microphone"
18+
android:foregroundServiceType="microphone|phoneCall"
1819
android:label="@string/connection_service_name"
1920
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
2021
<intent-filter>
2122
<action android:name="android.telecom.ConnectionService" />
2223
</intent-filter>
2324
</service>
2425

26+
<activity
27+
android:name=".ui.IncomingCallActivity"
28+
android:exported="false"
29+
android:theme="@style/TransparentCallTheme"
30+
android:showWhenLocked="true"
31+
android:turnScreenOn="true"
32+
android:launchMode="singleTask" />
33+
2534
<meta-data
2635
android:name="flutterEmbedding"
2736
android:value="2" />

android/src/main/kotlin/com/twilio/twilio_voice/TwilioVoicePlugin.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import android.content.DialogInterface
88
import android.content.Intent
99
import android.content.IntentFilter
1010
import android.content.pm.PackageManager
11+
import android.content.res.Resources
12+
import android.view.ContextThemeWrapper
1113
import android.os.Build
1214
import android.os.Bundle
1315
import android.telecom.CallAudioState
@@ -1330,6 +1332,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
13301332
Log.d(TAG, "onDetachedFromActivityForConfigChanges")
13311333
unregisterReceiver()
13321334
activity = null
1335+
permissionResultHandler.clear()
13331336
}
13341337

13351338
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
@@ -1344,6 +1347,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
13441347
Log.d(TAG, "onDetachedFromActivity")
13451348
unregisterReceiver()
13461349
activity = null
1350+
permissionResultHandler.clear()
13471351
}
13481352
//endregion
13491353

@@ -1623,21 +1627,32 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
16231627
}
16241628

16251629
logEvent("requestPermissionFor$permissionName")
1630+
1631+
permissionResultHandler[requestCode] = onPermissionResult
1632+
16261633
val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity!!, manifestPermission)
16271634
if (shouldShowRationale) {
1635+
var proceedClicked = false
16281636
val clickListener =
1629-
DialogInterface.OnClickListener { _: DialogInterface?, _: Int ->
1637+
DialogInterface.OnClickListener { dialog: DialogInterface?, _: Int ->
1638+
proceedClicked = true
1639+
dialog?.dismiss()
1640+
}
1641+
1642+
val dismissListener = DialogInterface.OnDismissListener { _: DialogInterface? ->
1643+
if (proceedClicked) {
16301644
ActivityCompat.requestPermissions(
16311645
activity!!, arrayOf(manifestPermission), requestCode
16321646
)
1647+
} else {
1648+
logEvent("Request${permissionName}AccessDismissed")
1649+
permissionResultHandler[requestCode]?.invoke(false)
1650+
permissionResultHandler.remove(requestCode)
16331651
}
1634-
val dismissListener = DialogInterface.OnDismissListener { _: DialogInterface? ->
1635-
logEvent("Request" + permissionName + "Access")
16361652
}
16371653
showPermissionRationaleDialog(activity!!, "$permissionName Permissions", description, clickListener, dismissListener)
16381654
} else {
16391655
ActivityCompat.requestPermissions(activity!!, arrayOf(manifestPermission), requestCode)
1640-
permissionResultHandler[requestCode] = onPermissionResult
16411656
}
16421657
}
16431658

@@ -1648,7 +1663,8 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
16481663
onClickListener: DialogInterface.OnClickListener,
16491664
onDismissListener: DialogInterface.OnDismissListener
16501665
) {
1651-
val builder = AlertDialog.Builder(context)
1666+
val themedContext = ContextThemeWrapper(context, R.style.AppDialogTheme)
1667+
val builder = AlertDialog.Builder(themedContext)
16521668
builder.setTitle(title)
16531669
builder.setMessage(message)
16541670
builder.setPositiveButton(R.string.proceed, onClickListener)

android/src/main/kotlin/com/twilio/twilio_voice/fcm/VoiceFirebaseMessagingService.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ class VoiceFirebaseMessagingService : FirebaseMessagingService(), MessageListene
165165
putExtra(TVConnectionService.EXTRA_CANCEL_CALL_INVITE, cancelledCallInvite)
166166
// applicationContext.startService(this)
167167
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
168-
applicationContext.startForegroundService(this) // Ensure it's started as a foreground service
168+
try {
169+
applicationContext.startForegroundService(this) // Ensure it's started as a foreground service
170+
} catch (e: Exception) {
171+
applicationContext.startService(this)
172+
}
169173
} else {
170174
applicationContext.startService(this)
171175
}

android/src/main/kotlin/com/twilio/twilio_voice/service/TVConnection.kt

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,40 @@ package com.twilio.twilio_voice.service
66

77
import android.content.Context
88
import android.content.Intent
9+
import android.os.Build
910
import android.os.Bundle
1011
import android.telecom.CallAudioState
1112
import android.telecom.Connection
1213
import android.telecom.DisconnectCause
14+
import android.telecom.StatusHints
1315
import android.util.Log
16+
import android.graphics.drawable.Icon
1417
import androidx.localbroadcastmanager.content.LocalBroadcastManager
18+
import com.twilio.twilio_voice.R
1519
import com.twilio.twilio_voice.call.TVParameters
1620
import com.twilio.twilio_voice.receivers.TVBroadcastReceiver
1721
import com.twilio.twilio_voice.types.CallAudioStateExtension.copyWith
1822
import com.twilio.twilio_voice.types.CallDirection
1923
import com.twilio.twilio_voice.types.CallExceptionExtension.toBundle
2024
import com.twilio.twilio_voice.types.CompletionHandler
25+
import com.twilio.twilio_voice.types.ContextExtension.hasMicrophoneAccess
2126
import com.twilio.twilio_voice.types.TVNativeCallActions
2227
import com.twilio.twilio_voice.types.TVNativeCallEvents
2328
import com.twilio.twilio_voice.types.ValueBundleChanged
2429
import com.twilio.voice.Call
2530
import com.twilio.voice.CallException
2631
import com.twilio.voice.CallInvite
32+
import com.twilio.twilio_voice.types.ContextExtension
33+
import android.app.NotificationChannel
34+
import android.app.NotificationManager
35+
import androidx.core.app.NotificationCompat
36+
import android.app.PendingIntent
37+
import android.os.Handler
38+
import android.os.Looper
39+
import com.twilio.twilio_voice.service.TVConnectionService
40+
import android.content.BroadcastReceiver
41+
import android.content.IntentFilter
42+
2743

2844

2945
class TVCallInviteConnection(
@@ -46,12 +62,55 @@ class TVCallInviteConnection(
4662

4763
override fun onAnswer() {
4864
Log.d(TAG, "onAnswer: onAnswer")
49-
super.onAnswer()
50-
twilioCall = callInvite.accept(context, this)
51-
onAction?.onChange(TVNativeCallActions.ACTION_ANSWERED, Bundle().apply {
52-
putParcelable(TVBroadcastReceiver.EXTRA_CALL_INVITE, callInvite)
53-
putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id)
54-
})
65+
66+
var isCallAccepted = false
67+
68+
fun acceptCall(receiver: BroadcastReceiver) {
69+
try {
70+
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
71+
} catch (_: Exception) {}
72+
73+
if (isCallAccepted) {
74+
return
75+
}
76+
77+
isCallAccepted = true
78+
79+
super.onAnswer()
80+
twilioCall = callInvite.accept(context, this)
81+
onAction?.onChange(TVNativeCallActions.ACTION_ANSWERED, Bundle().apply {
82+
putParcelable(TVBroadcastReceiver.EXTRA_CALL_INVITE, callInvite)
83+
putInt(TVBroadcastReceiver.EXTRA_CALL_DIRECTION, callDirection.id)
84+
})
85+
}
86+
87+
val incomingCallServiceReadyReceiver = object : android.content.BroadcastReceiver() {
88+
override fun onReceive(c: android.content.Context?, i: android.content.Intent?) {
89+
acceptCall(this)
90+
}
91+
}
92+
93+
LocalBroadcastManager.getInstance(context).registerReceiver(
94+
incomingCallServiceReadyReceiver,
95+
android.content.IntentFilter(TVConnectionService.ACTION_INCOMING_CALL_SERVICE_READY)
96+
)
97+
98+
Handler(Looper.getMainLooper()).postDelayed({
99+
acceptCall(incomingCallServiceReadyReceiver)
100+
}, 3000)
101+
102+
try {
103+
val launchIntent = Intent(context, com.twilio.twilio_voice.ui.IncomingCallActivity::class.java).apply {
104+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
105+
Intent.FLAG_ACTIVITY_SINGLE_TOP or
106+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
107+
Intent.FLAG_ACTIVITY_NO_USER_ACTION)
108+
}
109+
context.startActivity(launchIntent)
110+
} catch (e: Exception) {
111+
Log.w(TAG, "Unable to launch IncomingCallActivity from onAnswer: $e")
112+
acceptCall(incomingCallServiceReadyReceiver)
113+
}
55114
}
56115

57116
fun acceptInvite() {
@@ -159,6 +218,7 @@ open class TVCallConnection(
159218
onDisconnected?.withValue(disconnectCause)
160219
onEvent?.onChange(TVNativeCallEvents.EVENT_CONNECT_FAILURE, callException.toBundle())
161220
onCallStateListener?.withValue(call.state)
221+
destroy()
162222
}
163223

164224
/**
@@ -332,6 +392,47 @@ open class TVCallConnection(
332392
}
333393

334394
override fun onAnswer(videoState: Int) {
395+
if (!context.hasMicrophoneAccess()) {
396+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
397+
val channelId = "twilio_voice_permissions"
398+
val channelName = "Twilio Voice Permissions"
399+
400+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
401+
val channel = NotificationChannel(
402+
channelId,
403+
channelName,
404+
NotificationManager.IMPORTANCE_HIGH
405+
).apply {
406+
description = "Phone call permissions notifications"
407+
}
408+
notificationManager.createNotificationChannel(channel)
409+
}
410+
411+
// Create intent to open app settings
412+
val settingsIntent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
413+
data = android.net.Uri.fromParts("package", context.packageName, null)
414+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
415+
}
416+
val pendingIntent = PendingIntent.getActivity(
417+
context,
418+
0,
419+
settingsIntent,
420+
PendingIntent.FLAG_IMMUTABLE
421+
)
422+
423+
val notification = NotificationCompat.Builder(context, channelId)
424+
.setContentTitle("Unable to Answer Call - Microphone Access Needed")
425+
.setContentText("An incoming call is waiting, but you need to enable microphone access in settings to accept calls.")
426+
.setSmallIcon(context.resources.getIdentifier("ic_stat_onesignal_default", "drawable", context.packageName))
427+
.setLargeIcon(android.graphics.BitmapFactory.decodeResource(context.resources, context.resources.getIdentifier("ic_onesignal_large_icon_default", "drawable", context.packageName)))
428+
.setPriority(NotificationCompat.PRIORITY_HIGH)
429+
.setAutoCancel(true)
430+
.setContentIntent(pendingIntent)
431+
.build()
432+
notificationManager.notify(1, notification)
433+
return
434+
}
435+
335436
super.onAnswer(videoState)
336437
Log.d(TAG, "onAnswer: onAnswer")
337438
}

0 commit comments

Comments
 (0)