Core-Telecom

Core-Telecom 库通过提供一组强大且一致的 API,简化了将通话应用与 Android 平台集成的流程

如果您想探索实际实现,可以在 GitHub 上找到示例应用:

设置 Core-Telecom

androidx.core:core-telecom 依赖项添加到应用的 build.gradle 文件中:

dependencies {
    implementation ("androidx.core:core-telecom:1.0.0")
}

AndroidManifest.xml 中声明 MANAGE_OWN_CALLS 权限:

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

注册您的应用程序

使用 CallsManager 向 Android 注册您的通话应用,以便开始向系统添加通话。注册时,请指定应用的功能(例如,音频、视频支持):

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

通话管理

使用 Core-Telecom API 创建和管理通话生命周期。

创建通话

CallAttributesCompat 对象定义了唯一调用的属性,该调用可以具有以下特征:

  • displayName:调用方名称。
  • address:通话地址(例如电话号码、会议链接)。
  • direction:来电或去电。
  • callType:音频或视频。
  • callCapabilities:支持转接和保留。

以下示例展示了如何创建来电:

fun createIncomingCallAttributes(
    callerName: String,
    callerNumber: String,
    isVideoCall: Boolean): CallAttributesCompat {
    val addressUri = Uri.parse("YourAppScheme:$callerNumber")

    // Define capabilities supported by your call.
    val callCapabilities = CallAttributesCompat.CallCapability(
        supportsSetInactive = CallAttributesCompat.SUPPORTS_SET_INACTIVE // Call can be made inactive (implies hold)
    )

    return CallAttributesCompat(
        displayName = callerName,
        address = addressUri,
        direction = CallAttributesCompat.DIRECTION_INCOMING,
        callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
        callCapabilitiesCompat = callCapabilities
    )
}

添加通话

使用 callsManager.addCallCallAttributesCompat 以及回调向系统添加新通话并管理远程界面更新。addCall 区块中的 callControlScope 主要允许您的应用转换通话状态并接收音频更新:

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onAnswerCall, // Watch needs to know if it can answer the call.
        onSetCallDisconnected,
        onSetCallActive,
        onSetCallInactive
    ) {
        // The call was successfully added once this scope runs.
        callControlScope = this
    }
}
catch(addCallException: Exception){
   // Handle the addCall failure.
}

接听来电

CallControlScope 内接听来电:

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

拒接来电

使用 CallControlScope 内的 DisconnectCause.REJECTED 通过 disconnect() 拒绝通话:

disconnect(DisconnectCause(DisconnectCause.REJECTED))

激活去电

在远程方接听后,将去电设置为有效:

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

将通话置于保持状态

使用 setInactive() 将通话置于保持状态:

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

断开通话

使用 disconnect()DisconnectCause 断开通话:

disconnect(DisconnectCause(DisconnectCause.LOCAL))

管理通话音频端点

使用 CallControlScope 内的 currentCallEndpointavailableEndpointsisMuted Flow 观察和管理音频端点

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

使用 requestEndpointChange() 更改有效音频设备:

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

前台支持

该库使用 ConnectionService(Android 13 API 级别 33 及更低版本)或 foregroundtypes(Android 14 API 级别 34 及更高版本)来实现前台支持。

根据前台要求,应用必须发布通知,让用户知道应用正在前台运行。

为确保应用获得前台执行优先级,请在添加与平台的通话后立即创建通知。当应用结束通话或通知不再有效时,前台优先级会被移除。

详细了解前台服务

远程 Surface 支持

远程设备(智能手表、蓝牙耳机、Android Auto)无需直接与手机互动,即可进行通话管理。您的应用必须实现提供给 CallsManager.addCall 的回调 lambda(onAnswerCallonSetCallDisconnectedonSetCallActiveonSetCallInactive),以处理由这些设备发起的操作。

当发生远程操作时,系统会调用相应的 lambda。

如果 lambda 成功完成,则表示命令已处理完毕。如果无法遵守该命令,lambda 应抛出异常。

正确实现可确保在不同设备上实现无缝通话控制。 使用各种远程界面进行全面测试。

附加电话信息

除了管理通话状态和通话音频路由之外,该库还支持通话扩展功能,这些功能是可选功能,您的应用可以实现这些功能,以便在 Android Auto 等远程途径上提供更丰富的通话体验。这些功能包括会议室、通话静音和额外的通话图标。当应用实现扩展程序时,应用提供的信息将与所有已连接的设备同步,这些设备还支持在其界面中显示这些扩展程序。这意味着,用户还可以在远程设备上使用这些功能。

创建包含扩展功能的通话

创建通话时,您可以不使用 CallManager#addCall 来创建通话,而是使用 CallManager#addCallWithExtensions,这会使应用能够访问名为 ExtensionInitializationScope 的不同范围。此范围允许应用初始化其支持的一组可选扩展程序。此外,此范围还提供了一个额外的方法 onCall,该方法在扩展功能交换和初始化完成后提供一个 CallControlScope 返回到应用。

scope.launch {
    mCallsManager.addCallWithExtensions(
        attributes,
        onAnswer,
        onDisconnect,
        onSetActive,
        onSetInactive
    ) {
        // Initialize extension-specific code...

        // After the call has been initialized, perform in-call actions
        onCall {
            // Example: process call state updates
            callStateFlow.onEach { newState ->
                // handle call state updates and notify telecom
            }.launchIn(this)

            // Use initialized extensions...
        }
    }
}

