Skip to content

Commit

Permalink
Merge pull request #198 from cybex-dev/194-add-manage_own_calls-andro…
Browse files Browse the repository at this point in the history
…id-permission

[Feat] Add missing manage own calls Android permission
  • Loading branch information
cybex-dev authored Nov 10, 2023
2 parents e0fad92 + 53151f9 commit 2b144df
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* `Reconnected`
* `Reconnecting`
* Fix: [Android] Fix `unregister()` from Twilio (assign internal device token)
* Update: [Android] Add `MANGE_OWN_CALLS` permission to manifest, method channel implementation & example update, see discussion [here](https://github.com/cybex-dev/twilio_voice/issues/194).
* Update: example with logout action, new `CallEvent`s

## 0.1.1
Expand Down
5 changes: 5 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ Required for reading phone numbers (e.g. for Telecom App), this is required to c
* `android.permission.CALL_PHONE`
Required for `ConnectionService` to interact with the `TelecomManager` to place outgoing calls.

* `android.permission.MANAGE_OWN_CALLS`
* Required for `ConnectionService` to interact with the `TelecomManager` to receive incoming calls.
* According to Android documentation, this permission is only required for self-managed `ConnectionService`'s, however it seems to be required for system-managed `ConnectionService`'s as well (atleast on Android 13 and lower).
* Finally, this permission seems to be required to place outgoing calls on Android 13 and lower, if not will result `java.lang.SecurityException: Self-managed ConnectionServices require MANAGE_OWN_CALLS permission.`

#### ConnectionService integration
There are a few (additional) permissions added to use the [system-managed `ConnectionService`](https://developer.android.com/reference/android/telecom/ConnectionService), several permissions are required to enable this functionality (see example app). These permissions `android.permission.READ_PHONE_STATE`, `android.permission.READ_PHONE_NUMBERS`, `android.permission.RECORD_AUDIO` and `android.permission.CALL_PHONE` have already been added to the package, you do not have to add them. Finally, a [PhoneAccount] is required to interact with the `ConnectionService`, this is discussed in more detail below.

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ Similar to CallKit on iOS, Android implements their own via a [ConnectionService
TwilioVoice.instance.requestCallPhonePermission(); // Gives Android permissions to place outgoing calls
TwilioVoice.instance.requestReadPhoneStatePermission(); // Gives Android permissions to read Phone State including receiving calls
TwilioVoice.instance.requestReadPhoneNumbersPermission(); // Gives Android permissions to read Phone Accounts
TwilioVoice.instance.requestManageOwnCallsPermission(); // Gives Android permissions to manage calls, this isn't necessary to request as the permission is simply required in the Manifest, but added nontheless.
```
Following this, to register a Phone Account (required by all applications implementing a system-managed `ConnectionService`, run:
Expand All @@ -515,6 +516,8 @@ TwilioVoice.instance.isRejectingCallOnNoPermissions(); // Checks if the plugin i
If the `CALL_PHONE` permissions group i.e. `READ_PHONE_STATE`, `READ_PHONE_NUMBERS`, `CALL_PHONE` aren't granted nor a Phone Account is registered and enabled, the plugin will either reject the incoming call (true) or not show the incoming call UI (false).
_Note: If `MANAGE_OWN_CALLS` permission is not granted, outbound calls will not work._
See [Android Setup](#android-setup) and [Android Notes](https://github.com/diegogarciar/twilio_voice/blob/master/NOTES.md#android) for more information regarding configuring the `ConnectionService` and registering a Phone Account.
### Localization
Expand Down
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>

<application>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.twilio.twilio_voice.types.ContextExtension.hasReadPhoneNumbersPermiss
import com.twilio.twilio_voice.types.ContextExtension.hasReadPhoneStatePermission
import com.twilio.twilio_voice.types.ContextExtension.checkPermission
import com.twilio.twilio_voice.types.ContextExtension.hasCallPhonePermission
import com.twilio.twilio_voice.types.ContextExtension.hasManageOwnCallsPermission
import com.twilio.twilio_voice.types.IntentExtension.getParcelableExtraSafe
import com.twilio.twilio_voice.types.TVMethodChannels
import com.twilio.twilio_voice.types.TVNativeCallActions
Expand Down Expand Up @@ -97,6 +98,7 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
private val REQUEST_CODE_READ_PHONE_NUMBERS = 4
private val REQUEST_CODE_READ_PHONE_STATE = 5
private val REQUEST_CODE_MICROPHONE_FOREGROUND = 6
private val REQUEST_CODE_MANAGE_CALLS = 7

private var isSpeakerOn: Boolean = false
private var isBluetoothOn: Boolean = false
Expand Down Expand Up @@ -282,6 +284,13 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
if (permissions.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Call Phone permission granted")
logEventPermission("Call Phone", true)
requestPermissionForManagingCalls {
if(it) {
Log.d(TAG, "onRequestPermissionsResult: Manage Calls permission granted");
} else {
Log.d(TAG, "onRequestPermissionsResult: Manage Calls permission not granted");
}
}
} else {
Log.d(TAG, "Call Phone permission not granted")
logEventPermission("Call Phone State", false)
Expand All @@ -294,6 +303,14 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
Log.d(TAG, "Microphone foreground permission not granted")
logEventPermission("Microphone", false)
}
} else if (requestCode == REQUEST_CODE_MANAGE_CALLS) {
if (permissions.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Manage Calls permission granted")
logEventPermission("Manage Calls", true)
} else {
Log.d(TAG, "Manage Calls permission not granted")
logEventPermission("Manage Calls", false)
}
}
return true
}
Expand Down Expand Up @@ -810,6 +827,21 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
}
}

TVMethodChannels.HAS_MANAGE_OWN_CALLS_PERMISSION -> {
result.success(checkManageOwnCallsPermission())
}

TVMethodChannels.REQUEST_MANAGE_OWN_CALLS_PERMISSION -> {
logEvent("requestingManageOwnCallsPermission")
if (!checkManageOwnCallsPermission()) {
requestPermissionForManagingCalls() { granted ->
result.success(granted)
}
} else {
result.success(true)
}
}

TVMethodChannels.HAS_BLUETOOTH_PERMISSION -> {
// Deprecated in favour of native call screen handling these permissions
result.success(false)
Expand Down Expand Up @@ -1033,6 +1065,10 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
Log.e(TAG, "No call phone permission, call `requestCallPhonePermission()` first")
return false
}
if (!checkManageOwnCallsPermission()) {
Log.e(TAG, "No manage own calls permission, call `requestManageOwnCallsPermission()` first")
return false
}

val callParams = HashMap<String, String>(params)
if (params[Constants.PARAM_TO] == null) {
Expand Down Expand Up @@ -1399,6 +1435,15 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
return context?.hasCallPhonePermission() ?: false
}

private fun checkManageOwnCallsPermission(): Boolean {
Log.d(TAG, "checkManageOwnCallsPermission")
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
return context?.hasManageOwnCallsPermission() ?: false
} else {
return true
}
}

/**
* Checks if a [PhoneAccount] is registered with the Telecom app, and is enabled.
* Requires permissions:
Expand Down Expand Up @@ -1466,6 +1511,25 @@ class TwilioVoicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamH
}
}

/// Request permission for manage own calls for Android 13 and lower.
/// Source: https://developer.android.com/reference/android/Manifest.permission#MANAGE_OWN_CALLS
/// Note from source: "Allows a calling application which manages its own calls through the self-managed ConnectionService APIs..."
/// Even though we use a system-managed ConnectionService, we still need this permission for Android 13 and lower.
private fun requestPermissionForManagingCalls(onPermissionResult: (Boolean) -> Unit) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
Log.d(TAG, "requestPermissionForManagingCalls: Manage own calls automatically requested.");
return requestPermissionOrShowRationale(
"Manage Calls",
"Manage own calls permission.",
Manifest.permission.MANAGE_OWN_CALLS,
REQUEST_CODE_MANAGE_CALLS,
onPermissionResult
)
} else {
Log.d(TAG, "requestPermissionForManagingCalls: Manage own calls permission skipped.");
}
}

private fun requestPermissionForPhoneState(onPermissionResult: (Boolean) -> Unit) {
return requestPermissionOrShowRationale(
"Read Phone State",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.twilio.twilio_voice.types.CallDirection
import com.twilio.twilio_voice.types.CompletionHandler
import com.twilio.twilio_voice.types.ContextExtension.appName
import com.twilio.twilio_voice.types.ContextExtension.hasCallPhonePermission
import com.twilio.twilio_voice.types.ContextExtension.hasManageOwnCallsPermission
import com.twilio.twilio_voice.types.IntentExtension.getParcelableExtraSafe
import com.twilio.twilio_voice.types.TelecomManagerExtension.getPhoneAccountHandle
import com.twilio.twilio_voice.types.TelecomManagerExtension.hasCallCapableAccount
Expand Down Expand Up @@ -398,6 +399,11 @@ class TVConnectionService : ConnectionService() {
return@let
}

if (!applicationContext.hasManageOwnCallsPermission()) {
Log.e(TAG, "onStartCommand: Missing MANAGE_OWN_CALLS permission, request permission with `requestManageOwnCallsPermission()`")
return@let
}

// Create outgoing extras
val extras = Bundle().apply {
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ object ContextExtension {
return checkPermission(android.Manifest.permission.READ_PHONE_STATE)
}

/**
* Check if the app has the MANAGE_OWN_CALLS permission
* @return Boolean True if the app has the MANAGE_OWN_CALLS permission
*/
fun Context.hasManageOwnCallsPermission(): Boolean {
return checkPermission(android.Manifest.permission.MANAGE_OWN_CALLS)
}

/**
* Check if the app has the CALL_PHONE permission
* @return Boolean True if the app has the CALL_PHONE permission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ enum class TVMethodChannels(val method: String) {
BACKGROUND_CALL_UI("backgroundCallUi"),
SHOW_NOTIFICATIONS("showNotifications"),
HAS_READ_PHONE_NUMBERS_PERMISSION("hasReadPhoneNumbersPermission"),
HAS_MANAGE_OWN_CALLS_PERMISSION("hasManageOwnCallsPermission"),
REQUEST_MANAGE_OWN_CALLS_PERMISSION("requestManageOwnCallsPermission"),
@Deprecated("No longer required due to Custom UI replaced with native call screen")
REQUIRES_BACKGROUND_PERMISSIONS("requiresBackgroundPermissions"),
REQUEST_READ_PHONE_NUMBERS_PERMISSION("requestReadPhoneNumbersPermission"),
Expand Down
21 changes: 21 additions & 0 deletions example/lib/screens/widgets/permissions_block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ class _PermissionsBlockState extends State<PermissionsBlock> with WidgetsBinding
});
}

bool _hasManageCallsPermission = false;

set setManageCallsPermission(bool value) {
setState(() {
_hasManageCallsPermission = value;
});
}

bool _isPhoneAccountEnabled = false;

set setIsPhoneAccountEnabled(bool value) {
Expand Down Expand Up @@ -198,6 +206,7 @@ class _PermissionsBlockState extends State<PermissionsBlock> with WidgetsBinding
FirebaseMessaging.instance.requestPermission().then((value) => setBackgroundPermission = value.authorizationStatus == AuthorizationStatus.authorized);
}
_tv.hasCallPhonePermission().then((value) => setCallPhonePermission = value);
_tv.hasManageOwnCallsPermission().then((value) => setManageCallsPermission = value);
_tv.hasRegisteredPhoneAccount().then((value) => setPhoneAccountRegistered = value);
_tv.isPhoneAccountEnabled().then((value) => setIsPhoneAccountEnabled = value);
}
Expand Down Expand Up @@ -327,6 +336,18 @@ class _PermissionsBlockState extends State<PermissionsBlock> with WidgetsBinding
},
),

// if android
if (!kIsWeb && Platform.isAndroid)
PermissionTile(
icon: Icons.call_received,
title: "Manage Calls",
granted: _hasManageCallsPermission,
onRequestPermission: () async {
await _tv.requestManageOwnCallsPermission();
setManageCallsPermission = await _tv.hasManageOwnCallsPermission();
},
),

// if android
if (!kIsWeb && Platform.isAndroid)
PermissionTile(
Expand Down
22 changes: 22 additions & 0 deletions lib/_internal/method_channel/twilio_voice_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,28 @@ class MethodChannelTwilioVoice extends TwilioVoicePlatform {
return _channel.invokeMethod('requestCallPhonePermission', {});
}

/// Checks if device has permission to manage system calls
///
/// Android only
@override
Future<bool> hasManageOwnCallsPermission() {
if (defaultTargetPlatform != TargetPlatform.android) {
return Future.value(true);
}
return _channel.invokeMethod<bool?>('hasManageOwnCallsPermission', {}).then<bool>((bool? value) => value ?? false);
}

/// Requests system permission to manage calls
///
/// Android only
@override
Future<bool?> requestManageOwnCallsPermission() {
if (defaultTargetPlatform != TargetPlatform.android) {
return Future.value(true);
}
return _channel.invokeMethod('requestManageOwnCallsPermission', {});
}

/// Checks if device has read phone numbers permission
///
/// Android only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ abstract class TwilioVoicePlatform extends SharedPlatformInterface {
/// Android only
Future<bool?> requestCallPhonePermission();

/// Checks if device has permission to manage system calls
///
/// Android only
Future<bool> hasManageOwnCallsPermission();

/// Requests system permission to manage calls
///
/// Android only
Future<bool?> requestManageOwnCallsPermission();

/// Checks if device has read phone numbers permission
///
/// Android only
Expand Down

0 comments on commit 2b144df

Please sign in to comment.