你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

使用 UI 库集成通话和聊天

本文介绍如何使用 Azure 通信服务 UI 库在 Android 或 iOS 应用中集成通话和聊天功能。

先决条件

获取完整示例

你可以从 GitHub 获取完整的示例项目

使用同一应用中的通话和聊天来展示 Android 上体验的动画。

设置项目

在 Android Studio 中,创建新的项目:

  1. 在“文件”菜单中,依次选择“新建”>“新建项目”

  2. 在“新建项目”中,选择“空活动”项目模板。

    屏幕截图显示 Android Studio 中的“新建项目”对话框,其中已选择“空活动”。

  3. 选择“下一步”。

  4. 在“空活动”中,将项目命名为 UILibraryQuickStart。 对于语言,请选择“Java”或“Kotlin”。 对于最低 SDK”,请选择“API 26: Android 8.0 (Oreo)”或更高版本

  5. 选择“完成”。

    显示新建项目选项和“完成”按钮的屏幕截图。

安装包

完成以下部分以安装所需的应用程序包。

添加依赖项

在应用级 UILibraryQuickStart/app/build.gradle 文件中,添加以下依赖项

dependencies {
    ...
    implementation("com.azure.android:azure-communication-ui-calling:+")
    implementation("com.azure.android:azure-communication-ui-chat:+")
    ...
}

META-INF 排除添加到 UILibraryQuickStart/app/build.gradle android 部分:

packaging {
    resources.excludes.add("META-INF/*")
}

添加 Maven 存储库

需要两个 Maven 存储库才能集成库:

  • mavenCentral 存储库

  • Azure 包存储库

    repositories {
        ...
        mavenCentral()
        maven {
            url = URI("https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1")
        }
        ...
    }
    

使用通话和聊天功能连接到 Teams 会议

你将使用 CallComposite 连接到通话。 在用户获准接入通话后,CallComposite 会通过将状态更改为 connected 来通知你。 然后,用户可以连接到聊天线程。

当用户选择“聊天”按钮时,一个自定义按钮会被添加到 CallComposite CallComposite 会最小化,且“聊天”将显示在 Teams 中。

在 Activity_main.xml 中添加按钮和聊天容器视图

在 app/src/main/res/layout/activity_main.xml 布局文件中,添加以下代码来创建一个启动复合组件的按钮:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <LinearLayout
        android:id="@+id/buttonContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        >
        <Button
            android:id="@+id/startCallButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start Call"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="4dp"
            />
    </LinearLayout>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/chatContainer"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@+id/buttonContainer"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        >
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

下载聊天图标

  1. GitHub 存储库下载图标。
  2. 将图标保存到 UILibraryQuickStart/app/src/main/res/drawable
  3. 打开图标并将 android:fillColor 更改为 @color/white

初始化复合组件

若要初始化通话复合,请转到 MainActivity 并更新连接设置:

  • TEAM_MEETING_LINK 替换为 Teams 会议链接。
  • ACS_ENDPOINT 替换为你的 Azure 通信服务资源的终结点。
  • DISPLAY_NAME 替换为自己的名称。
  • USER_ID 替换为你的 Azure 通信服务用户 ID。
  • USER_ACCESS_TOKEN 替换为你的令牌。

获取 Azure 通信服务用户的 Teams 会议聊天线程

Graph 文档中所述,可以使用 Graph API 检索 Teams 会议详细信息。 Azure 通信服务通话 SDK 可接受完整的 Teams 会议链接或会议 ID。 它们将作为 onlineMeeting 资源的一部分返回,可在 joinWebUrl 属性下访问。

使用图形 API,还可以获取 threadID 值。 响应拥有包含 threadID 值的 chatInfo 对象。

package com.example.uilibraryquickstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.ViewGroup
import android.widget.Button
import androidx.constraintlayout.widget.ConstraintLayout
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.common.CommunicationUserIdentifier
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.CallCompositeBuilder
import com.azure.android.communication.ui.calling.models.CallCompositeCallScreenHeaderViewData
import com.azure.android.communication.ui.calling.models.CallCompositeCallScreenOptions
import com.azure.android.communication.ui.calling.models.CallCompositeCallStateCode
import com.azure.android.communication.ui.calling.models.CallCompositeCustomButtonViewData
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions
import com.azure.android.communication.ui.calling.models.CallCompositeMultitaskingOptions
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator
import com.azure.android.communication.ui.chat.ChatAdapter
import com.azure.android.communication.ui.chat.ChatAdapterBuilder
import com.azure.android.communication.ui.chat.presentation.ChatThreadView
import java.util.UUID

