diff --git a/webtrit_callkeep/lib/src/webtrit_callkeep_permissions.dart b/webtrit_callkeep/lib/src/webtrit_callkeep_permissions.dart index ddf2490e..be36a4ac 100644 --- a/webtrit_callkeep/lib/src/webtrit_callkeep_permissions.dart +++ b/webtrit_callkeep/lib/src/webtrit_callkeep_permissions.dart @@ -74,6 +74,24 @@ class WebtritCallkeepPermissions { return platform.getBatteryMode(); } + /// Returns how incoming calls are delivered on this device. + /// + /// On devices without `android.software.telecom` this reports + /// [CallkeepAndroidCallDeliveryMode.standalone], a limited path the system may + /// throttle. On non-Android platforms returns + /// [CallkeepAndroidCallDeliveryMode.unknown]. + Future getCallDeliveryMode() { + if (kIsWeb) { + return Future.value(CallkeepAndroidCallDeliveryMode.unknown); + } + + if (!Platform.isAndroid) { + return Future.value(CallkeepAndroidCallDeliveryMode.unknown); + } + + return platform.getCallDeliveryMode(); + } + /// Requests the specified [permissions] on Android. /// /// Returns a [Map] where: diff --git a/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/Generated.kt b/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/Generated.kt index a5f0833f..d826280a 100644 --- a/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/Generated.kt +++ b/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/Generated.kt @@ -135,6 +135,19 @@ enum class PCallkeepAndroidBatteryMode( } } +enum class PCallkeepAndroidCallDeliveryMode( + val raw: Int, +) { + TELECOM(0), + STANDALONE(1), + UNKNOWN(2), + ; + + companion object { + fun ofRaw(raw: Int): PCallkeepAndroidCallDeliveryMode? = values().firstOrNull { it.raw == raw } + } +} + enum class PHandleTypeEnum( val raw: Int, ) { @@ -968,6 +981,12 @@ private open class GeneratedPigeonCodec : StandardMessageCodec() { } } + 155.toByte() -> { + return (readValue(buffer) as Long?)?.let { + PCallkeepAndroidCallDeliveryMode.ofRaw(it.toInt()) + } + } + else -> { super.readValueOfType(type, buffer) } @@ -1109,6 +1128,11 @@ private open class GeneratedPigeonCodec : StandardMessageCodec() { writeValue(stream, value.toList()) } + is PCallkeepAndroidCallDeliveryMode -> { + stream.write(155) + writeValue(stream, value.raw) + } + else -> { super.writeValue(stream, value) } @@ -1325,6 +1349,8 @@ interface PHostPermissionsApi { fun getBatteryMode(callback: (Result) -> Unit) + fun getCallDeliveryMode(callback: (Result) -> Unit) + fun requestPermissions( permissions: List, callback: (Result>) -> Unit, @@ -1419,6 +1445,24 @@ interface PHostPermissionsApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.getCallDeliveryMode$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getCallDeliveryMode { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeneratedPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.requestPermissions$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/PermissionsApi.kt b/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/PermissionsApi.kt index f46c65e5..d07da4c5 100644 --- a/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/PermissionsApi.kt +++ b/webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/PermissionsApi.kt @@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat import com.webtrit.callkeep.common.ActivityHolder import com.webtrit.callkeep.common.BatteryModeHelper import com.webtrit.callkeep.common.PermissionsHelper +import com.webtrit.callkeep.common.TelephonyUtils import com.webtrit.callkeep.common.toAndroidPermissions import com.webtrit.callkeep.common.toPPermissionResults import io.flutter.plugin.common.PluginRegistry @@ -75,6 +76,23 @@ class PermissionsApi( callback.invoke(Result.success(mode)) } + /** + * Reports whether incoming calls are delivered via the Telecom + * [android.telecom.ConnectionService] path or the limited standalone + * foreground-service path used when the device lacks + * `android.software.telecom`. Mirrors the same feature gate the router uses, + * so the value reflects the actually active delivery path. + */ + override fun getCallDeliveryMode(callback: (Result) -> Unit) { + val mode = + if (TelephonyUtils.isTelecomSupported(context)) { + PCallkeepAndroidCallDeliveryMode.TELECOM + } else { + PCallkeepAndroidCallDeliveryMode.STANDALONE + } + callback.invoke(Result.success(mode)) + } + /** * Requests the given permissions from the user. * @param permissions The list of permissions to request. diff --git a/webtrit_callkeep_android/lib/src/common/callkeep.pigeon.dart b/webtrit_callkeep_android/lib/src/common/callkeep.pigeon.dart index 2498322d..bf793caa 100644 --- a/webtrit_callkeep_android/lib/src/common/callkeep.pigeon.dart +++ b/webtrit_callkeep_android/lib/src/common/callkeep.pigeon.dart @@ -45,6 +45,8 @@ enum PSpecialPermissionStatusTypeEnum { denied, granted, unknown } enum PCallkeepAndroidBatteryMode { unrestricted, optimized, restricted, unknown } +enum PCallkeepAndroidCallDeliveryMode { telecom, standalone, unknown } + enum PHandleTypeEnum { generic, number, email } enum PCallInfoConsts { uuid, dtmf, isVideo, number, name } @@ -816,6 +818,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PCallkeepConnection) { buffer.putUint8(154); writeValue(buffer, value.encode()); + } else if (value is PCallkeepAndroidCallDeliveryMode) { + buffer.putUint8(155); + writeValue(buffer, value.index); } else { super.writeValue(buffer, value); } @@ -886,6 +891,9 @@ class _PigeonCodec extends StandardMessageCodec { return PCallkeepDisconnectCause.decode(readValue(buffer)!); case 154: return PCallkeepConnection.decode(readValue(buffer)!); + case 155: + final int? value = readValue(buffer) as int?; + return value == null ? null : PCallkeepAndroidCallDeliveryMode.values[value]; default: return super.readValueOfType(type, buffer); } @@ -1198,6 +1206,34 @@ class PHostPermissionsApi { } } + Future getCallDeliveryMode() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.getCallDeliveryMode$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as PCallkeepAndroidCallDeliveryMode?)!; + } + } + Future> requestPermissions(List permissions) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.requestPermissions$pigeonVar_messageChannelSuffix'; diff --git a/webtrit_callkeep_android/lib/src/common/converters.dart b/webtrit_callkeep_android/lib/src/common/converters.dart index f8f9bbda..7af6073a 100644 --- a/webtrit_callkeep_android/lib/src/common/converters.dart +++ b/webtrit_callkeep_android/lib/src/common/converters.dart @@ -198,6 +198,19 @@ extension PCallkeepAndroidBatteryModeConverter on PCallkeepAndroidBatteryMode { } } +extension PCallkeepAndroidCallDeliveryModeConverter on PCallkeepAndroidCallDeliveryMode { + CallkeepAndroidCallDeliveryMode toCallkeep() { + switch (this) { + case PCallkeepAndroidCallDeliveryMode.telecom: + return CallkeepAndroidCallDeliveryMode.telecom; + case PCallkeepAndroidCallDeliveryMode.standalone: + return CallkeepAndroidCallDeliveryMode.standalone; + case PCallkeepAndroidCallDeliveryMode.unknown: + return CallkeepAndroidCallDeliveryMode.unknown; + } + } +} + extension CallkeepLifecycleTypeConverter on CallkeepLifecycleEvent { PCallkeepLifecycleEvent toPigeon() { switch (this) { diff --git a/webtrit_callkeep_android/lib/src/webtrit_callkeep_android.dart b/webtrit_callkeep_android/lib/src/webtrit_callkeep_android.dart index a5f852e9..86d46f7d 100644 --- a/webtrit_callkeep_android/lib/src/webtrit_callkeep_android.dart +++ b/webtrit_callkeep_android/lib/src/webtrit_callkeep_android.dart @@ -196,6 +196,11 @@ class WebtritCallkeepAndroid extends WebtritCallkeepPlatform { return _permissionsApi.getBatteryMode().then((value) => value.toCallkeep()); } + @override + Future getCallDeliveryMode() { + return _permissionsApi.getCallDeliveryMode().then((value) => value.toCallkeep()); + } + @override Future> getDiagnosticReport() async { final rawData = await _diagnosticsApi.getDiagnosticReport(); diff --git a/webtrit_callkeep_android/pigeons/callkeep.messages.dart b/webtrit_callkeep_android/pigeons/callkeep.messages.dart index 0168013f..459fbdeb 100644 --- a/webtrit_callkeep_android/pigeons/callkeep.messages.dart +++ b/webtrit_callkeep_android/pigeons/callkeep.messages.dart @@ -66,6 +66,8 @@ class PPermissionResult { enum PCallkeepAndroidBatteryMode { unrestricted, optimized, restricted, unknown } +enum PCallkeepAndroidCallDeliveryMode { telecom, standalone, unknown } + enum PHandleTypeEnum { generic, number, email } enum PCallInfoConsts { uuid, dtmf, isVideo, number, name } @@ -261,6 +263,11 @@ abstract class PHostPermissionsApi { @async PCallkeepAndroidBatteryMode getBatteryMode(); + /// How incoming calls are delivered: Telecom `ConnectionService` vs the + /// limited standalone foreground service (device without `android.software.telecom`). + @async + PCallkeepAndroidCallDeliveryMode getCallDeliveryMode(); + @async List requestPermissions(List permissions); diff --git a/webtrit_callkeep_android/test/converters_test.dart b/webtrit_callkeep_android/test/converters_test.dart index 05276103..9fb240a6 100644 --- a/webtrit_callkeep_android/test/converters_test.dart +++ b/webtrit_callkeep_android/test/converters_test.dart @@ -420,6 +420,24 @@ void main() { }); }); + // --------------------------------------------------------------------------- + // PCallkeepAndroidCallDeliveryModeConverter + // --------------------------------------------------------------------------- + + group('PCallkeepAndroidCallDeliveryModeConverter.toCallkeep()', () { + test('telecom maps to CallkeepAndroidCallDeliveryMode.telecom', () { + expect(PCallkeepAndroidCallDeliveryMode.telecom.toCallkeep(), CallkeepAndroidCallDeliveryMode.telecom); + }); + + test('standalone maps to CallkeepAndroidCallDeliveryMode.standalone', () { + expect(PCallkeepAndroidCallDeliveryMode.standalone.toCallkeep(), CallkeepAndroidCallDeliveryMode.standalone); + }); + + test('unknown maps to CallkeepAndroidCallDeliveryMode.unknown', () { + expect(PCallkeepAndroidCallDeliveryMode.unknown.toCallkeep(), CallkeepAndroidCallDeliveryMode.unknown); + }); + }); + // --------------------------------------------------------------------------- // CallkeepLifecycleTypeConverter (CallkeepLifecycleEvent -> PCallkeepLifecycleEvent) // --------------------------------------------------------------------------- diff --git a/webtrit_callkeep_android/test/webtrit_callkeep_android_api_test.dart b/webtrit_callkeep_android/test/webtrit_callkeep_android_api_test.dart index 8ac52910..79ffef80 100644 --- a/webtrit_callkeep_android/test/webtrit_callkeep_android_api_test.dart +++ b/webtrit_callkeep_android/test/webtrit_callkeep_android_api_test.dart @@ -305,6 +305,21 @@ void main() { expect(await WebtritCallkeepPlatform.instance.getBatteryMode(), CallkeepAndroidBatteryMode.restricted); }); + test('getCallDeliveryMode returns telecom', () async { + _mockPigeon('$_prefix.PHostPermissionsApi.getCallDeliveryMode', PCallkeepAndroidCallDeliveryMode.telecom); + expect(await WebtritCallkeepPlatform.instance.getCallDeliveryMode(), CallkeepAndroidCallDeliveryMode.telecom); + }); + + test('getCallDeliveryMode returns standalone', () async { + _mockPigeon('$_prefix.PHostPermissionsApi.getCallDeliveryMode', PCallkeepAndroidCallDeliveryMode.standalone); + expect(await WebtritCallkeepPlatform.instance.getCallDeliveryMode(), CallkeepAndroidCallDeliveryMode.standalone); + }); + + test('getCallDeliveryMode returns unknown', () async { + _mockPigeon('$_prefix.PHostPermissionsApi.getCallDeliveryMode', PCallkeepAndroidCallDeliveryMode.unknown); + expect(await WebtritCallkeepPlatform.instance.getCallDeliveryMode(), CallkeepAndroidCallDeliveryMode.unknown); + }); + test('requestPermissions maps readPhoneState to granted', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler( '$_prefix.PHostPermissionsApi.requestPermissions', diff --git a/webtrit_callkeep_platform_interface/lib/src/models/callkeep_android_call_delivery_mode.dart b/webtrit_callkeep_platform_interface/lib/src/models/callkeep_android_call_delivery_mode.dart new file mode 100644 index 00000000..5759074b --- /dev/null +++ b/webtrit_callkeep_platform_interface/lib/src/models/callkeep_android_call_delivery_mode.dart @@ -0,0 +1,10 @@ +/// How incoming calls are delivered on Android. +/// +/// - [telecom]: the device supports `android.software.telecom`, so calls go +/// through the system Telecom `android.telecom.ConnectionService` path (full integration). +/// - [standalone]: the device lacks Telecom, so calls use a limited standalone +/// foreground service. Delivery may be throttled by the system (Doze, +/// background restrictions) and outgoing calls, hold and Bluetooth/wired +/// headset selection are not available. +/// - [unknown]: the mode could not be determined or the platform is not Android. +enum CallkeepAndroidCallDeliveryMode { telecom, standalone, unknown } diff --git a/webtrit_callkeep_platform_interface/lib/src/models/models.dart b/webtrit_callkeep_platform_interface/lib/src/models/models.dart index 2b4c97e0..a1ff635a 100644 --- a/webtrit_callkeep_platform_interface/lib/src/models/models.dart +++ b/webtrit_callkeep_platform_interface/lib/src/models/models.dart @@ -1,4 +1,5 @@ export 'callkeep_android_battery_mode.dart'; +export 'callkeep_android_call_delivery_mode.dart'; export 'callkeep_incoming_call_metadata.dart'; export 'callkeep_audio_device.dart'; export 'callkeep_call_request_error.dart'; diff --git a/webtrit_callkeep_platform_interface/lib/src/webtrit_callkeep_platform_interface.dart b/webtrit_callkeep_platform_interface/lib/src/webtrit_callkeep_platform_interface.dart index 9e960a26..b39631d5 100644 --- a/webtrit_callkeep_platform_interface/lib/src/webtrit_callkeep_platform_interface.dart +++ b/webtrit_callkeep_platform_interface/lib/src/webtrit_callkeep_platform_interface.dart @@ -190,6 +190,11 @@ abstract class WebtritCallkeepPlatform extends PlatformInterface { throw UnimplementedError('getBatteryMode() has not been implemented.'); } + /// Returns how incoming calls are delivered (Telecom vs limited standalone). + Future getCallDeliveryMode() { + throw UnimplementedError('getCallDeliveryMode() has not been implemented.'); + } + /// Requests the specified [permissions] on Android. /// /// Returns a [Map] where: diff --git a/webtrit_callkeep_web/lib/webtrit_callkeep_web.dart b/webtrit_callkeep_web/lib/webtrit_callkeep_web.dart index e03cd51d..bd1cb599 100644 --- a/webtrit_callkeep_web/lib/webtrit_callkeep_web.dart +++ b/webtrit_callkeep_web/lib/webtrit_callkeep_web.dart @@ -260,6 +260,9 @@ class WebtritCallkeepWeb extends WebtritCallkeepPlatform { @override Future getBatteryMode() async => CallkeepAndroidBatteryMode.unknown; + @override + Future getCallDeliveryMode() async => CallkeepAndroidCallDeliveryMode.unknown; + @override Future> requestPermissions( List permissions,