Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add missing manage own calls Android permission #198

Merged
merged 6 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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