class MainActivity : AppCompatActivity() {
    companion object {
        private var callComposite: CallComposite? = null
        private var chatAdapter: ChatAdapter? = null
    }

    private val displayName = "USER_NAME"
    private val endpoint = "ACS_ENDPOINT"
    private val teamsMeetingLink = "TEAM_MEETING_LINK"
    private val threadId = "CHAT_THREAD_ID"
    private val communicationUserId = "USER_ID"
    private val userToken = "USER_ACCESS_TOKEN"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.startCallButton).setOnClickListener {
            startCallComposite()
        }
    }

    private fun startCallComposite() {
        val communicationTokenRefreshOptions = CommunicationTokenRefreshOptions({ userToken }, true)
        val communicationTokenCredential = CommunicationTokenCredential(communicationTokenRefreshOptions)
        val locator = CallCompositeTeamsMeetingLinkLocator(teamsMeetingLink)

        val localOptions = CallCompositeLocalOptions()
            .setCallScreenOptions(
                CallCompositeCallScreenOptions().setHeaderViewData(
                    CallCompositeCallScreenHeaderViewData().setCustomButtons(
                        listOf(
                            CallCompositeCustomButtonViewData(
                                UUID.randomUUID().toString(),
                                R.drawable.ic_fluent_chat_24_regular,
                                "Open Chat",
                            ) {
                                callComposite?.sendToBackground()
                                showChatUI()
                            }
                        )
                    )
                ))
        val callComposite = CallCompositeBuilder()
            .applicationContext(this.applicationContext)
            .credential(communicationTokenCredential)
            .displayName(displayName)
            .multitasking(CallCompositeMultitaskingOptions(true, true))
            .build()

        callComposite.addOnCallStateChangedEventHandler { callState ->
            // When a user is admitted to the Teams meeting, the call state becomes connected.
            // Only users admitted to the meeting can connect to the meeting's chat thread.
            if (callState.code == CallCompositeCallStateCode.CONNECTED) {
                connectChat()
            }
        }

        callComposite.launch(this, locator, localOptions)
        MainActivity.callComposite = callComposite
    }

    private fun connectChat() {
        if (chatAdapter != null)
            return

        val communicationTokenRefreshOptions =
            CommunicationTokenRefreshOptions( { userToken }, true)
        val communicationTokenCredential =
            CommunicationTokenCredential(communicationTokenRefreshOptions)

        val chatAdapter = ChatAdapterBuilder()
            .endpoint(endpoint)
            .credential(communicationTokenCredential)
            .identity(CommunicationUserIdentifier(communicationUserId))
            .displayName(displayName)
            .threadId(threadId)
            .build()
        chatAdapter.connect(applicationContext)
        MainActivity.chatAdapter = chatAdapter
    }

    private fun showChatUI() {
        chatAdapter?.let {
            // Create Chat Composite View
            val chatView = ChatThreadView(this, chatAdapter)
            val chatContainer = findViewById<ConstraintLayout>(R.id.chatContainer)
            chatContainer.removeAllViews()
            chatContainer.addView(
                chatView,
                ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
            )
        }
    }
}

先决条件

获取完整示例

你可以从 GitHub 获取完整的示例项目

使用同一应用中的通话和聊天来展示 iOS 上体验的动画。

设置项目

完成以下部分以设置快速入门项目。

创建新的 Xcode 项目

在 Xcode 中创建新项目:

  1. 在“文件”菜单中,选择“新建”>“项目”。

  2. 在“为新项目选择模板”中,选择 iOS 平台并选择“应用”应用程序模板。 快速入门使用 UIKit Storyboard。 快速入门不会创建测试,因此可以清除“包括测试”复选框。

    显示 Xcode 新建项目对话框的屏幕截图,其中选择了 iOS 和“应用”模板。

  3. 在“为新建项目选择选项”中,在产品名称处输入 UILibraryQuickStart。 请在“界面”处选择“Storyboard”。

    显示在 Xcode 中设置新建项目选项的屏幕截图。

安装包和依赖项

  1. (可选)对于 MacBook with M1,请在 Xcode 中安装并启用 Rosetta

  2. 在项目根目录中,运行 pod init 以创建 Podfile。 如果遇到错误,请将 CocoaPods 更新为当前版本。

  3. 请将以下代码添加到 Podfile 中。 将 UILibraryQuickStart 替换为你的项目名称。

    platform :ios, '15.0'
    
    target 'UILibraryQuickStart' do
        use_frameworks!
          pod 'AzureCommunicationUICalling', '1.12.0-beta.1'
          pod 'AzureCommunicationUIChat', '1.0.0-beta.4'
    end
    
  4. 运行 pod install --repo-update

  5. 在 Xcode 中,打开 generated.xcworkspace 文件。

