1# UIAbility与UIAbility连接开发指南 2<!--Kit: Distributed Service Kit--> 3<!--Subsystem: DistributedSched--> 4<!--Owner: @hobbycao--> 5<!--Designer: @gsxiaowen--> 6<!--Tester: @hanjiawei--> 7<!--Adviser: @w_Machine_cc--> 8 9 10## 简介 11 12应用跨设备连接管理可以通过分布式操作系统,将用户拥有的多个设备整合为一个整体,实现设备与设备之间的能力互助,为用户提供比单设备更加高效的沉浸式体验。<!--Del-->例如通过手表相机应用拉起手机的相机功能并实现实时画面预览和遥控拍照。<!--DelEnd--> 13 14 15### 能力范围 16 17- 跨设备拉起应用:可以通过本设备应用,拉起其他组网设备的同应用,并进行协同作业。 18- 数据交互:实现跨设备数据传输<!--Del-->,包括文本信息、字节流、图片、传输流(三方应用仅支持文本信息交互能力)<!--DelEnd-->。 19 20 21### 亮点特征 22 23通过多样化的跨设备传输流特性,实现本设备相机拉起对端设备相机功能。为用户提供<!--Del-->实时预览对端设备摄像头的画面、<!--DelEnd-->文本信息交互<!--Del-->、接受回传照片、遥控拍照等<!--DelEnd-->能力。 24 25 26### 基本概念 27 28在进行应用跨设备连接管理开发前,开发者应了解以下基本概念: 29 30- **DMS** 31 32 DMS(Distributedsched Management Service)是分布式组件管理框架,提供分布式组件的管理能力。 33 34- **UIAbility** 35 36 描述应用程序的界面交互能力,负责管理应用界面的生命周期、用户交互以及界面渲染等任务。 37 38- **Extension** 39 40 用于扩展应用的功能或实现跨设备协同。它允许应用在后台运行某些任务,或者将部分功能迁移到其他设备上执行,从而实现分布式能力。 41 42<!--Del--> 43- **字节流** 44 45 字节流是数据类型为[ArrayBuffer](../arkts-utils/arraybuffer-object.md)类型的数据。可以被用于存储二进制数据,例如图像或音频数据。 46 47- **传输流** 48 49 可进行图片、视频流传输的媒体流。 50<!--DelEnd--> 51### 实现原理 52 53应用跨设备连接管理生长在分布式组件管理框架之上,在分布式组件管理框架上进行了JS对象型的封装,能通过分布式组件管理框架服务建立协同关系并进行应用间的连接,数据的交互能力由系统支持。 54 55**图1** 应用跨设备连接运行机制 56 57 58 59 60### 约束与限制 61 62- 设备间需要登录相同的华为账号。 63 64- 不同设备间只有相同bundleName的UIAbility应用才能进行协同。 65<!--Del--> 66- 字节流、图片以及传输流的能力仅支持系统应用。 67<!--DelEnd--> 68- 业务协同完毕后需及时结束协同状态。未申请长时任务的应用,在锁屏或退至后台5秒以上,会被结束掉协同生命周期。 69 70- 分布式组件管理框架在协同过程中不会对传输内容进行审查。涉及隐私敏感数据时,建议业务通过数据加密、弹框提醒等方式加强信息安全。 71 72 73## 环境准备 74 75### 环境要求 76 77可登录华为账号的设备A和设备B,设备间需要组网成功(双端登录同一个华为账号,并使用蓝牙连接)。 78 79 80### 搭建环境 81 821. 在PC上安装[DevEco Studio](https://developer.huawei.com/consumer/cn/download/deveco-studio),要求版本在4.1及以上。 832. 将public-SDK更新到API 18或以上<!--Del-->,更新SDK的具体操作可参见[更新指南]( ../tools/openharmony_sdk_upgrade_assistant.md)<!--DelEnd-->。 843. 用USB线缆将两台调测设备(设备A和设备B)连接到PC。 854. 打开设备A和设备B的蓝牙,互相识别,实现组网。 86 87 88### 检验环境是否搭建成功 89 90PC上执行shell命令: 91 92```shell 93hdc shell 94hidumper -s 4700 -a "buscenter -l remote_device_info" 95``` 96 97组网成功时可显示组网设备数量的信息,如“remote device num = 1”。 98 99 100## 开发指导 101 102应用跨设备连接管理可以通过分布式操作系统,将用户拥有的多个设备作为一个整体,设备与设备之间取长补短、相互帮助,为用户提供比单设备更加高效、沉浸的体验。 103 104 105### 接口说明 106 107应用跨设备连接管理接口如下表所示。具体API说明详见API参考:[abilityConnectionManager](../reference/apis-distributedservice-kit/js-apis-distributed-abilityConnectionManager.md)。 108 109**表1** abilityConnectionManager接口功能介绍 110 111| 接口名 | 描述 | 112| -------- | -------- | 113| createAbilityConnectionSession(serviceName: string, context: Context, peerInfo: PeerInfo, connectOptions: ConnectOptions): number; | 创建应用间的会话。 | 114| destroyAbilityConnectionSession(sessionId: number): void; | 销毁应用间的会话。 | 115| connect(sessionId: number): Promise<ConnectResult>; | source侧进行ability的连接。 | 116| acceptConnect(sessionId: number, token: string): Promise<void>; | sink侧进行ability的连接。 | 117| disconnect(sessionId: number): void; | 断开ability的连接。 | 118| on(type: 'connect' \| 'disconnect' \| 'receiveMessage' \| 'receiveData', sessionId: number, callback: Callback<EventCallbackInfo>): void | 监听<!--Del-->connect/disconnect/receiveMessage/receiveData<!--DelEnd-->事件。 | 119| off(type: 'connect' \| 'disconnect' \| 'receiveMessage' \| 'receiveData', sessionId: number, callback?: Callback<EventCallbackInfo>): void | 取消<!--Del-->connect/disconnect/receiveMessage/receiveData<!--DelEnd-->事件的监听。 | 120| sendMessage(sessionId: number, msg: string): Promise<void>; | 发送文本信息。 | 121|<!--DelRow--> sendData(sessionId: number, data: ArrayBuffer): Promise<void>; | 发送字节流(仅支持系统应用调用)。 | 122|<!--DelRow--> sendImage(sessionId: number, image: image.PixelMap): Promise<void>; | 发送图片(仅支持系统应用调用)。 | 123|<!--DelRow--> createStream(sessionId: number, param: StreamParam): Promise<number>; | 创建传输流(仅支持系统应用调用)。 | 124|<!--DelRow--> destroyStream(sessionId: number): void; | 关闭传输流(仅支持系统应用调用)。 | 125 126 127### 开发步骤 128 129通过应用跨设备管理模块,设备A拉起并连接设备B上的应用。连接成功后,设备A和设备B通过on接口注册相应事件的回调监听。设备A或设备B通过sendMessage<!--Del-->、sendData、sendImage、createStream等<!--DelEnd-->接口发送消息<!--Del-->、字节流、传输流<!--DelEnd-->。对端通过监听到的回调信息进行后续协同业务。 130 131**导入AbilityConnectionManager模块文件** 132 133 ```ts 134 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 135 ``` 136 137 138**发现设备** 139 140设备A上的应用,需要发现并选择设备B的netWorkId来作为协同接口的入参。可调用分布式设备管理模块接口,进行对端设备的发现和选择,详情可参考[分布式设备管理模块](devicemanager-guidelines.md)进行开发。 141 142 143**应用间创建会话并进行连接** 144 145设备A和设备B在创建会话和连接时要执行的操作不同,接下来的开发步骤中,以设备A作为连接发起方,设备B作为连接接收端。 146 147**1.设备A** 148 149应用主动调用createAbilityConnectionSession()接口创建会话,获得sessionId。之后调用connect()方法启动ability会话连接(此时设备B上应用会被拉起)。 150 151 ```ts 152 import { abilityConnectionManager, distributedDeviceManager } from '@kit.DistributedServiceKit'; 153 import { common } from '@kit.AbilityKit'; 154 import { hilog } from '@kit.PerformanceAnalysisKit'; 155 156 let dmClass: distributedDeviceManager.DeviceManager; 157 158 function initDmClass(): void { 159 try { 160 dmClass = distributedDeviceManager.createDeviceManager('com.example.remotephotodemo'); 161 } catch (err) { 162 hilog.error(0x0000, 'testTag', 'createDeviceManager err: ' + JSON.stringify(err)); 163 } 164 } 165 // 获取设备B的设备ID 166 function getRemoteDeviceId(): string | undefined { 167 initDmClass(); 168 if (typeof dmClass === 'object' && dmClass !== null) { 169 hilog.info(0x0000, 'testTag', 'getRemoteDeviceId begin'); 170 let list = dmClass.getAvailableDeviceListSync(); 171 if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') { 172 hilog.info(0x0000, 'testTag', 'getRemoteDeviceId err: list is null'); 173 return; 174 } 175 if (list.length === 0) { 176 hilog.info(0x0000, 'testTag', 'getRemoteDeviceId err: list is empty'); 177 return; 178 } 179 return list[0].networkId; 180 } else { 181 hilog.info(0x0000, 'testTag', 'getRemoteDeviceId err: dmClass is null'); 182 return; 183 } 184 } 185 // 定义设备B的协同信息 186 const peerInfo: abilityConnectionManager.PeerInfo = { 187 deviceId: getRemoteDeviceId(), 188 bundleName: 'com.example.remotephotodemo', 189 moduleName: 'entry', 190 abilityName: 'EntryAbility', 191 serviceName: 'collabTest' 192 }; 193 const myRecord: Record<string, string> = { 194 "newKey1": "value1", 195 }; 196 197 const options: Record<string, string> = { 198 'ohos.collabrate.key.start.option': 'ohos.collabrate.value.foreground', 199 }; 200 // 定义连接选项 201 const connectOptions: abilityConnectionManager.ConnectOptions = { 202 needSendData: true, 203 startOptions: abilityConnectionManager.StartOptionParams.START_IN_FOREGROUND, 204 parameters: myRecord 205 }; 206 let context = this.getUIContext().getHostContext(); 207 try { 208 this.sessionId = abilityConnectionManager.createAbilityConnectionSession("collabTest", context, peerInfo, connectOptions); 209 hilog.info(0x0000, 'testTag', 'createSession sessionId is', this.sessionId); 210 211 abilityConnectionManager.connect(this.sessionId).then((ConnectResult) => { 212 if (!ConnectResult.isConnected) { 213 hilog.info(0x0000, 'testTag', 'connect failed'); 214 return; 215 } 216 }).catch(() => { 217 hilog.error(0x0000, 'testTag', "connect failed"); 218 }) 219 220 } catch (error) { 221 hilog.error(0x0000, 'testTag', error); 222 } 223 ``` 224 225**2.设备B** 226 227设备A的应用调用connect()后,设备B的应用会通过协同的方式被拉起,拉起时会触发协同生命周期函数onCollaborate(),可在该接口中配置createAbilityConnectionSession()接口以及acceptConnect()接口的调用。 228 229 ```ts 230 import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; 231 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 232 import { hilog } from '@kit.PerformanceAnalysisKit'; 233 234 export default class EntryAbility extends UIAbility { 235 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 236 hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); 237 } 238 239 onCollaborate(wantParam: Record<string, Object>): AbilityConstant.CollaborateResult { 240 hilog.info(0x0000, 'testTag', '%{public}s', 'on collaborate'); 241 let param = wantParam["ohos.extra.param.key.supportCollaborateIndex"] as Record<string, Object> 242 this.onCollab(param); 243 return 0; 244 } 245 246 onCollab(collabParam: Record<string, Object>) { 247 const sessionId = this.createSessionFromWant(collabParam); 248 if (sessionId == -1) { 249 hilog.info(0x0000, 'testTag', 'Invalid session ID.'); 250 return; 251 } 252 const collabToken = collabParam["ohos.dms.collabToken"] as string; 253 abilityConnectionManager.acceptConnect(sessionId, collabToken).then(() => { 254 hilog.info(0x0000, 'testTag', 'acceptConnect success'); 255 }).catch(() => { 256 hilog.error("failed"); 257 }) 258 } 259 260 createSessionFromWant(collabParam: Record<string, Object>): number { 261 let sessionId = -1; 262 const peerInfo = collabParam["PeerInfo"] as abilityConnectionManager.PeerInfo; 263 if (peerInfo == undefined) { 264 return sessionId; 265 } 266 267 const options = collabParam["ConnectOptions"] as abilityConnectionManager.ConnectOptions; 268 options.needSendBigData = true; 269 options.needSendStream = true; 270 options.needReceiveStream = false; 271 try { 272 sessionId = abilityConnectionManager.createAbilityConnectionSession("collabTest", this.context, peerInfo, options); 273 AppStorage.setOrCreate('sessionId', sessionId); 274 hilog.info(0x0000, 'testTag', 'createSession sessionId is' + sessionId); 275 } catch (error) { 276 hilog.error(0x0000, 'testTag', error); 277 } 278 return sessionId; 279 } 280 } 281 ``` 282 283**注册事件监听** 284 285在应用创建会话成功并获得sessionId后,开发者可调用on()方法进行对应事件的监听,通过触发回调函数的方式通知监听者,以便执行对应业务。 286<!--RP1--> 287 ```ts 288 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 289 import { hilog } from '@kit.PerformanceAnalysisKit'; 290 291 abilityConnectionManager.on("connect", this.sessionId,(callbackInfo) => { 292 hilog.info(0x0000, 'testTag', 'session connect, sessionId is', callbackInfo.sessionId); 293 }); 294 abilityConnectionManager.on("disconnect", this.sessionId,(callbackInfo) => { 295 hilog.info(0x0000, 'testTag', 'session disconnect, sessionId is', callbackInfo.sessionId); 296 }); 297 abilityConnectionManager.on("receiveMessage", this.sessionId,(callbackInfo) => { 298 hilog.info(0x0000, 'testTag', 'session receiveMessage, sessionId is', callbackInfo.sessionId); 299 }); 300 abilityConnectionManager.on("receiveData", this.sessionId,(callbackInfo) => { 301 hilog.info(0x0000, 'testTag', 'session receiveData, sessionId is', callbackInfo.sessionId); 302 }); 303 abilityConnectionManager.on("receiveImage", this.sessionId,(callbackInfo) => { 304 hilog.info(0x0000, 'testTag', 'session receiveImage, sessionId is', callbackInfo.sessionId); 305 }); 306``` 307<!--RP1End--> 308<!--Del--> 309**发送数据** 310<!--DelEnd--> 311**<!--Del-->1.<!--DelEnd-->发送消息** 312 313应用连接成功后,开发者可在设备A或者设备B上调用sendMessage()方法给对端应用发送文本信息。 314 315 ```ts 316 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 317 import { hilog } from '@kit.PerformanceAnalysisKit'; 318 319 abilityConnectionManager.sendMessage(this.sessionId, "message send success").then(() => { 320 hilog.info(0x0000, 'testTag', "sendMessage success"); 321 }).catch(() => { 322 hilog.error(0x0000, 'testTag', "connect failed"); 323 }) 324 ``` 325<!--Del--> 326**2.发送字节流数据** 327 328应用连接成功后,开发者可在设备A或者设备B上调用sendData()方法给对端应用发送字节数据(仅支持系统应用调用)。 329 330 ```ts 331 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 332 import { hilog } from '@kit.PerformanceAnalysisKit'; 333 334 let textEncoder = util.TextEncoder.create("utf-8"); 335 const arrayBuffer = textEncoder.encodeInto("data send success"); 336 337 abilityConnectionManager.sendData(this.sessionId, arrayBuffer.buffer).then(() => { 338 hilog.info(0x0000, 'testTag', "sendMessage success"); 339 }).catch(() => { 340 hilog.info(0x0000, 'testTag', "sendMessage failed"); 341 }) 342 ``` 343 344**3.发送图片** 345 346应用连接成功后,开发者可在设备A或者设备B上调用sendImage()方法给对端应用发送图片(仅支持系统应用调用)。 347 348 ```ts 349 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 350 import { hilog } from '@kit.PerformanceAnalysisKit'; 351 import CameraService from '../model/CameraService'; 352 import { photoAccessHelper } from '@kit.MediaLibraryKit'; 353 import { image } from '@kit.ImageKit'; 354 import { fileIo as fs } from '@kit.CoreFileKit'; 355 356 try { 357 let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); 358 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; 359 photoSelectOptions.maxSelectNumber = 5; 360 let photoPicker = new photoAccessHelper.PhotoViewPicker(); 361 photoPicker.select(photoSelectOptions).then((photoSelectResult) => { 362 if (!photoSelectResult) { 363 hilog.error(0x0000, 'testTag', 'photoSelectResult = null'); 364 return; 365 } 366 367 let file = fs.openSync(photoSelectResult.photoUris[0], fs.OpenMode.READ_ONLY); 368 hilog.info(0x0000, 'testTag', 'file.fd:' + file.fd); 369 370 let imageSourceApi: image.ImageSource = image.createImageSource(file.fd); 371 if (imageSourceApi) { 372 imageSourceApi.createPixelMap().then((pixelMap) => { 373 abilityConnectionManager.sendImage(this.sessionId, pixelMap) 374 }); 375 } else { 376 hilog.info(0x0000, 'testTag', 'imageSourceApi is undefined'); 377 } 378 }) 379 } catch (error) { 380 hilog.error(0x0000, 'testTag', 'photoPicker failed with error: ' + JSON.stringify(error)); 381 } 382 ``` 383 384**4.发送传输流** 385 386应用连接成功后,开发者可在设备A或者设备B上调用createStream()方法创建传输流(仅支持系统应用调用),之后调用startStream()方法传输流给对端设备。 387 388 ```ts 389 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 390 import { hilog } from '@kit.PerformanceAnalysisKit'; 391 392 hilog.info(0x0000, 'testTag', 'startStream'); 393 abilityConnectionManager.createStream(sessionId ,{name: 'receive', role: 0}).then(async (streamId) => { 394 let surfaceParam: abilityConnectionManager.SurfaceParam = { 395 width: 640, 396 height: 480, 397 format: 1 398 } 399 let surfaceId = abilityConnectionManager.getSurfaceId(streamId, surfaceParam); 400 hilog.info(0x0000, 'testTag', 'surfaceId is'+surfaceId); 401 AppStorage.setOrCreate<string>('surfaceId', surfaceId); 402 await CameraService.initCamera(surfaceId, 0); 403 abilityConnectionManager.startStream(streamId); 404 }) 405 ``` 406<!--DelEnd--> 407**结束协同** 408 409业务协同完毕后需及时结束协同状态。若是后续短期内还有协同需要,可调用disconnect()方法断开应用间的连接,保留sessionId,以便下次继续使用该sessionId进行连接。若是短期无需使用协同业务,可直接调用destroyAbilityConnectionSession()接口销毁会话,此时会自动断开连接。 410 411 ```ts 412 import { abilityConnectionManager } from '@kit.DistributedServiceKit'; 413 import { hilog } from '@kit.PerformanceAnalysisKit'; 414 415 hilog.info(0x0000, 'testTag', 'disconnectRemoteAbility begin'); 416 if (this.sessionId == -1) { 417 hilog.info(0x0000, 'testTag', 'Invalid session ID.'); 418 return; 419 } 420 abilityConnectionManager.disconnect(this.sessionId); 421 422 hilog.info(0x0000, 'testTag', 'destroyAbilityConnectionSession called'); 423 abilityConnectionManager.destroyAbilityConnectionSession(this.sessionId); 424 ``` 425 426 427### 调测验证 428 429应用侧开发完成后,可在设备A和设备B上安装应用,测试步骤如下: 430 4311. 点击设备A应用的“连接”按钮,此时设备B上的应用被拉起。 4322. 点击设备A应用的“sendMessage”按钮,此时设备B上的应用会触发on()方法的回调,接受该字符串。 433<!--Del--> 4343. 点击设备A应用的“sendData”按钮,此时设备B上的应用会触发on()方法的回调,接受该字节流。 4354. 点击设备A应用的“sendImage”按钮,此时设备B上的应用会触发on()方法的回调,接受该图片。 4365. 点击设备A应用的“启动传输流”按钮,此时设备B上的应用会触发on()方法的回调,接受传输流内容。 437<!--DelEnd--> 4386. 点击设备A或设备B应用的“disconnect”按钮,此时双端会断开连接,触发connect()接口的回调,将断连信息上报给双端应用。 439 440## 常见问题 441 442### 设备A应用无法拉起设备B应用 443 444**可能原因** 445 446- 【原因1】:设备间没有相互组网,导致设备A发起连接时,createAbilityConnectionSession()接口中的peerInfo.deviceId属性未设置正确。 447 448- 【原因2】:有多台设备相互组网,设备A发起连接时,createAbilityConnectionSession()接口中的peerInfo.deviceId属性设置为其他设备的deviceId,未正确指定到B设备上。 449 450**解决措施** 451 452- 针对原因1,设备A和设备B开启USB调试功能,用USB线连接设备和PC。执行shell命令: 453 454 ```shell 455 hdc shell 456 hidumper -s 4700 -a "buscenter -l remote_device_info" 457 ``` 458 回显信息为“remote device num = 0”即为组网失败,请确保登录同一华为账号并使用蓝牙连接。组网成功时可显示组网设备数量的信息,如“remote device num = 1”。 459 460- 针对原因2,查询并选择指定设备时,添加设备选择列表,确保指定到期望的设备。 461 462### 应用锁屏或者退后台一段时间后,正在执行的协同业务被断开 463 464**可能原因** 465 466应用在协同过程中,DMS会对应用的生命周期进行监听。发生锁屏、退后台操作持续五秒后,未申请长时任务的应用会被结束协同状态。 467 468**解决措施** 469 470应用[申请长时任务](../task-management/continuous-task.md),消除此限制。