支持通话参与者

如果您的应用支持会议或群组通话的通话参与者,请使用 addParticipantExtension 声明支持此扩展程序,并在参与者发生变化时使用相关 API 更新远程界面。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial participants state in the call.
        val participantExtension = addParticipantExtension(
            initialParticipants,
            initialActiveParticipant
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
        }
    }

除了通知远程途径通话中的参与者之外,还可以使用 ParticipantExtension#updateActiveParticipant 更新活跃的参与者。

还支持与通话参与者相关的可选操作。 应用可以使用 ParticipantExtension#addRaiseHandSupport 来支持参与者在通话中举手的概念,并查看哪些其他参与者也举了手。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial list of participants in the call.
        val participantExtension = addParticipantExtension(initialParticipants)
        // Notifies Jetpack that this app supports the notion of participants
        // being able to raise and lower their hands.
        val raiseHandState = participantExtension.addRaiseHandSupport(
                initialRaisedHands
            ) { onHandRaisedStateChanged ->
                // handle this user's raised hand state changed updates from
                // remote surfaces.
            }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
            // notify remote surfaces of which of the participants have their
            // hands raised
            raisedHandsFlow.onEach { newRaisedHands ->
                raiseHandState.updateRaisedHands(newRaisedHands)
            }.launchIn(this)
        }
    }

支持来电静音

通话静音功能允许用户请求应用将通话的输出音频静音,而无需实际将设备的麦克风静音。此功能按每次通话进行管理,因此 Jetpack 可处理在 VOIP 通话处于有效状态时管理正在进行的移动网络通话的全局静音状态的复杂性。这样一来,在多通话场景中,静音传出音频时出错的可能性会更低,同时还可实现实用功能,例如当用户在通话静音已启用的情况下说话时,系统会显示“您在说话吗”的提示。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for locally silencing the call's outgoing audio and
        // register a handler for when the user changes the call silence state
        // from a remote surface.
        val callSilenceExtension = addLocalCallSilenceExtension(
            initialCallSilenceState = false
        ) { newCallSilenceStateRequest ->
            // handle the user's request to enable/disable call silence from
            // a remote surface
        }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's call silence state changes, update remote
            // surfaces of the new state.
            callSilenceState.onEach { isSilenced ->
                callSilenceExtension.updateIsLocallySilenced(isSilenced)
            }.launchIn(this)
        }
    }

支持通话图标

通话图标允许应用指定一个自定义图标,用于表示通话期间要在远程界面上显示的通话。此图标还可以在通话期间更新。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for a custom call icon to be displayed during the
        // lifetime of the call.
        val callIconExtension = addCallIconExtension(
            initialCallIconUri = initialUri
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's icon changes, update remote surfaces by providing
            // the new URI.
            callIconUri.onEach { newIconUri ->
                callIconExtension.updateCallIconUri(newIconUri)
            }.launchIn(this)
        }
    }

与系统通话记录集成

Core-Telecom 可让您的应用与系统通话记录集成。此功能可让使用 VoIP 应用拨打或接听的电话显示在系统拨号器的通话记录中,从而允许用户使用拨号器发起回拨。

启用通话记录集成

如需启用此功能,您的应用必须注册以在 AndroidManifest.xml 中处理 TelecomManager.ACTION_CALL_BACK intent,并提供 Activity 来处理回调。例如,假设 VoipCallbackActivity 是处理回调的 activity,那么您的清单应如下所示:

在清单中注册 intent

<activity
    android:name=".VoipCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.action.CALL_BACK" />
    </intent-filter>
</activity>

处理回调 intent

当用户从系统拨号器发起回拨时,系统会启动您注册的 activity,并使用 TelecomManager.ACTION_CALL_BACK 操作。 该 intent 包含您最初使用 CallsManager 添加通话时提供给应用的 TelecomManager.EXTRA_UUID

CallControlScope 方法 getCallId 会返回相应调用的唯一 ID。

您应保存在应用内通过 UUID 标识的开始通话所需的所有信息(例如,群组通话的电话号码列表),以便在触发回调时重新构建并拨打该电话。

class VoipCallbackActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (intent?.action == TelecomManager.ACTION_CALL_BACK) {
            val uuidString = intent.getStringExtra(TelecomManager.EXTRA_UUID)
            if (uuidString != null) {
                val uuid = UUID.fromString(uuidString)
                
                // Retrieve your persisted call info using the UUID
                val callData = CallRepository.getCallByUuid(uuid)
                
                if (callData != null) {
                    // Place the call again using your app's logic
                    // ...
                }
            }
        }
        finish()
    }
}

选择不集成通话记录

默认情况下,通话会记录到系统通话记录中。您可以在创建 CallAttributesCompat 时将 isLogged 参数设置为 false,从而针对每次调用选择停用此功能:

val callAttributes = CallAttributesCompat(
    displayName = callerName,
    address = addressUri,
    direction = CallAttributesCompat.DIRECTION_OUTGOING,
    isLogExcluded = true // Opt-out of system call logging
)