请求访问设备硬件

若要访问设备的硬件(包括麦克风和摄像头),请更新应用的信息属性列表。 将关联的值设置为一个字符串,该字符串将包含在系统用于向用户请求访问权限的对话框中。

  1. 右键单击项目树的 Info.plist 条目,然后选择“打开为”>“源代码” 。 将以下代码行添加到顶层 <dict> 节,然后保存文件。

    <key>NSCameraUsageDescription</key>
    <string></string>
    <key>NSMicrophoneUsageDescription</key>
    <string></string>
    

    以下是 Xcode 文件中 Info.plist 源代码的示例:

    显示 Xcode 文件中信息属性列表的示例源代码的屏幕截图。

  2. 若要验证是否已正确添加设备权限请求,请选择“打开为”>“属性列表”。 检查信息属性列表是否如下例所示:

    显示 Xcode 中摄像头和麦克风设备隐私的屏幕截图。

关闭 Bitcode

在 Xcode 项目中的“生成设置”下,将“启用 Bitcode”选项设置为“否”。 若要查找设置,请将筛选器从“基本”更改为“全部”,或使用搜索栏。

显示用于关闭 Bitcode 的“生成设置”选项的屏幕截图。

下载聊天图标

  1. GitHub 存储库下载图标。
  2. 打开下载的文件,并将 fill 更改为 fill="#FFFFFF"
  3. 在 Xcode 中,转到“资产”。 创建新的映像集并将其命名为 ic_fluent_chat_24_regular。 将下载的文件选为通用图标。

初始化复合组件

若要初始化复合,请转到 ViewController 并更新连接设置:

  • TEAM_MEETING_LINK 替换为 Teams 会议链接。
  • ACS_ENDPOINT 替换为你的 Azure 通信服务资源的终结点。
  • DISPLAY_NAME 替换为自己的名称。
  • USER_ID 替换为你的 Azure 通信服务用户 ID。
  • USER_ACCESS_TOKEN 替换为你的令牌。

获取 Azure 通信服务用户的 Teams 会议聊天线程

Graph 文档中所述,可以使用 Graph API 检索 Teams 会议详细信息。 Azure 通信服务通话 SDK 可接受完整的 Teams 会议链接或会议 ID。 它们将作为 onlineMeeting 资源的一部分返回,可在 joinWebUrl 属性下访问。

使用图形 API,还可以获取 threadID 值。 响应拥有包含 threadID 值的 chatInfo 对象。

import UIKit
import AzureCommunicationCalling
import AzureCommunicationUICalling
import AzureCommunicationUIChat
    
class ViewController: UIViewController {
    private let displayName = "USER_NAME"
    private let endpoint = "ACS_ENDPOINT"
    private let teamsMeetingLink = "TEAM_MEETING_LINK"
    private let chatThreadId = "CHAT_THREAD_ID"
    private let communicationUserId = "USER_ID"
    private let userToken = "USER_ACCESS_TOKEN"
    
    
    private var callComposite: CallComposite?
    private var chatAdapter: ChatAdapter?
    private var chatCompositeViewController: ChatCompositeViewController?
    
