Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cybex-dev/twilio_voice
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.1.0
Choose a base ref
...
head repository: cybex-dev/twilio_voice
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.1.1
Choose a head ref
Loading
2 changes: 1 addition & 1 deletion .github/workflows/flutter.yml
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ jobs:
- run: cd ./example; flutter build web --release --target=lib/main.dart --output=build/web

- name: Update service worker
run: cat ./example/service-worker/twilio-sw.js > ./example/build/web/flutter_service_worker.js
run: cat ./example/service-worker/twilio-sw.js >> ./example/build/web/flutter_service_worker.js

- name: Archive Production Artifact
uses: actions/upload-artifact@master
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
## Next release

## 0.1.1

* Fix: [Web] Multiple missed call notifications
* Fix: [Android] `openPhoneAccountsSettings` not always opening on various Android (mainly Samsung) devices
* Fix: Showing `CallDirection.outgoing` instead of `CallDirection.incoming` when Incoming call is ringing in `CallEventsListeners`.
* Fix: `ActiveCall` is not null even after the Call is declined.
* Fix: Android foreground service not starting on Android +11, many thanks to [@mohsen-jalali](https://github.com/mohsen-jalali)
* Fix: Android foreground microphone permission not granted on Android +14.
* Revert: [Android] allow registering clients with an empty name (supporting current implementation).

## 0.1.0

* Feat: [Android] Turn off the screen when a call is active and the head is against the handset. @solid-software (https://solid.software)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -210,7 +210,7 @@ A snippet of the suggested service worker integration is as follows:
- run: cd ./example; flutter build web --release --target=lib/main.dart --output=build/web
- name: Update service worker
run: cat ./example/web/twilio-sw.js > ./example/build/web/flutter_service_worker.js
run: cat ./example/web/twilio-sw.js >> ./example/build/web/flutter_service_worker.js
#...
```
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ android {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version"))
implementation(platform("com.google.firebase:firebase-bom:32.2.2"))
implementation("com.twilio:voice-android:6.2.1")
implementation("com.twilio:voice-android:6.3.2")
implementation("com.google.firebase:firebase-messaging-ktx:23.2.1")
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>

<application>
<service
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.telecom.CallAudioState
import android.telecom.PhoneAccountHandle
@@ -95,6 +96,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
private val REQUEST_CODE_CALL_PHONE = 3
private val REQUEST_CODE_READ_PHONE_NUMBERS = 4
private val REQUEST_CODE_READ_PHONE_STATE = 5
private val REQUEST_CODE_MICROPHONE_FOREGROUND = 6

private var isSpeakerOn: Boolean = false
private var isBluetoothOn: Boolean = false
@@ -244,6 +246,9 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
if (requestCode == REQUEST_CODE_MICROPHONE) {
if (permissions.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Microphone permission granted")
requestPermissionForMicrophoneForeground(onPermissionResult = { granted ->
Log.d(TAG, "onRequestPermissionsResult: Microphone foreground permission granted: $granted");
});
logEventPermission("Microphone", true)
} else {
Log.d(TAG, "Microphone permission not granted")
@@ -281,6 +286,14 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
Log.d(TAG, "Call Phone permission not granted")
logEventPermission("Call Phone State", false)
}
} else if (requestCode == REQUEST_CODE_MICROPHONE_FOREGROUND) {
if (permissions.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Microphone foreground permission granted")
logEventPermission("Microphone", true)
} else {
Log.d(TAG, "Microphone foreground permission not granted")
logEventPermission("Microphone", false)
}
}
return true
}
@@ -1435,6 +1448,23 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
)
}

/// Request permission for microphone on Android 14 and higher.
/// Source: https://developer.android.com/reference/android/Manifest.permission#FOREGROUND_SERVICE_MICROPHONE
private fun requestPermissionForMicrophoneForeground(onPermissionResult: (Boolean) -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Log.d(TAG, "requestPermissionForMicrophoneForeground: Microphone foreground permission automatically requested.");
return requestPermissionOrShowRationale(
"Microphone Foreground",
"Microphone Foreground permission is required to make or receive phone calls on Android 14 and higher.",
Manifest.permission.FOREGROUND_SERVICE_MICROPHONE,
REQUEST_CODE_MICROPHONE_FOREGROUND,
onPermissionResult
)
} else {
Log.d(TAG, "requestPermissionForMicrophoneForeground: Microphone foreground permission skipped.");
}
}

private fun requestPermissionForPhoneState(onPermissionResult: (Boolean) -> Unit) {
return requestPermissionOrShowRationale(
"Read Phone State",
Original file line number Diff line number Diff line change
@@ -472,19 +472,22 @@ class TVConnectionService : ConnectionService() {
//endregion

override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
assert(request != null) { "ConnectionRequest cannot be null" }
assert(connectionManagerPhoneAccount != null) { "ConnectionManagerPhoneAccount cannot be null" }

super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
Log.d(TAG, "onCreateIncomingConnection")

val extras = request?.extras
val myBundle: Bundle = extras?.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS) ?: run {
Log.e(TAG, "onCreateIncomingConnection: request is missing Bundle EXTRA_INCOMING_CALL_EXTRAS")
return super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateIncomingConnection: request is missing Bundle EXTRA_INCOMING_CALL_EXTRAS");
}

myBundle.classLoader = CallInvite::class.java.classLoader
val ci: CallInvite = myBundle.getParcelableSafe(EXTRA_INCOMING_CALL_INVITE) ?: run {
Log.e(TAG, "onCreateIncomingConnection: request is missing CallInvite EXTRA_INCOMING_CALL_INVITE")
return super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateIncomingConnection: request is missing CallInvite EXTRA_INCOMING_CALL_INVITE");
}

// Create storage instance for call parameters
@@ -512,27 +515,30 @@ class TVConnectionService : ConnectionService() {
}

override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
assert(request != null) { "ConnectionRequest cannot be null" }
assert(connectionManagerPhoneAccount != null) { "ConnectionManagerPhoneAccount cannot be null" }

super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
Log.d(TAG, "onCreateOutgoingConnection")

val extras = request?.extras
val myBundle: Bundle = extras?.getBundle(EXTRA_OUTGOING_PARAMS) ?: run {
Log.e(TAG, "onCreateOutgoingConnection: request is missing Bundle EXTRA_OUTGOING_PARAMS")
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateOutgoingConnection: request is missing Bundle EXTRA_OUTGOING_PARAMS");
}

// check required EXTRA_TOKEN, EXTRA_TO, EXTRA_FROM
val token: String = myBundle.getString(EXTRA_TOKEN) ?: run {
Log.e(TAG, "onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TOKEN")
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TOKEN");
}
val to = myBundle.getString(EXTRA_TO) ?: run {
Log.e(TAG, "onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TO")
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_TO");
}
val from = myBundle.getString(EXTRA_FROM) ?: run {
Log.e(TAG, "onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_FROM")
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
throw Exception("onCreateOutgoingConnection: ACTION_PLACE_OUTGOING_CALL is missing String EXTRA_FROM");
}

// Get all params from bundle
@@ -590,6 +596,8 @@ class TVConnectionService : ConnectionService() {
// Apply extras
connection.extras = request.extras

startForegroundService()

return connection
}

Original file line number Diff line number Diff line change
@@ -75,15 +75,17 @@ class StorageImpl(ctx: Context) : Storage {
}

override fun addRegisteredClient(id: String, name: String): Boolean {
assert(id.isNotEmpty()) { "addRegisteredClient: id cannot be empty" }
// TODO: remove this assert, it's not needed now.
// assert(id.isNotEmpty()) { "addRegisteredClient: id cannot be empty" }
prefs.let {
val editor = it.edit()
return editor.putString(id, name).commit()
}
}

override fun removeRegisteredClient(id: String): Boolean {
assert(id.isNotEmpty()) { "removeRegisteredClient: id cannot be empty" }
// TODO: remove this assert, it's not needed now.
// assert(id.isNotEmpty()) { "removeRegisteredClient: id cannot be empty" }
prefs.let {
val editor = it.edit()
return editor.remove(id).commit()
Original file line number Diff line number Diff line change
@@ -61,13 +61,22 @@ object TelecomManagerExtension {

fun TelecomManager.openPhoneAccountSettings(activity: Activity) {
if (Build.MANUFACTURER.equals("Samsung", ignoreCase = true)) {
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
intent.component = ComponentName(
"com.android.server.telecom",
"com.android.server.telecom.settings.EnableAccountPreferenceActivity"
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
activity.startActivity(intent, null)
try {
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
intent.component = ComponentName(
"com.android.server.telecom",
"com.android.server.telecom.settings.EnableAccountPreferenceActivity"
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
activity.startActivity(intent, null)
} catch (e: Exception) {
Log.e("TelecomManager", "openPhoneAccountSettings: ${e.message}")

// use fallback method
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
activity.startActivity(intent, null)
}

} else {
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
activity.startActivity(intent, null)
6 changes: 3 additions & 3 deletions ios/twilio_voice.podspec
Original file line number Diff line number Diff line change
@@ -9,13 +9,13 @@ Pod::Spec.new do |s|
s.description = <<-DESC
Provides an interface to Twilio&#x27;s Programmable Voice SDK to allows adding voice-over-IP (VoIP) calling into your Flutter applications.
DESC
s.homepage = 'https://github.com/diegogarciar/twilio_voice/'
s.homepage = 'https://github.com/cybex-dev/twilio_voice/'
s.license = { :file => '../LICENSE' }
s.author = { 'Diego Garcia' => 'diego_gr_94@hotmail.com' }
s.author = { 'Charles Dyason' => 'charles@earthbase.io' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'TwilioVoice','~> 6.3.0'
s.dependency 'TwilioVoice','~> 6.9.1'
s.platform = :ios, '11.0'

# Flutter.framework does not contain a i386 slice.
4 changes: 4 additions & 0 deletions lib/_internal/method_channel/twilio_voice_method_channel.dart
Original file line number Diff line number Diff line change
@@ -315,15 +315,19 @@ class MethodChannelTwilioVoice extends TwilioVoicePlatform {
// https://www.twilio.com/docs/api/errors/31486
// The callee is busy.
if (tokens[1].contains("31603") || tokens[1].contains("31486")) {
call.activeCall = null;
return CallEvent.declined;
} else if (tokens.toString().toLowerCase().contains("call rejected")) {
// Android call reject from string: "LOG|Call Rejected"
call.activeCall = null;
return CallEvent.declined;
} else if (tokens.toString().toLowerCase().contains("rejecting call")) {
// iOS call reject from string: "LOG|provider:performEndCallAction: rejecting call"
call.activeCall = null;
return CallEvent.declined;
} else if (tokens[1].contains("Call Rejected")) {
// macOS / web call reject from string: "Call Rejected"
call.activeCall = null;
return CallEvent.declined;
}
return CallEvent.log;
3 changes: 2 additions & 1 deletion lib/_internal/twilio_voice_web.dart
Original file line number Diff line number Diff line change
@@ -943,12 +943,13 @@ class Call extends MethodChannelTwilioCall {
// TODO(cybex-dev) future actions
// {'action': 'callback', 'title': 'Return Call'},
];
final callSid = callParams["CallSid"] as String;

// show JS notification using SW
NotificationService.instance.showNotification(
action: action,
title: title,
tag: "",
tag: callSid,
body: body,
actions: actions,
requiresInteraction: true,
2 changes: 1 addition & 1 deletion lib/twilio_voice.dart
Original file line number Diff line number Diff line change
@@ -12,4 +12,4 @@ class TwilioVoice extends MethodChannelTwilioVoice {
static TwilioVoicePlatform get instance => MethodChannelTwilioVoice.instance;
}

class Call extends MethodChannelTwilioCall {}
class Call extends MethodChannelTwilioCall {}
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: twilio_voice
description: Provides an interface to Twilio's Programmable Voice SDK to allow adding voice-over-IP (VoIP) calling into your Flutter applications.
version: 0.1.0
homepage: https://github.com/diegogarciar/twilio_voice
version: 0.1.1
homepage: https://github.com/cybex-dev/twilio_voice

environment:
sdk: ">=2.17.0 <4.0.0"