Skip to content
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
2 changes: 1 addition & 1 deletion webtrit_callkeep/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: webtrit_callkeep
description: Flutter WebTrit CallKeep plugin
version: 1.3.1+0
version: 1.3.1+1
publish_to: none

environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import android.Manifest
import android.content.ComponentName
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.telephony.PhoneNumberUtils
import android.telephony.TelephonyManager
import androidx.annotation.RequiresPermission
import com.webtrit.callkeep.models.CallMetadata
Expand All @@ -18,17 +16,6 @@ import com.webtrit.callkeep.services.services.connection.PhoneConnectionService
class TelephonyUtils(
private val context: Context,
) {
fun isEmergencyNumber(number: String): Boolean {
val telephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager

return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
telephonyManager.isEmergencyNumber(number)
} else {
PhoneNumberUtils.isEmergencyNumber(number)
}
}

fun getTelecomManager(): TelecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager

@RequiresPermission(Manifest.permission.CALL_PHONE)
Expand Down Expand Up @@ -103,8 +90,34 @@ class TelephonyUtils(
// Defined as a local constant to avoid a lint InlinedApi warning on minSdk 26.
private const val FEATURE_TELECOM = "android.software.telecom"

// RFC 9476 reserved .internal TLD, guaranteed to never resolve publicly. This host is
// never used for actual dialing, only for Telecom's own bookkeeping - see buildOutgoingUri.
private const val OUTGOING_URI_HOST = "portadialer.internal"

private val logger = Log(TAG)

/**
* Builds the [Uri] to pass to [TelecomManager.placeCall] for an outgoing call to [number].
*
* Deliberately uses the sip: scheme with an explicit @host instead of tel:. Android's
* emergency-number matching (Telecom's internal isEmergencyNumber() re-check before a
* self-managed PhoneAccount is allowed to place a call) only ever applies to tel: scheme
* addresses whose scheme-specific-part looks like a bare phone number - a self-managed
* PhoneAccount can never place a Telecom-classified emergency call, so a legitimate PBX
* extension that happens to collide with the device/SIM-region emergency-number list
* (e.g. "112" or "911") would otherwise be silently blocked. This Uri is only used for
* Telecom bookkeeping; the real number keeps flowing unchanged via CallMetadata into
* onCreateOutgoingConnection, which never reads request.address.
*
* The number is percent-encoded so URI-reserved characters in it (e.g. "#" in PBX
* feature codes) cannot break the Uri structure, while the "@host" delimiter stays
* literal - a bare "sip:<number>" (no literal @host) still matches the emergency
* check, so the delimiter must not be encoded (Uri.fromParts would encode it too).
* Plain digit numbers are unaffected by the encoding, keeping the exact Uri shape
* validated on-device.
*/
fun buildOutgoingUri(number: String): Uri = Uri.parse("${PhoneAccount.SCHEME_SIP}:${Uri.encode(number)}@$OUTGOING_URI_HOST")

/**
* Returns true if the device supports the Android Telecom framework.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.os.Bundle

enum class OutgoingFailureType {
UNENTITLED,
EMERGENCY_NUMBER,
}

open class FailureMetadata(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import android.telecom.Connection
import android.telecom.ConnectionRequest
import android.telecom.ConnectionService
import android.telecom.DisconnectCause
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import androidx.annotation.RequiresPermission
import com.webtrit.callkeep.PIncomingCallError
Expand All @@ -20,10 +19,8 @@ import com.webtrit.callkeep.common.Log
import com.webtrit.callkeep.common.TelephonyUtils
import com.webtrit.callkeep.models.CallConnectionState
import com.webtrit.callkeep.models.CallMetadata
import com.webtrit.callkeep.models.EmergencyNumberException
import com.webtrit.callkeep.models.FailureMetadata
import com.webtrit.callkeep.models.InvalidCallMetadataException
import com.webtrit.callkeep.models.OutgoingFailureType
import com.webtrit.callkeep.services.broadcaster.CallCommandEvent
import com.webtrit.callkeep.services.broadcaster.CallLifecycleEvent
import com.webtrit.callkeep.services.broadcaster.ConnectionEvent
Expand Down Expand Up @@ -765,30 +762,18 @@ class PhoneConnectionService : ConnectionService() {
?: throw InvalidCallMetadataException(
"startOutgoingCall: missing destination number for callId=${metadata.callId}",
)
val uri: Uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null)
val telephonyUtils = TelephonyUtils(context)

if (telephonyUtils.isEmergencyNumber(number)) {
Log.i(TAG, "onOutgoingCall, trying to call on emergency number: $number")
val uri: Uri = TelephonyUtils.buildOutgoingUri(number)

val failureMetadata =
FailureMetadata(
metadata,
"Failed to establish outgoing connection: Emergency number",
outgoingFailureType = OutgoingFailureType.EMERGENCY_NUMBER,
)

throw EmergencyNumberException(failureMetadata)
} else {
// If there is already an active call not on hold, we terminate it and start a new one,
// otherwise, we would encounter an exception when placing the outgoing call.
connectionManager.getActiveConnection()?.let {
Log.i(TAG, "onOutgoingCall, hung up previous call: $it")
it.hungUp()
}

telephonyUtils.placeOutgoingCall(uri, metadata)
// If there is already an active call not on hold, we terminate it and start a new one,
// otherwise, we would encounter an exception when placing the outgoing call.
connectionManager.getActiveConnection()?.let {
Log.i(TAG, "onOutgoingCall, hung up previous call: $it")
it.hungUp()
}

telephonyUtils.placeOutgoingCall(uri, metadata)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.webtrit.callkeep.common.TelephonyUtils
import com.webtrit.callkeep.managers.NotificationChannelManager
import com.webtrit.callkeep.models.CallConnectionState
import com.webtrit.callkeep.models.CallMetadata
import com.webtrit.callkeep.models.EmergencyNumberException
import com.webtrit.callkeep.models.FailedCallInfo
import com.webtrit.callkeep.models.FailureMetadata
import com.webtrit.callkeep.models.InvalidCallMetadataException
Expand Down Expand Up @@ -375,15 +374,11 @@ class ForegroundService :
OutgoingFailureSource.CS_CALLBACK,
failureMetaData.getThrowable(),
)
val result =
val result: Result<PCallRequestError?> =
when (failureMetaData.outgoingFailureType) {
OutgoingFailureType.UNENTITLED -> {
Result.failure(failureMetaData.getThrowable())
}

OutgoingFailureType.EMERGENCY_NUMBER -> {
Result.success(PCallRequestError(PCallRequestErrorEnum.EMERGENCY_NUMBER))
}
}
finish(result)
}
Expand Down Expand Up @@ -420,10 +415,6 @@ class ForegroundService :
@SuppressLint("MissingPermission")
core.startOutgoingCall(metadata)
logger.i("$logContext: startOutgoingCall dispatched")
} catch (e: EmergencyNumberException) {
logger.e("$logContext failed: emergency number", e)
saveFailedOutgoingCall(metadata, OutgoingFailureSource.DISPATCH_ERROR, e)
finish(Result.success(PCallRequestError(PCallRequestErrorEnum.EMERGENCY_NUMBER)))
} catch (e: Exception) {
logger.e("$logContext failed: ${e.javaClass.simpleName}: ${e.message}", e)
saveFailedOutgoingCall(metadata, OutgoingFailureSource.DISPATCH_ERROR, e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@ class FailureMetadataTest {
assertEquals(OutgoingFailureType.UNENTITLED, restored.outgoingFailureType)
}

@Test
fun `round-trip preserves EMERGENCY_NUMBER failure type`() {
val original = FailureMetadata(
callMetadata = null,
message = "emergency",
outgoingFailureType = OutgoingFailureType.EMERGENCY_NUMBER,
)
val restored = FailureMetadata.fromBundle(original.toBundle())
assertEquals(OutgoingFailureType.EMERGENCY_NUMBER, restored.outgoingFailureType)
}

@Test
fun `unknown string falls back to UNENTITLED`() {
val bundle = Bundle().apply { putString("FAILURE_OUTGOING_TYPE", "FUTURE_TYPE") }
Expand Down
9 changes: 0 additions & 9 deletions webtrit_callkeep_android/docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

`CallConnectionState` is a local domain enum (`models/CallConnectionState.kt`:
INITIALIZING/NEW/RINGING/DIALING/ACTIVE/HOLDING/DISCONNECTED) with `fromTelecomState(Int)`. It is
deliberately NOT the raw `android.telecom.Connection` state ints (untyped magic numbers, and the

Check warning on line 33 in webtrit_callkeep_android/docs/models.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (ints)
no-Telecom backend has no android `Connection`) nor the Pigeon-generated enum (Pigeon types stay
out of the model layer; the Pigeon mapping happens only at the tracker boundary).

Expand Down Expand Up @@ -116,15 +116,6 @@

---

## EmergencyNumberException

**File**: `kotlin/com/webtrit/callkeep/models/EmergencyNumberException.kt`

Thrown (and caught, then reported to Dart) when the app attempts to handle an emergency number
(`112`, `911`, etc.) that must be routed to the system dialer instead.

---

## Converters

**File**: `kotlin/com/webtrit/callkeep/models/Converters.kt`
Expand Down
Loading