    private var startCallButton: UIButton?
    private var chatContainerView: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        initControlBar()
    }

    @objc private func startCallComposite() {
        let callCompositeOptions = CallCompositeOptions(
            enableMultitasking: true,
            enableSystemPictureInPictureWhenMultitasking: true,
            displayName: displayName)
        
        let communicationTokenCredential = try! CommunicationTokenCredential(token: userToken)

        let callComposite = self.callComposite ?? CallComposite(credential: communicationTokenCredential, withOptions: callCompositeOptions)
        self.callComposite = callComposite
        
        callComposite.events.onCallStateChanged = { [weak self] callState in
            if callState.requestString == CallState.connected.requestString {
                self?.connectChat()
            }
        }
        
        let chatCustomButton = CustomButtonViewData(
            id: UUID().uuidString,
            image: UIImage(named: "ic_fluent_chat_24_regular")!,
            title: "Chat") { [weak self] _ in
                self?.callComposite?.isHidden = true
                self?.showChat()
            }
        let callScreenHeaderViewData = CallScreenHeaderViewData(customButtons: [chatCustomButton])
        let localOptions = LocalOptions(callScreenOptions: CallScreenOptions(headerViewData: callScreenHeaderViewData))
        callComposite.launch(locator: .teamsMeeting(teamsLink: teamsMeetingLink), localOptions: localOptions)
    }
    
    @objc private func connectChat() {
        let communicationIdentifier = CommunicationUserIdentifier(communicationUserId)
        guard let communicationTokenCredential = try? CommunicationTokenCredential(
            token: userToken) else {
            return
        }

        self.chatAdapter = ChatAdapter(
            endpoint: endpoint,
            identifier: communicationIdentifier,
            credential: communicationTokenCredential,
            threadId: chatThreadId,
            displayName: displayName)

        Task { @MainActor in
            guard let chatAdapter = self.chatAdapter else {
                return
            }
            try await chatAdapter.connect()
        }
    }
        
    @objc private func showChat() {
        guard let chatAdapter = self.chatAdapter,
              let chatContainerView = self.chatContainerView,
              self.chatCompositeViewController == nil else {
            return
        }
    
        let chatCompositeViewController = ChatCompositeViewController(with: chatAdapter)
        
        self.addChild(chatCompositeViewController)
        chatContainerView.addSubview(chatCompositeViewController.view)
        
        chatCompositeViewController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            chatCompositeViewController.view.topAnchor.constraint(equalTo: chatContainerView.topAnchor),
            chatCompositeViewController.view.bottomAnchor.constraint(equalTo: chatContainerView.bottomAnchor),
            chatCompositeViewController.view.leadingAnchor.constraint(equalTo: chatContainerView.leadingAnchor),
            chatCompositeViewController.view.trailingAnchor.constraint(equalTo: chatContainerView.trailingAnchor)
        ])
        
        chatCompositeViewController.didMove(toParent: self)
        self.chatCompositeViewController = chatCompositeViewController
    }
        
    private func initControlBar() {
        let startCallButton = UIButton()
        self.startCallButton = startCallButton
        startCallButton.layer.cornerRadius = 10
        startCallButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16)
        startCallButton.backgroundColor = .systemBlue
        startCallButton.setTitle("Call", for: .normal)
        startCallButton.addTarget(self, action: #selector(startCallComposite), for: .touchUpInside)
        startCallButton.translatesAutoresizingMaskIntoConstraints = false
                        
        let margin: CGFloat = 32.0
        
        let buttonsContainerView = UIView()
        buttonsContainerView.backgroundColor = .clear
        
        let buttonsStackView = UIStackView(arrangedSubviews: [startCallButton])
        buttonsStackView.axis = .horizontal
        buttonsStackView.alignment = .center
        buttonsStackView.distribution = .equalSpacing
        buttonsStackView.spacing = 10
        buttonsStackView.translatesAutoresizingMaskIntoConstraints = false
        buttonsStackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        buttonsContainerView.addSubview(buttonsStackView)

        buttonsContainerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            buttonsStackView.topAnchor.constraint(equalTo: buttonsContainerView.topAnchor, constant: 8),
            buttonsStackView.bottomAnchor.constraint(equalTo: buttonsContainerView.bottomAnchor, constant: -8),
            buttonsStackView.leadingAnchor.constraint(equalTo: buttonsContainerView.leadingAnchor, constant: 16),
        ])
        
        let chatContainerView = UIView()
        self.chatContainerView = chatContainerView
        
        let verticalStackView = UIStackView(arrangedSubviews: [
            buttonsContainerView,
            chatContainerView
            ])
        verticalStackView.axis = .vertical
        verticalStackView.alignment = .fill
        verticalStackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(verticalStackView)
        
        let margins = view.safeAreaLayoutGuide
        let constraints = [
            verticalStackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
            verticalStackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
            verticalStackView.topAnchor.constraint(equalTo: margins.topAnchor, constant: margin),
            verticalStackView.bottomAnchor.constraint(equalTo: margins.bottomAnchor, constant: -margin)
        ]
        NSLayoutConstraint.activate(constraints)
    }
}

运行代码

运行代码以在设备上构建并运行应用。

其他功能

用例列表提供有关其他功能的详细信息。

向移动应用添加通知

Azure 通信服务与 Azure 事件网格Azure 通知中心集成,因此你可以向 Azure 中的应用添加推送通知。 可以使用推送通知将信息从你的应用程序发送到用户的移动设备。 推送通知可显示对话、播放声音或显示传入的通话 UI。