# 客户端实现（全功能版本） (/zh-CN/solutions/iot-apaas/build/implement-core-features/client-call)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="android" />
本文介绍如何使用声网灵隼**全功能版本**的客户端示例项目在 Android 客户端实现设备管理、呼叫、通话、告警等功能。

:::info[信息]
**全功能版本的客户端示例项目**提供用户注册/登录、绑定/解绑设备、Wi-Fi 二维码配网、呼叫、实时音视频等功能。

如果你没有自研的设备管理等模块，声网建议你使用全功能版本示例项目。
:::

## 前提条件 [#前提条件]

开始前，请确保你的开发环境满足以下条件：

- Android Studio 4.1.2 或以上版本。
- Android API level 16 或以上。
- 运行 Android 4.1 或以上版本的移动设备。
- 参考[开通并配置声网灵隼物联网服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)开通灵隼服务

## 跑通示例项目 [#跑通示例项目]

### 跑通步骤 [#跑通步骤]

1. 从 [GitHub](https://github.com/AgoraIO-Community/ag-iot-android-app) 上下载最新版本的 Android 客户端示例项目。

2. 使用 Android Studio 加载工程后，在 **app&gt;&gt;res&gt;&gt;values&gt;&gt;config.xml** 文件中，将以下配置项修改为你在灵隼控制台的**应用配置&gt;&gt;开发者选项**页面中获取的相关参数。详见[开通声网灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)。

	```xml
	<!-- 声网灵隼控制台的应用配置>>开发者选项中的 App ID-->
	<string name="AGORA_APPID">your_app_id</string>
	<!-- 声网灵隼控制台的应用配置>>开发者选项中的 Project ID -->
	<string name="PROJECT_ID">your_project_id</string>
	<!-- 声网灵隼控制台的应用配置>>开发者选项中的 Master Server URL -->
	<string name="MASTER_SERVER_URL">your_master_server</string>
	<!-- 声网灵隼控制台的应用配置>>开发者选项中的 Slave Server URL -->
	<string name="SALVE_SERVER_URL">your_slave_serveri</string>
	```

3. 连接上 Android 设备后，用 Android Studio 编译并运行示例项目。

4. 在登录页面选择注册新账户或者登录已有账户，目前国内只支持手机号注册。

<DocsImage alt="登录页面" src="https://web-cdn.agora.io/docs-files/1656331016147" displayWidth="40%" />

5. 在登录成功后的页面，可以点击 **+** 号添加新设备（设备产品码请参考[开通灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)文档获取产品二维码章节），或者选择已经添加设备实时预览对应设备的画面（设备端示例项目请参考[实现设备端功能](/zh-CN/solutions/iot-apaas/build/implement-core-features/device-call)文档）。

<DocsImage alt="添加设备" src="https://web-cdn.agora.io/docs-files/1656330954299" displayWidth="40%" />

### （可选）开通离线呼叫 [#可选开通离线呼叫]

当 Android 端应用不再运行时（包括不在后台运行时或应用进程不存在时），离线呼叫可以实现在有设备端呼叫客户端时，客户端能收到后台服务，并启动应用来接听设备端呼叫。你可以参考以下步骤开通离线呼叫：

1. 在 `app\src\main\res\values\config.xml` 文件中配置离线呼叫的相应参数。

	```xml
	<!-- 声网灵隼控制台的应用配置>>开发者选项中的 Easemob App Key -->
	<string name="EASEMOB_APPKEY">your_easemob_appkey</string>
	<!-- 配置 FCM 离线推送参数 -->
	<bool name="FCM_ENBALE">false</bool>
	<string name="FCM_SENDERID"></string>
	<!-- 配置小米手机离线推送参数 -->
	<string name="MI_PUSH_APPID"></string>
	<string name="MI_PUSH_APPKEY"></string>
	<!-- 配置 OPPO 手机离线推送参数 -->
	<string name="OPPO_PUSH_APPKEY"></string>
	<string name="OPPO_PUSH_APPSECRET"></string>
	<!-- 配置 VIVIO 手机离线推送参数  -->
	<integer name="VIVO_PUSH_APPID">0</integer>
	<string name="VIVO_PUSH_APPKEY"></string>
	<!-- 配置华为手机离线推送参数，注意‘appid=’不要删除 -->
	<string name="HUAWEI_PUSH_APPKID">appid=106736987</string>
	```

2. 恢复  `\app\src\main\AndroidManifest.xml` 文件中关于离线推送注释的代码。主要包括离线推送权限配置、离线推送账号配置和离线推送服务配置。

	```xml
	<!--    &lt;!&ndash; ==================== 离线推送权限 BEGIN ==================== &ndash;&gt;-->
	<!--        &lt;!&ndash; Mi推送权限配置 start &ndash;&gt;-->
	<!--        <permission-->
	<!--            android:name="${applicationId}.permission.MIPUSH_RECEIVE"-->
	<!--            android:protectionLevel="signature" />-->
	<!--        <uses-permission android:name="${applicationId}.permission.MIPUSH_RECEIVE" />-->
	<!--        <uses-permission android:name="android.permission.GET_TASKS"/>-->
	<!--        &lt;!&ndash; Mi推送权限配置 end &ndash;&gt;-->

	<!--        &lt;!&ndash; HUAWEI 推送权限配置 start &ndash;&gt;-->
	<!--        <uses-permission android:name="android.permission.WAKE_LOCK" />-->
	<!--        &lt;!&ndash; HUAWEI 推送权限配置 end &ndash;&gt;-->

	<!--        &lt;!&ndash; OPPO推送权限配置 start &ndash;&gt;-->
	<!--        <uses-permission android:name="com.coloros.mcs.permission.RECIEVE_MCS_MESSAGE"/>-->
	<!--        <uses-permission android:name="com.heytap.mcs.permission.RECIEVE_MCS_MESSAGE"/>-->
	<!--        &lt;!&ndash; OPPO推送权限配置 end &ndash;&gt;-->
	<!--    &lt;!&ndash; ==================== 离线推送权限 END ==================== &ndash;&gt;-->

	<!--        &lt;!&ndash; ==================== 离线推送账号配置 BEGIN ==================== &ndash;&gt;-->
	<!--            &lt;!&ndash; 环信账号配置 &ndash;&gt;-->
	<!--            <meta-data-->
	<!--                android:name="EASEMOB_APPKEY"-->
	<!--                android:value="@string/EASEMOB_APPKEY" />-->

	<!--            &lt;!&ndash; Mi账号配置 &ndash;&gt;-->
	<!--            <meta-data-->
	<!--                android:name="com.mi.push.api_key"-->
	<!--                android:value="@string/MI_PUSH_APPKEY" />-->
	<!--            <meta-data-->
	<!--                android:name="com.mi.push.app_id"-->
	<!--                android:value="@string/MI_PUSH_APPID" />-->

	<!--            &lt;!&ndash;华为账号配置 &ndash;&gt;-->
	<!--            <meta-data-->
	<!--                android:name="com.huawei.hms.client.appid"-->
	<!--                android:value="@string/HUAWEI_PUSH_APPKID" />-->
	<!--            <meta-data-->
	<!--                android:name="push_kit_auto_init_enabled"-->
	<!--                android:value="true"/>-->

	<!--            &lt;!&ndash; Oppo账号配置 &ndash;&gt;-->
	<!--            <meta-data-->
	<!--                android:name="com.oppo.push.api_key"-->
	<!--                android:value="@string/OPPO_PUSH_APPKEY" />-->
	<!--            <meta-data-->
	<!--                android:name="com.oppo.push.app_secret"-->
	<!--                android:value="@string/OPPO_PUSH_APPSECRET" />-->

	<!--            &lt;!&ndash; VIVO账号配置 &ndash;&gt;-->
	<!--            <meta-data-->
	<!--                android:name="com.vivo.push.api_key"-->
	<!--                android:value="@string/VIVO_PUSH_APPKEY" />-->
	<!--            <meta-data-->
	<!--                android:name="com.vivo.push.app_id"-->
	<!--                android:value="@integer/VIVO_PUSH_APPID" />-->
	<!--        &lt;!&ndash; ==================== 离线推送账号配置 END ==================== &ndash;&gt;-->

	<!--        &lt;!&ndash; ==================== 离线推送服务 BEGIN ==================== &ndash;&gt;-->
	<!--            &lt;!&ndash; Mi推送服务配置 start &ndash;&gt;-->
	<!--            <service-->
	<!--                android:name="com.xiaomi.push.service.XMPushService"-->
	<!--                android:enabled="true"-->
	<!--                android:process=":pushservice" />-->
	<!--            <service-->
	<!--                android:name="com.xiaomi.push.service.XMJobService"-->
	<!--                android:enabled="true"-->
	<!--                android:exported="false"-->
	<!--                android:permission="android.permission.BIND_JOB_SERVICE"-->
	<!--                android:process=":pushservice" /> &lt;!&ndash;注：此service必须在3.0.1版本以后（包括3.0.1版本）加入&ndash;&gt;-->
	<!--            <service-->
	<!--                android:name="com.xiaomi.mipush.sdk.PushMessageHandler"-->
	<!--                android:enabled="true"-->
	<!--                android:exported="true" />-->
	<!--            <service-->
	<!--                android:name="com.xiaomi.mipush.sdk.MessageHandleService"-->
	<!--                android:enabled="true" /> &lt;!&ndash;注：此service必须在2.2.5版本以后（包括2.2.5版本）加入&ndash;&gt;-->
	<!--            <receiver-->
	<!--                android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"-->
	<!--                android:exported="true">-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />-->
	<!--                    <category android:name="android.intent.category.DEFAULT" />-->
	<!--                </intent-filter>-->
	<!--            </receiver>-->
	<!--            <receiver-->
	<!--                android:name="com.xiaomi.push.service.receivers.PingReceiver"-->
	<!--                android:exported="false"-->
	<!--                android:process=":pushservice">-->

	<!--                <intent-filter>-->
	<!--                    <action android:name="com.xiaomi.push.PING_TIMER" />-->
	<!--                </intent-filter>-->
	<!--            </receiver>-->
	<!--            <receiver android:name="com.hyphenate.push.platform.mi.EMMiMsgReceiver">-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />-->
	<!--                </intent-filter>-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />-->
	<!--                </intent-filter>-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.xiaomi.mipush.ERROR" />-->
	<!--                </intent-filter>-->
	<!--            </receiver>-->
	<!--            &lt;!&ndash; Mi推送服务配置 end&ndash;&gt;-->

	<!--            &lt;!&ndash; HuaWei 推送服务配置 start &ndash;&gt;-->
	<!--            <service android:name="io.agora.iotlinkdemo.huanxin.HMSPushService"-->
	<!--                android:exported="false">-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.huawei.push.action.MESSAGING_EVENT" />-->
	<!--                </intent-filter>-->
	<!--            </service>-->
	<!--            &lt;!&ndash; HuaWei 推送服务配置 end &ndash;&gt;-->

	<!--            &lt;!&ndash; Oppo推送服务配置 start &ndash;&gt;-->
	<!--            <service-->
	<!--                android:name="com.heytap.msp.push.service.CompatibleDataMessageCallbackService"-->
	<!--                android:permission="com.coloros.mcs.permission.SEND_MCS_MESSAGE"-->
	<!--                android:exported="false" >-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.coloros.mcs.action.RECEIVE_MCS_MESSAGE"/>-->
	<!--                </intent-filter>-->
	<!--            </service> &lt;!&ndash;兼容Q以下版本&ndash;&gt;-->
	<!--            <service-->
	<!--                android:name="com.heytap.msp.push.service.DataMessageCallbackService"-->
	<!--                android:permission="com.heytap.mcs.permission.SEND_PUSH_MESSAGE"-->
	<!--                android:exported="false">-->
	<!--                <intent-filter>-->
	<!--                    <action android:name="com.heytap.mcs.action.RECEIVE_MCS_MESSAGE"/>-->
	<!--                    <action android:name="com.heytap.msp.push.RECEIVE_MCS_MESSAGE"/>-->
	<!--                </intent-filter>-->
	<!--            </service> &lt;!&ndash;兼容Q版本&ndash;&gt;-->
	<!--            &lt;!&ndash; Oppo推送服务配置 end &ndash;&gt;-->

	<!--            &lt;!&ndash; VIVO推送服务配置 start &ndash;&gt;-->
	<!--            <service-->
	<!--                android:name="com.vivo.push.sdk.service.CommandClientService"-->
	<!--                android:exported="true" />-->
	<!--            <activity-->
	<!--                android:name="com.vivo.push.sdk.LinkProxyClientActivity"-->
	<!--                android:exported="false"-->
	<!--                android:screenOrientation="portrait"-->
	<!--                android:theme="@android:style/Theme.Translucent.NoTitleBar" />-->
	<!--            <receiver-->
	<!--                android:name="com.hyphenate.push.platform.vivo.EMVivoMsgReceiver"-->
	<!--                android:exported="false" >-->
	<!--                <intent-filter>-->
	<!--                    &lt;!&ndash; 接收 push 消息 &ndash;&gt;-->
	<!--                    <action android:name="com.vivo.pushclient.action.RECEIVE" />-->
	<!--                </intent-filter>-->
	<!--            </receiver>-->
	<!--            &lt;!&ndash; VIVO推送服务配置 end &ndash;&gt;-->
	<!--        &lt;!&ndash; ==================== 离线推送服务 END ==================== &ndash;&gt;-->
	```

3. 恢复 `app\build.gradle` 中标注为 `各手机厂商离线推送 SDK` 的代码 ，以集成各家手机厂商的离线推送 SDK。

	```gradle
	//    // 各手机厂商离线推送SDK
	//    //implementation(name: 'oppo_push_v2.1.0', ext: 'aar')
	//    implementation 'com.huawei.hms:push:4.0.2.300'
	//    implementation files('libs/mi_push_v3.6.12.jar')
	//    implementation files('libs/vivo_push_v2.3.1.jar')
	//    implementation 'io.hyphenate:hyphenate-push:0.1.0'  // 环信
	//
	//    // Google firebase 离线推送
	//    implementation 'com.google.android.gms:play-services-base:18.1.0'
	//    implementation 'com.google.firebase:firebase-messaging:23.0.6'
	```

4.  `\app\src\main\io\agora\iotlinkdemo\base\PushApplication.java` 文件的 `PushApplication.onCreate()` 方法中，恢复 import 语句和标注为 `初始化环信的离线推送` 的代码。

	```java
	//import io.agora.iotlinkdemo.huanxin.EmAgent;

	//
	// 初始化环信的离线推送
	//
	//        if (mMetaData != null)
	//        {
	//            EmAgent.EmPushParam  pushParam = new EmAgent.EmPushParam();
	//            pushParam.mFcmSenderId = mMetaData.getString("com.fcm.push.senderid", "");
	//            pushParam.mMiAppId = mMetaData.getString("com.mi.push.app_id", "");
	//            pushParam.mMiAppKey = mMetaData.getString("com.mi.push.api_key", "");
	//            pushParam.mOppoAppKey = mMetaData.getString("com.oppo.push.api_key", "");
	//            pushParam.mOppoAppSecret = mMetaData.getString("com.oppo.push.app_secret", "");;
	//            pushParam.mVivoAppId = String.valueOf(mMetaData.getInt("com.vivo.push.app_id", 0));
	//            pushParam.mVivoAppKey = mMetaData.getString("com.vivo.push.api_key", "");
	//            pushParam.mHuaweiAppId = mMetaData.getString("com.huawei.hms.client.appid", "");
	//            EmAgent.getInstance().initialize(this,  pushParam);
	//        }
			}
	```

5. 在 `\app\src\main\io\agora\iotlinkdemo\base\PushApplication.java` 文件的 `PushApplication.initializeEngine()` 方法中，恢复以下代码：

	```java
	initParam.mPusherId = EmAgent.getInstance().getEid();
	```

6.在  `\app\src\main\io\agora\iotlinkdemo\huanxin\EmAgent.java` 文件，恢复全部代码。

对于**华为推送**，你还要进行以下额外步骤：

1. 恢复以下文件中的全部代码：
	 - `\app\src\main\io\agora\iotlinkdemo\huanxin\HMSPushHelper.java`
	 - `\app\src\main\io\agora\iotlinkdemo\huanxin\HMSPushService.java`

2. 从 [AppGallery Connect](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/android-config-agc-0000001050170137) 下载 `agconnect-services.json` 文件，覆盖 `\app\agconnect-services.json` 文件。

3. 将证书文件 `iot.jks` 的 SHA256 指纹，填写到 [AppGallery Connect](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/android-config-agc-0000001050170137) 中。

## 客户端实现 [#客户端实现]

Android 客户端的核心逻辑如下。

### 配置离线推送账号信息 [#配置离线推送账号信息]

在你的 Android 项目中，修改 `AndroidManifest.xml` 文件配置离线推送账号信息。参考[开通声网灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)获取账号信息。

```xml
<!-- 离线推送的账号配置 start -->
<!-- 环信账号配置 -->
<meta-data
 android:name="EASEMOB_APPKEY"
 android:value="@string/EASEMOB_APPKEY" />

<!-- Firebase账号配置 -->
<meta-data
 android:name="com.fcm.push.enable"
 android:value="@bool/FCM_ENBALE"/>
<meta-data
 android:name="com.fcm.push.senderid"
 android:value="@string/FCM_SENDERID"/>

<!-- Mi账号配置 -->
<meta-data
 android:name="com.mi.push.api_key"
 android:value="@string/MI_PUSH_APPKEY" />
<meta-data
 android:name="com.mi.push.app_id"
 android:value="@string/MI_PUSH_APPID" />


<!-- Oppo账号配置 -->
<meta-data
 android:name="com.oppo.push.api_key"
 android:value="@string/OPPO_PUSH_APPKEY" />
<meta-data
 android:name="com.oppo.push.app_secret"
 android:value="@string/OPPO_PUSH_APPSECRET" />

<!-- VIVO账号配置 -->
<meta-data
 android:name="com.vivo.push.api_key"
 android:value="@string/VIVO_PUSH_APPKEY" />
<meta-data
 android:name="com.vivo.push.app_id"
 android:value="@integer/VIVO_PUSH_APPID" />
<meta-data
 android:name="com.agora.demo.productkey"
 android:value="@string/product_key" />

<!--华为账号配置 -->
<meta-data
 android:name="com.huawei.hms.client.appid"
 android:value="@string/HUAWEI_PUSH_APPKID" />
<meta-data
 android:name="push_kit_auto_init_enabled"
 android:value="true"/>

<!-- 离线推送的账号配置 end -->
```

### 初始化 [#初始化]

参考以下步骤调用 `initialize()` 方法来初始化。参考[开通声网灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)获取相关参数。

```java
//
//初始化IotAppSdk2.0 引擎
//
IAgoraIotAppSdk.InitParam initParam = new IAgoraIotAppSdk.InitParam();
initParam.mContext = this;
initParam.mRtcAppId = appId;  // 声网 App ID

String storageRootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
initParam.mLogFilePath = storageRootPath + "/callkit.log";   // 设置日志路径

initParam.mPublishVideo = false;  // 客户端默认不推送视频流
initParam.mPublishAudio = false;  // 客户端默认不推送音频流
initParam.mSubscribeAudio = true; // 客户端默认订阅视频流
initParam.mSubscribeVideo = true; // 客户端默认订阅视频流
// 开发者选项>>呼叫服务>>Master Server URL 获取
initParam.mMasterServerUrl = "http://iot-api-gateway.sh.agoralab.co/api";
// 开发者选项>>呼叫服务>>Slave Server URL 获取
initParam.mSlaveServerUrl = "https://un2nfllop5.execute-api.cn-north-1.amazonaws.com.cn/Prod";
// 开发者选项>>呼叫服务>>Project ID 获取
initParam.mProjectID = "Your Project ID";

// 调用 SDK 初始化接口，返回错误码为 0，表示初始化成功
int ret = AIotAppSdkFactory.getInstance().initialize(initParam);
```

### 注册用户账号 [#注册用户账号]

可以通过邮箱和手机号码两种方式来注册用户账号。账号注册分为两个步骤：

1. 通过手机号码或者邮箱账号获取注册验证码。

    ```java
    // 获取验证码，这里 accountName 可以是手机号码或者邮箱，type 固定为 "REGISTER"
    int ret = AIotAppSdkFactory.getInstance().getAccountMgr().getCode(accountName, type);

    // 获取验证码完成后，会触发 IAccountMgr.ICallback.onGetCodeDone() 事件
    // 相应的验证码，过几分钟后会发送到手机短信或者邮箱邮件
    @Override
    public void onGetCodeDone(int errCode, String account) { }
    ```

2. 使用手机号或邮箱、验证码和密码进行账号注册。

   `accountName` 可以填入手机号码或者邮箱作为账户名，你也可以使用其他账户名。

    ```java
    if (registerWithPhoneNumber) {
        // 手机注册账号
        ret = AIotAppSdkFactory.getInstance().getAccountMgr().register(accountName,
                    password, verifyCode, phoneNumber, null);
    } else {
        // 邮箱注册账号
        ret = AIotAppSdkFactory.getInstance().getAccountMgr().register(accountName,
                    password, verifyCode, null, email);
    }

    // 注册完成后触发 IAccountMgr.ICallback.onRegisterDone() 回调
    @Override
    public void onRegisterDone(int errCode, String account) {  }
    ```

### 登录用户账号 [#登录用户账号]

参考以下步骤登录用户账号：

```java
// 使用（账号名+密码）方式进行登录
int ret = AIotAppSdkFactory.getInstance().getAccountMgr().login(accountName, password);

// 登录成功/失败后，触发 IAccoungMgr.ICallback.onLoginDone() 回调
@Override
public void onLoginDone(int errCode, String account) { }
```

当用户账号成功登录后，可以开始进行正式的操作，包括设备管理、告警信息查询、系统信息查询、用户信息更新等。

### 添加设备 [#添加设备]

当前版本中，只支持设备端扫描二维码进行添加，需要客户端生成二维码：

1. 生成设备二维码图。在客户端需要使用 JSON 字符串生成相应的二维码图，其中 JSON 字符串格式为：

	```json
	{
		"s": "wifi_ssid",
		"p": "wifi_password",
		"u": "code_user_id",
		"k": "productkey"
	}
	```

其中：

- `wifi_ssid`: 配置给设备的 WIFI SSID。
- `wifi_password`: 配置给设备的 WIFI 网络访问密码。
- `code_user_id`: 用于配置的特定用户 ID，可以通过 `AIotAppSdkFactory.getInstance().getAccountMgr().getQRCodeUserId()` 获取。
- `product_key`: 配置的产品 ID。获取方式详见[开通并配置灵隼物联网服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)。

JSON 内容示例：

```json
{
    "s":"TP-LINK_AGORA08",
    "p":"Agora123456",
    "u":"690191659910586368",
    "k":"EJImm64m65ECOl5"
    }
```

然后使用第三方二维码生成库（例如 [zxing](https://github.com/journeyapps/zxing-android-embedded)），根据 JSON 内容生成二维码 Bitmap，并提供给设备端摄像头来扫描。具体生成方式可以参考 SDK 自带的示例项目中的 `ZXingUtils.class` 文件。

2. 设备摄像头扫描二维码图进行主动绑定。

3. 绑定设备列表查询。你可以通过设备列表查询方法来获取当前所有绑定设备列表。

    ```java
    // 查询当前账号下绑定设备列表
    int ret = AIotAppSdkFactory.getInstance().getDeviceMgr().queryAllDevices();

    // 查询完成后，触发 IDeviceMgr.ICallback.onAllDevicesQueryDone() 回调事件
    @Override
    public void onAllDevicesQueryDone(int errCode, List<IotDevice> deviceList) { }
    ```

4. 设备改名。设备绑定成功后，会使用默认的设备名。用户也可以根据需要，将设备名更改成方便记忆的名字。

    ```java
    // 设备进行改名操作，
    int errCode = AIotAppSdkFactory.getInstance().getDeviceMgr().renameDevice(iotDevice, newName);

    // 改名完成触发 IDeviceMgr.ICallback.onDeviceRenameDone() 回调事件
    @Override
    public void onDeviceRenameDone(int errCode, IotDevice iotDevice, String newName) {  }
    ```

### 客户端呼叫设备端 [#客户端呼叫设备端]

1. 客户端发出呼叫请求。

   ```java
   // 呼叫设备端，可以同时附带一些呼叫消息
   int ret = AIotAppSdkFactory.getInstance().getCallkitMgr().callDial(iotDevice, attachMsg);

   // 呼叫完成后，触发 ICallkitMgr.ICallback.onDialDone()回调事件
   // 可以根据 errCode 错误码判断呼叫是否成功，或者对端是否忙
   @Override
   public void onDialDone(int errCode, IotDevice iotDevice) {  }
   ```

2. 客户端发出呼叫请求成功后，设备端会有三种状态回应：

    - 设备端正常接听，客户端接收到 `ICallkitMgr.ICallback.onPeerAnswer(IotDevice iotDev)` 回调事件，进入双方通话状态。
    - 设备端拒绝接听，客户端接收到 `ICallkitMgr.ICallback.onPeerHangup(IotDevice iotDev)` 回调事件，结束本次通话。
    - 设备端超时不回应，客户端接收到 `ICallkitMgr.ICallback.onPeerTimer(IotDevice iotDev)` 回调事件，结束本次通话。

3. 正常接通时，客户端会接收到 `ICallkitMgr.ICallback.onPeerFirstVideo(IotDevice iotDevice, int videoWidth, int videoHeight)` 事件，表示接收到设备端首帧视频帧，应用层可以根据自己实际的显示业务逻辑进行处理。正常通话过程中，客户端可以发送音视频，对端也可以发送音视频。客户端可以调用 `AIotAppSdkFactory.getInstance().getCallkitMgr().setAudioEffect();` 来设置各种变声效果。客户端也可以设置设备端是否推送音视频流等达到静音等效果，详见 `ICallkitMgr` 接口提供的方法。

4. 如果客户端想要结束通话，调用 `AIotAppSdkFactory.getInstance().getCallkitMgr().callHangup();` 方法主动挂断电话。在通话过程中接收到 `ICallkitMgr.ICallback.onPeerHangup(IotDevice iotDev)`  事件，说明对端设备主动挂断，结束通话。

### 设备端呼叫客户端 [#设备端呼叫客户端]

1. 客户端处于空闲状态时，如果有设备端呼叫，会接收到来电呼叫事件 `ICallkitMgr.ICallback.onPeerIncoming()`，你可以根据自己的业务逻辑显示来电界面。

    ```java
    // 来自于设备端的呼叫，附带消息
    @Override
    public void onPeerIncoming(IotDevice iotDevice, String attachMsg)
    ```

2. 客户端根据实际的业务逻辑进行处理。

    - 如果客户端正常接听，则调用 `ICallkitMgr.getCallkitMgr. callAnswer(IotDevice iotDev)` 方法，进入双方通话状态。
    - 如果客户端拒绝接听，则调用 `ICallkitMgr.getCallkitMgr.callHangup(IotDevice iotDev)` 方法挂断 ，结束本次通话、
    - 如果客户端不回应，设备端会超时主动挂断，客户端接收到 `ICallkitMgr.ICallback. onPeerHangup(IotDevice iotDev)`  回调事件，结束本次通话。

3. 正常接通时，客户端会接收到 `ICallkitMgr.ICallback.onPeerFirstVideo(IotDevice iotDevice, int videoWidth, int videoHeight)` 事件，表示接收到设备端首帧视频帧，应用层可以根据自己实际的显示业务逻辑进行处理。正常通话过程中，客户端可以发送音视频，对端也可以发送音视频。客户端可以调用 `AIotAppSdkFactory.getInstance().getCallkitMgr().setAudioEffect();` 来设置各种变声效果。客户端也可以设置设备端是否推送音视频流等达到静音等效果，详见 `ICallkitMgr` 接口提供的方法。

4. 如果客户端想要结束通话，调用 `AIotAppSdkFactory.getInstance().getCallkitMgr().callHangup();` 方法主动挂断电话。在通话过程中接收到 `ICallkitMgr.ICallback.onPeerHangup(IotDevice iotDev)`  事件，说明对端设备主动挂断，结束通话。

### 告警信息查询 [#告警信息查询]

当有人员经过监控设备时，设备端会将 入侵检测、移动侦测相应的事件上报给服务器。

客户端可以通过相应的接口进行查询，设置不同的查询参数字段，可以进行相应的过滤。

```java
IAlarmMgr.QueryParam queryParam = new IAlarmMgr.QueryParam();
queryParam.mPageIndex = 1;
queryParam.mPageSize = 512;
queryParam.mMsgType = -1;
queryParam.mMsgStatus = -1;
int ret = AIotAppSdkFactory.getInstance().getAlarmMgr().queryByPage(queryParam);
```

### 系统告警查询 [#系统告警查询]

设备的绑定、解绑、上线、下线等系统事件，也会同步上报给服务器。

客户端可以通过相应的接口进行查询，设置不同的查询参数字段，可以进行相应的查询过滤。

```java
IDevMessageMgr.QueryParam queryParam = new IDevMessageMgr.QueryParam();
queryParam.mDevIDList.addAll(mDeviceIDList);
queryParam.mPageIndex = 1;
queryParam.mPageSize = 256;
queryParam.mMsgType = -1;
queryParam.mMsgStatus = -1;
int ret = AIotAppSdkFactory.getInstance().getDevMessageMgr().queryByPage(queryParam);
```

### 发送和接收云信令 RTM 消息 [#发送和接收云信令-rtm-消息]

你可以通过 `sendMessage` 和 `onMessageReceived` 发送和接收云信令 RTM 消息。

```java
// 获取 IRtmMgr 对象
IRtmMgr rtmMgr = AIotAppSdkFactory.getInstance().getRtmMgr();
// 链接 RTM 云信令
int errCode = rtmMgr.connect(mLivingDevice);
// 发送 RTM 云信令消息
rtmMgr.sendMessage(messageData, new IRtmMgr.ISendCallback() {
            @Override
            public void onSendDone(int errCode) {
                Log.d(TAG, "<sendMessage.onSendDone> errCode=" + errCode);
                getISingleCallback().onSingleCallback(Constant.CALLBACK_TYPE_RTM_SEND_DONE, errCode);
            }
        });

// 通过 onMessageReceived 回调接收云信令 RTM 消息
@Override
public void onMessageReceived(byte[] messageData) {
		Log.d(TAG, "<onMessageReceived> messageData.length=" + messageData.length);
		getISingleCallback().onSingleCallback(Constant.CALLBACK_TYPE_RTM_RECVED, messageData);
}
// 与 RTM 云信令断开连接
int errCode = rtmMgr.disconnect();
```

### 登出用户账号 [#登出用户账号]

你可以通过以下方法登出用户账号。

```java
// 登出操作，触发 IAccountMgr.ICallback.onLogoutDone()事件
int ret = AIotAppSdkFactory.getInstance().getAccountMgr().logout();

// 登出完成事件，
@Override
public void onLogoutDone(int errCode, String account) {  }
```

### 注销用户账号 [#注销用户账号]

你可以通过以下方法注销用户账号。

```java
// 注销操作，触发 IAccountMgr.ICallback.onUnregisterDone() 回调事件
int ret = AIotAppSdkFactory.getInstance().getAccountMgr().unregister();

// 注销完成事件
@Override
public void onUnregisterDone(int errCode, String account) {  }
```

## 参考信息 [#参考信息]

- [客户端全功能 API 参考](/zh-CN/api-reference/iot-apaas/client-api/full)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="ios" />
本文介绍如何使用声网灵隼**全功能版本**的客户端示例项目在 iOS 客户端实现设备管理、呼叫、通话、告警等功能。

:::info[信息]
**全功能版本的客户端示例项目** 提供用户注册/登录、绑定/解绑设备、Wi-Fi 二维码配网、呼叫、实时音视频等功能。

如果你没有自研的设备管理等模块，声网建议你使用全功能版本示例项目。
:::

## 前提条件 [#前提条件-1]

开始前，请确保你的开发环境满足以下条件：

- Xcode（推荐最新版本）。
- iOS 12.0 或以上版本的 Apple 设备。
- 参考[开通并配置声网灵隼物联网服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)开通灵隼服务

## 跑通示例项目 [#跑通示例项目-1]

1. 从 [GitHub](https://github.com/AgoraIO-Community/ag-iot-ios-app) 上下载最新版本的 iOS 客户端示例项目。

2. 在项目的 `ag-iot-ios-app\AgoraIoT` 目录运行 `pod install` 安装依赖。
3. 连接上 iOS 设备后，点击 `.xcworkspace` 文件打开项目，并在 iOS Device 选项中勾选上你的 iOS 设备。
4. 使用 Xcode 打开项目。修改配置文件 `Config.swift`，将以下配置项修改为你在灵隼控制台的**应用配置&gt;&gt;开发者选项**页面中获取的相关参数。详见[开通并配置声网灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)。

	```swift
	// 声网灵隼控制台的应用配置>>开发者选项中的 App ID
	public static let appId = "4b31f**********************3037"  //appid
	// 声网灵隼控制台的应用配置>>开发者选项中的 Project ID
	public static let projectId:String = "a******_C" // 项目id"
	// Easemob App Key。在开通声网灵隼服务之后，可以在应用配置页面中的开发者选项选项卡中获取
	public static let ntfAppKey: String = "52****88#3****2" //离线推送的appkey
	// APNS 证书名。在开通声网灵隼服务之后，可以在应用配置页面中的开发者选项选项卡中获取
	public static let ntfApnsCertName:String = "xxx.xxx.xxx"//离线推送的AnpsCertName
	// 声网灵隼控制台的应用配置>>开发者选项中的 Master Server URL
	public static let masterServerUrl:String = "https://un2nfllop5.execute-api.cn-north-1.amazonaws.com.cn/Prod"
	// 声网灵隼控制台的应用配置>>开发者选项中的 Slave Server URL
	public static let slaveServerUrl:String = "https://api.sd-rtn.com/agoralink/cn/api"
	```

5. 在项目 TARGETS 下的 **Signing & Capabilities** 界面勾选 **Automatically manage signing**，配置你的苹果开发者账号和 Bundle Identifier。
6. 连接上 Apple 设备后，编译并运行项目。
7. 在登录页面选择注册新账户或者登录已有账户，目前国内只支持手机号注册。

<DocsImage alt="登录页面" src="https://web-cdn.agora.io/docs-files/1656331016147" displayWidth="40%" />

8. 在登录成功后的页面，可以点击 **+** 号添加新设备（设备产品码请参考参考[开通灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)文档获取产品二维码章节），或者选择已经添加设备实时预览对应设备的画面（设备端示例项目请参考[实现设备端功能](/zh-CN/solutions/iot-apaas/build/implement-core-features/device-call)文档）。

<DocsImage alt="添加设备" src="https://web-cdn.agora.io/docs-files/1656330954299" displayWidth="40%" />

## 客户端实现 [#客户端实现-1]

iOS 客户端的核心逻辑如下。

### 初始化 [#初始化-1]

参考以下步骤调用 `initialize` 方法进行初始化。

```swift
let param:InitParam = InitParam()
// 声网 App ID。在开通声网灵隼服务之后，可以在应用配置页面中的开发者选项选项卡中获取。
param.rtcAppId = Config.appId
param.logFilePath = FileCenter.logFilePath()
param.subscribeVideo = true
param.subscribeAudio = true
param.publishVideo = false
param.publishAudio = false
// APNS 证书名。在开通声网灵隼服务之后，可以在应用配置页面中的开发者选项选项卡中获取。
param.ntfApnsCertName = Config.ntfApnsCertName
// APNS 证书秘钥。在开通声网灵隼服务之后，可以在应用配置页面中的开发者选项选项卡中获取。
param.ntfAppKey = Config.ntfAppKey
// 开发者选项>>呼叫服务>>Master Server URL 获取
param.masterServerUrl = "http://iot-api-gateway.sh.agoralab.co/api"
// 开发者选项>>呼叫服务>>Slave Server URL 获取
param.slaveServerUrl = "https://un2nfllop5.execute-api.cn-north-1.amazonaws.com.cn/Prod"
// 开发者选项>>呼叫服务>>Project ID 获取
param.projectId = "Your Project ID"
let ret = AgoraIotSdk.iotsdk.initialize(initParam: param, netStatus: {status,msg in}, callBackFilter: { ec, msg in return (ec,msg)})
```

### 注册用户账号 [#注册用户账号-1]

可以通过邮箱和手机号码两种方式来注册用户账号。账号注册分为两个步骤：

1. 通过手机号码或者邮箱账号获取注册验证码。

    ```swift
    //
    // 获取验证码，这里 使用getCode来获取邮箱验证码，getSms来获取手机验证码，type固定为""REGISTER""
    // 获取手机验证码时，lang表示所在国家语言
    // 相应的验证码，过几分钟后会发送到手机短信或者邮箱邮件
    // 接口通过 result 异步返回
    AgoraIotSdk.iotsdk.accountMgr.getCode(email: email, type: "REGISTER", result: {errCode,errMsg in})
    AgoraIotSdk.iotsdk.accountMgr.getSms(phone: phone, type: "REGISTER",lang:"ZH_CN", result: {errCode,errMsg in})
    ```

2. 使用手机号或邮箱、验证码和密码进行账号注册。

   `accountName` 可以填入手机号码或者邮箱作为账户名，你也可以使用其他账户名。

    ```swift
    if(registerWithPhoneNumber){
        //使用手机注册
				AgoraIotSdk.iotsdk.accountMgr.register(account: acc, password: pwd,code:code,email: nil,phone:phone, result: result: {errCode, errMsg in})
    }
    else{
       //使用邮箱注册
			 AgoraIotSdk.iotsdk.accountMgr.register(account: acc, password: pwd,code:code,email: email,phone:nil, result: result: {errCode, errMsg in})
    }
    ```

### 登录用户账号 [#登录用户账号-1]

参考以下步骤登录用户账号：

```swift
//
// 使用（账号名+密码）方式进行登录
// 登录成功/失败后，触发 result 回调登录结果
//
AgoraIotSdk.iotsdk.accountMgr.login(account: acc, password: pwd,result: {errCode, errMsg in})
```

当用户账号成功登录后，可以开始进行正式的操作，包括设备管理、告警信息查询、系统信息查询、用户信息更新等。

### 添加设备 [#添加设备-1]

当前版本中，只支持设备端扫描二维码进行添加，需要客户端生成二维码：

1、生成设备二维码图。在客户端需要使用 JSON 字符串生成相应的二维码图，其中 JSON 字符串格式为：

	```json
	{
		"s": "wifi_ssid",
		"p": "wifi_password",
		"u": "code_user_id",
		"k": "productkey"
	}
	```

其中：

- `wifi_ssid`: 配置给设备的 WIFI SSID。
- `wifi_password`: 配置给设备的 WIFI 网络访问密码。
- `code_user_id`: 用于配置的特定用户 ID，可以通过 `AgoraIotSdk.iotsdk.accountMgr.getUserId()` 获取。
- `product_key`: 配置的产品 ID。获取方式详见[开通声网灵隼服务](/zh-CN/solutions/iot-apaas/build/setup-and-access/enable-service)。

JSON 内容示例：

```json
{
    "s":"TP-LINK_AGORA08",
    "p":"Agora123456",
    "u":"690191659910586368",
    "k":"EJImm64m65ECOl5"
    }
```

然后使用二维码生成库（例如 `CoreImage.CIFilterBuiltins`），根据 JSON 内容生成二维码 Bitmap，并提供给设备端摄像头来扫描。具体生成方式可以参考 SDK 自带的示例项目中的 `/View/Home/SetDevice/SDStep3View.swift` 文件。

2. 设备摄像头扫描二维码图进行主动绑定。

3. 绑定设备列表查询。你可以通过设备列表查询方法来获取当前所有绑定设备列表。

    ```swift
    // 查询当前账号下绑定设备列表，通过回调 result 方法返回设备列表
    AgoraIotSdk.iotsdk.deviceMgr.queryAllDevices(result: {errCode,errMsg,devs in})
    ```

4. 设备改名。设备绑定成功后，会使用默认的设备名。用户也可以根据需要，将设备名更改成方便记忆的名字。

    ```swift
    // 设备进行改名操作，通过 result 方法返回改名是否成功
    AgoraIotSdk.iotsdk.deviceMgr.renameDevice(device: dev, newName: newName, result: {errCode,errMsg in})
    ```

### 客户端呼叫设备端 [#客户端呼叫设备端-1]

1. 客户端发出呼叫请求。

   ```swift
   // 呼叫设备端，可以同时附带一些呼叫消息
   AgoraIotSdk.iotsdk.callkitMgr.callDial(device:dev,attachMsg: "",result:{
                (ec,msg) in
                cb(ec == ErrCode.XOK ? true : false , msg)
            },peerAction: {s in
                log.i("peer \(s)")
            })
   ```

2. 客户端发出呼叫请求成功后，设备端会有三种状态回应：

    - 如果设备端正常接听，`peerAction` 返回 `.Answer`，开始音视频通话。
    - 如果设备端拒绝接听，`peerAction` 返回 `.Hangup`，结束本次通话。
    - 如果设备端接听超时，`peerAction` 返回 `.Timeout`，结束本次通话。

3. 正常通话过程中，客户端可以发送音视频，对端也发送音视频。当设备接听后挂断电话，`peerAction` 返回 `.Hangup`。

4. 如果客户端想要结束通话，调用 `AgoraIotSdk.iotsdk.callkitMgr.callHangup()` 方法主动挂断电话。

    ```swift
    AgoraIotSdk.iotsdk.callkitMgr.callHangup(result: {errCode,errMsg in})
    ```

### 设备端呼叫客户端 [#设备端呼叫客户端-1]

1. 接收到来电呼叫事件需要在初始化完成后注册来电回调接口

    ```swift
    AgoraIotSdk.iotsdk.callkitMgr.register(incoming: {peerId,msg,action in})
    ```

2. 客户端处于空闲状态时，如果有设备端呼叫，会接收到来电呼叫事件 `AgoraIotSdk.iotsdk.ICallkitMgrCallback.onPeerIncoming`，你可以根据自己的业务逻辑显示来电界面。

3. 客户端根据实际的业务逻辑进行处理。

    - 如果客户端拒绝，则调用 `AgoraIotSdk.iotsdk.callkitMgr.callHangup()` 方法挂断 ，结束本次通话。
    - 如果客户端接听，则调用 `AgoraIotSdk.iotsdk.callkitMgr.callAnswer()` 方法，进入双方通话状态。客户端可以使用 `AgoraIotSdk.iotsdk.callkitMgr.callHangup()` 主动挂断通话。如果设备端挂断通话，客户端会收到 `AgoraIotSdk.iotsdk.ICallkitMgrCallback.onPeerHangup()` 回调。

### 告警信息查询 [#告警信息查询-1]

当有人员经过监控设备时，设备端会将 入侵检测、移动侦测相应的事件上报给服务器。

客户端可以通过相应的接口进行查询，设置不同的查询参数字段，可以进行相应的过滤。

```swift
IAlarmMgr.QueryParam queryParam = new IAlarmMgr.QueryParam();
queryParam.mPageIndex = 1;
queryParam.mPageSize = 512;
queryParam.mMsgType = -1;
queryParam.mMsgStatus = -1;
int ret = AIotAppSdkFactory.getInstance().getAlarmMgr().queryByPage(queryParam);
```

### 系统告警查询 [#系统告警查询-1]

设备的绑定、解绑、上线、下线等系统事件，也会同步上报给服务器。

客户端可以通过相应的接口进行查询，设置不同的查询参数字段，可以进行相应的查询过滤。

```swift
var query = IAlarmMgr.QueryParam(dateBegin: nil)
query.status = status
query.messageType = type
query.createdDateBegin = nil
query.createdDateEnd = date
query.pageSize = 10
query.currentPage = 1
AgoraIotSdk.iotsdk.alarmMgr.queryByParam(queryParam: query, result: {errCode,errMsg,alarms in})
```

### 发送和接收云信令 RTM 消息 [#发送和接收云信令-rtm-消息-1]

你可以通过 `sendMessage`和 `messageReceived` 发送和接收云信令 RTM 消息。

```swift
// 连接 RTM 云信令
AgoraIotLink.iotsdk.deviceMgr.sendMessageBegin(device: device!) { ec, msg in
		log.level(ec == ErrCode.XOK ? .info : .error, "demo sendMessageBegin() \(msg)(\(ec))")
		if(ec != ErrCode.XOK){
				AGToolHUD.showFaild(info: msg)
		}
} statusUpdated: { status,msg,data in
		if(status == .DataArrived){
				log.i("demo sendMessage status updated:\(status),\(msg)")
				AGToolHUD.showSuccess(info: "rtm info :\(msg)")
		}
		else if(data != nil){
				log.i("demo 收到设备数据长度：" + String(data!.count))
				AGToolHUD.showSuccess(info: "rtm info :\(msg)")
		}
}

// 发送 RTM 云信令消息
AgoraIotLink.iotsdk.deviceMgr.sendMessage(data: data, description: "this is description") { ec, msg in
		log.i("demo send message \(msg)(\(ec))")
		if(ec != ErrCode.XOK){
				AGToolHUD.showFaild(info: msg)
		}
}

// 与 RTM 云信令断开连接
AgoraIotLink.iotsdk.deviceMgr.sendMessageEnd()
```

### 登出用户账号 [#登出用户账号-1]

你可以通过以下方法登出用户账号。

```swift
AgoraIotSdk.iotsdk.accountMgr.logout(result: {errCode,errMsg in})
```

### 注销用户账号 [#注销用户账号-1]

你可以通过以下方法注销用户账号。

```swift
AgoraIotSdk.iotsdk.accountMgr.unregister(result: {errCode,errMsg in})
```

## 参考信息 [#参考信息-1]

- [客户端全功能 API 参考](/zh-CN/api-reference/iot-apaas/client-api/full)
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
