1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import UIAbility from '@ohos.app.ability.UIAbility'; 17import Want from '@ohos.app.ability.Want'; 18import notificationManager from '@ohos.notificationManager'; 19import Notification from '@ohos.notification'; 20import Base from '@ohos.base'; 21import image from '@ohos.multimedia.image'; 22import fs from '@ohos.file.fs'; 23import wantAgent from '@ohos.app.ability.wantAgent'; 24import { WantAgent } from '@ohos.app.ability.wantAgent'; 25import commonEventManager from '@ohos.commonEventManager'; 26import resourceManager from '@ohos.resourceManager'; 27import opp from '@ohos.bluetooth.opp'; 28import type { BusinessError } from '@ohos.base'; 29import fileUri from '@ohos.file.fileuri'; 30import rpc from '@ohos.rpc'; 31import { DrawableDescriptor } from '@ohos.arkui.drawableDescriptor'; 32import backgroundTaskManager from '@ohos.backgroundTaskManager'; 33 34const TAG: string = '[BT_RECEIVE_SERVICE]==>' 35 36export default class BluetoothReceiveUIAbility extends UIAbility { 37 private subscriber: commonEventManager.CommonEventSubscriber | null = null; 38 private subscribeinfo: commonEventManager.CommonEventSubscribeInfo = { 39 events: [ 40 'usual.event.bluetooth.OPP.TAP.ACCEPT', 41 'usual.event.bluetooth.OPP.TAP.REJECT', 42 'usual.event.bluetooth.OPP.TAP.REMOVE', 43 'ohos.event.notification.BT.LIVEVIEW_REMOVE', 44 'usual.event.bluetooth.OPP.FINISH.NOTIFICATION', 45 'usual.event.bluetooth.OPP.FINISH.REMOVE', 46 'ohos.event.notification.BT.GET_URI', 47 'usual.event.bluetooth.OPP.RECEIVE' 48 ] 49 } 50 private cancelTransEvent: string = 'ohos.event.notification.BT.TAP_CANCEL'; 51 private capsuleNotificationID: number = 100; 52 private timeInterval: number = 0; 53 private receiveDirectory: string = ''; 54 private fileName: string = ''; 55 private sandBoxUri: string = ''; 56 private fileFd: number = -1; 57 private oppProfile = opp.createOppServerProfile(); 58 private jumpUriIsAllowed = false; 59 private tapEventIsAllowed = true; 60 private transfering: boolean = false; 61 private timerIdCreate: number; 62 private timerIdReceive: number; 63 private fileNameToReceiveMap = new Map(); 64 65 onCreate(want: Want): void { 66 console.info(TAG, 'BluetoothReceiveUIAbility onCreate'); 67 this.readyReceiveEvent(); 68 this.startContinuousTask(); 69 this.timerIdCreate = setTimeout(() => { 70 console.info(TAG, 'onCreate 1 second but nothing received'); 71 this.handleTerminate(); 72 }, 1000); 73 } 74 75 onDestroy(): void { 76 console.info(TAG, 'BluetoothReceiveUIAbility onDestroy'); 77 } 78 79 onForeground(): void { 80 console.info(TAG, 'BluetoothReceiveUIAbility onForeground'); 81 } 82 83 handleTerminate() { 84 this.stopContinuousTask(); 85 this.context.terminateSelf(); 86 } 87 88 async readyReceiveEvent() { 89 try { 90 console.info(TAG, 'readyReceiveEvent'); 91 this.subscriber = commonEventManager.createSubscriberSync(this.subscribeinfo); 92 try { 93 await commonEventManager.subscribe(this.subscriber, 94 (err: BusinessError, data: commonEventManager.CommonEventData) => { 95 if (err) { 96 console.error(TAG, `subscribe failed, code is ${err.code}, message is ${err.message}`); 97 this.handleTerminate(); 98 } else { 99 this.handleReceivedEvent(data); 100 } 101 }); 102 } catch (error) { 103 let err: BusinessError = error as BusinessError; 104 console.error(TAG, `subscribe failed, code is ${err.code}, message is ${err.message}`); 105 this.handleTerminate(); 106 } 107 } catch (error) { 108 let err: BusinessError = error as BusinessError; 109 console.error(TAG, `createSubscriber failed, code is ${err.code}, message is ${err.message}`); 110 this.handleTerminate(); 111 } 112 } 113 114 handleReceivedEvent(data: commonEventManager.CommonEventData) { 115 console.info(TAG, 'handleReceivedEvent: ' + data.event); 116 switch (data.event) { 117 case 'usual.event.bluetooth.OPP.TAP.ACCEPT': { 118 console.info(TAG, 'tapEventIsAllowed is ' + this.tapEventIsAllowed); 119 if (!this.tapEventIsAllowed) { 120 break; 121 } 122 clearTimeout(this.timerIdReceive); 123 this.subscriberLiveViewNotification(); 124 this.startOPPReceiveServiceUIAbility(); 125 this.tapEventIsAllowed = false; 126 break; 127 } 128 case 'usual.event.bluetooth.OPP.TAP.REJECT': { 129 console.info(TAG, 'tapEventIsAllowed is ' + this.tapEventIsAllowed); 130 if (!this.tapEventIsAllowed) { 131 break; 132 } 133 clearTimeout(this.timerIdReceive); 134 this.transfering = false; 135 this.oppProfile.setIncomingFileConfirmation(false, -1); 136 this.pullUpReceiveResultNotification(0, 1); 137 this.handleTerminate(); 138 this.tapEventIsAllowed = false; 139 break; 140 } 141 case 'usual.event.bluetooth.OPP.TAP.REMOVE': { 142 console.info(TAG, 'tapEventIsAllowed is ' + this.tapEventIsAllowed); 143 clearTimeout(this.timerIdReceive); 144 this.transfering = false; 145 this.oppProfile.setIncomingFileConfirmation(false, -1); 146 this.pullUpReceiveResultNotification(0, 1); 147 this.handleTerminate(); 148 this.tapEventIsAllowed = true; 149 break; 150 } 151 case 'ohos.event.notification.BT.LIVEVIEW_REMOVE': { 152 this.tapEventIsAllowed = true; 153 this.oppProfile.cancelTransfer(); 154 this.handleTerminate(); 155 break; 156 } 157 case 'usual.event.bluetooth.OPP.FINISH.NOTIFICATION': { 158 console.info(TAG, 'jumpUriIsAllowed is ' + this.jumpUriIsAllowed); 159 if (this.jumpUriIsAllowed) { 160 this.oppProfile.setLastReceivedFileUri(this.receiveDirectory); 161 } 162 setTimeout(() => { 163 if (this.transfering) { 164 console.info(TAG, 'still transfering, do not terminate'); 165 return; 166 } 167 console.info(TAG, 'transfer finished, terminateSelf'); 168 this.handleTerminate(); 169 }, 100); 170 break; 171 } 172 case 'usual.event.bluetooth.OPP.FINISH.REMOVE': { 173 setTimeout(() => { 174 if (this.transfering) { 175 console.info(TAG, 'still transfering, do not terminate'); 176 return; 177 } 178 console.info(TAG, 'transfer finished, terminateSelf'); 179 this.handleTerminate(); 180 }, 100); 181 break; 182 } 183 case 'ohos.event.notification.BT.GET_URI': { 184 this.handleBtUri(data); 185 this.jumpUriIsAllowed = true; 186 break; 187 } 188 case 'usual.event.bluetooth.OPP.RECEIVE': { 189 clearTimeout(this.timerIdCreate); 190 if (data.parameters == undefined) { 191 console.error(TAG, 'data.parameters undefined'); 192 break; 193 } 194 this.fileName = data.parameters?.['fileName']; 195 if (this.fileNameToReceiveMap.has(this.fileName)) { 196 console.error(TAG, 'fileName is repeat, discard this OPP.RECEIVE'); 197 break; 198 } 199 this.fileNameToReceiveMap.set(this.fileName, true); 200 this.transfering = true; 201 this.handleReceive(data); 202 break; 203 } 204 default: { 205 break; 206 } 207 } 208 } 209 210 handleBtUri(data: commonEventManager.CommonEventData) { 211 if (data.parameters == undefined) { 212 console.error(TAG, 'data.parameters undefined'); 213 return; 214 } 215 216 this.oppProfile.on('transferStateChange', (data: opp.OppTransferInformation) => { 217 if (data.status == 1) { 218 this.pullUpSendProgressNotification(data.currentBytes / data.totalBytes * 100, this.fileName); 219 } else if (data.status == 2) { 220 this.tapEventIsAllowed = true; 221 if (data.result != 0) { 222 console.info(TAG, 'receive fail'); 223 this.cancelSendProgressNotification(0); 224 this.pullUpReceiveResultNotification(0, 1); 225 } 226 console.info(TAG, 'transferStateChange ' + data.result); 227 this.oppProfile.off('transferStateChange'); 228 } 229 }); 230 231 this.receiveDirectory = data.parameters.uri; 232 const uri = new fileUri.FileUri(data.parameters.uri); 233 this.sandBoxUri = uri.path + '/' + this.fileName; 234 let file = fs.openSync(this.sandBoxUri, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 235 this.fileFd = file.fd; 236 this.oppProfile.setIncomingFileConfirmation(true, file.fd); 237 } 238 239 handleReceive(data: commonEventManager.CommonEventData) { 240 this.pullUpNotification(this.fileName); 241 } 242 243 async pullUpNotification(fileName: string) { 244 console.info(TAG, 'pullUpNotification'); 245 let wantAgentObjAccept: WantAgent; 246 let wantAgentObjReject: WantAgent; 247 let wantAgentObjRemove: WantAgent; 248 wantAgentObjAccept = await this.getNotificationWantAgent('ohos.event.notification.BT.TAP_ACCEPT'); 249 wantAgentObjReject = await this.getNotificationWantAgent('ohos.event.notification.BT.TAP_REJECT'); 250 wantAgentObjRemove = await this.getNotificationWantAgent('ohos.event.notification.BT.TAP_REMOVE'); 251 await this.publishReceiveNotification(wantAgentObjReject, wantAgentObjAccept, wantAgentObjRemove, fileName); 252 } 253 254 async getNotificationWantAgent(info: string): Promise<WantAgent> { 255 let wantAgentObjUse: WantAgent; 256 let wantAgentInfo: wantAgent.WantAgentInfo = { 257 wants: [ 258 { 259 action: info, 260 } 261 ], 262 actionType: wantAgent.OperationType.SEND_COMMON_EVENT, 263 requestCode: 0, 264 wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG], 265 }; 266 wantAgentObjUse = await wantAgent.getWantAgent(wantAgentInfo); 267 console.info(TAG, 'getNotificationWantAgent success for ' + info); 268 return wantAgentObjUse; 269 } 270 271 async publishReceiveNotification(wantAgentObjReject: WantAgent, wantAgentObjAccept: WantAgent, 272 wantAgentObjRemove: WantAgent, fileName: string) { 273 let waitTime: number = 50 * 1000; 274 let timeout: number = new Date().getTime() + waitTime; 275 let notificationRequest: notificationManager.NotificationRequest = { 276 content: { 277 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, 278 normal: { 279 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_receive_notification_title').id), 280 text: fileName, 281 } 282 }, 283 id: 1, 284 notificationSlotType: notificationManager.SlotType.SERVICE_INFORMATION, 285 actionButtons: [ 286 { 287 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_receive_notification_button_reject').id), 288 wantAgent: wantAgentObjReject 289 }, 290 { 291 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_receive_notification_button_accept').id), 292 wantAgent: wantAgentObjAccept 293 } 294 ], 295 autoDeletedTime: timeout, 296 removalWantAgent: wantAgentObjRemove 297 }; 298 notificationManager.publish(notificationRequest).then(() => { 299 console.info(TAG, 'publishReceiveNotification success'); 300 this.timerIdReceive = setTimeout(() => { 301 console.info(TAG, 'receive notification 50 seconds but no tap'); 302 this.transfering = false; 303 this.oppProfile.setIncomingFileConfirmation(false, -1); 304 this.pullUpReceiveResultNotification(0, 1); 305 this.handleTerminate(); 306 }, waitTime); 307 }).catch((err: Base.BusinessError) => { 308 console.error(TAG, 'publishReceiveNotification fail'); 309 this.handleTerminate(); 310 }); 311 } 312 313 async publishFinishNotification(wantAgentObj: WantAgent, wantAgentObjRemove: WantAgent, 314 successNum: number, failNum: number) { 315 console.info(TAG, 'publishFinishNotification'); 316 let notificationRequest: notificationManager.NotificationRequest = { 317 content: { 318 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, 319 normal: { 320 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_receive_finish_title').id), 321 text: this.getFormatString($r('app.string.bluetooth_receive_finish_text'), 322 this.getFormatPlural($r('app.plural.bluetooth_receive_finish_success_text', successNum, successNum), successNum), 323 this.getFormatPlural($r('app.plural.bluetooth_receive_finish_fail_text', failNum, failNum), failNum)) 324 } 325 }, 326 id: 2, 327 notificationSlotType: notificationManager.SlotType.SERVICE_INFORMATION, 328 wantAgent: wantAgentObj, 329 tapDismissed: true, 330 removalWantAgent: wantAgentObjRemove 331 }; 332 notificationManager.publish(notificationRequest).then(() => { 333 console.info(TAG, 'publishFinishNotification success'); 334 }).catch((err: Base.BusinessError) => { 335 console.error(TAG, 'publishFinishNotification fail'); 336 this.handleTerminate(); 337 }); 338 } 339 340 startOPPReceiveServiceUIAbility() { 341 console.info(TAG, 'startOPPReceiveServiceUIAbility'); 342 AppStorage.setOrCreate('oppProfile', this.oppProfile); 343 this.oppProfile.setIncomingFileConfirmation(true, -1); 344 } 345 346 async pullUpReceiveResultNotification(successNum: number, failNum: number) { 347 console.info(TAG, 'pullUpReceiveResultNotification'); 348 let wantAgentObj: WantAgent; 349 let wantAgentObjRemove: WantAgent; 350 wantAgentObj = await this.getNotificationWantAgent('ohos.event.notification.BT.FINISH_NOTIFICATION'); 351 wantAgentObjRemove = await this.getNotificationWantAgent('ohos.event.notification.BT.FINISH_REMOVE'); 352 await this.publishFinishNotification(wantAgentObj, wantAgentObjRemove, successNum, failNum); 353 } 354 355 async publishTransProgessNotification(imagePixelMapButton: image.PixelMap, imagePixelMapCapsule: image.PixelMap, 356 wantAgentObjRemove: WantAgent, percent: number, name: string) { 357 console.info(TAG, 'publishTransProgessNotification'); 358 let notificationRequest: notificationManager.NotificationRequest = { 359 notificationSlotType: notificationManager.SlotType.LIVE_VIEW, 360 id: this.capsuleNotificationID, 361 content: { 362 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW, 363 systemLiveView: { 364 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_transfer_notification_title').id), 365 text: this.context.resourceManager.getStringSync($r('app.string.bluetooth_transfer_receive_text').id) + name, 366 typeCode: 8, 367 button: { 368 names: [this.cancelTransEvent], 369 icons: [imagePixelMapButton], 370 }, 371 capsule: { 372 title: 'bluetooth', 373 icon: imagePixelMapCapsule, 374 backgroundColor: '#0A59F7', 375 }, 376 progress: { 377 maxValue: 100, 378 currentValue: percent, 379 isPercentage: true, 380 }, 381 } 382 }, 383 tapDismissed: false, 384 removalWantAgent: wantAgentObjRemove 385 }; 386 387 notificationManager.publish(notificationRequest).then(() => { 388 console.info(TAG, 'publishTransProgessNotification success'); 389 if (percent === 100) { 390 this.cancelSendProgressNotification(1); 391 } 392 }).catch((err: Base.BusinessError) => { 393 console.error(TAG, 'publishTransProgessNotification fail'); 394 }); 395 } 396 397 async pullUpSendProgressNotification(percent: number, name: string) { 398 const currentDate: Date = new Date(); 399 const currentTimeInMsUsingGetTime: number = currentDate.getTime(); 400 if (percent !== 100 && (currentTimeInMsUsingGetTime - this.timeInterval) < 1000) { 401 return; 402 } 403 this.timeInterval = currentTimeInMsUsingGetTime; 404 405 console.info(TAG, 'ready to pullUpSendProgressNotification'); 406 let imagePixelMapButton: image.PixelMap | undefined = undefined; 407 let imagePixelMapCapsule: image.PixelMap | undefined = undefined; 408 try { 409 let drawableDescriptor1: DrawableDescriptor = this.context.resourceManager.getDrawableDescriptor($r('app.media.public_cancel_filled').id); 410 imagePixelMapButton = drawableDescriptor1.getPixelMap(); 411 let drawableDescriptor2: DrawableDescriptor = this.context.resourceManager.getDrawableDescriptor($r('app.media.foreground').id); 412 imagePixelMapCapsule = drawableDescriptor2.getPixelMap(); 413 } catch (error) { 414 let code = (error as BusinessError).code; 415 let message = (error as BusinessError).message; 416 console.error(TAG, `getDrawableDescriptor failed, error code is ${code}, message is ${message}`); 417 return; 418 } 419 420 let wantAgentObjRemove: WantAgent; 421 wantAgentObjRemove = await this.getNotificationWantAgent('ohos.event.notification.BT.LIVEVIEW_REMOVE'); 422 await this.publishTransProgessNotification(imagePixelMapButton, imagePixelMapCapsule, 423 wantAgentObjRemove, percent, name); 424 } 425 426 cancelSendProgressNotification(successOrNot: number) { 427 console.info(TAG, 'cancelSendProgressNotification ready to cancel.'); 428 notificationManager.cancel(this.capsuleNotificationID).then(() => { 429 console.info(TAG, 'Succeeded in canceling notification flag is ' + successOrNot); 430 if (successOrNot === 1) { 431 this.pullUpReceiveResultNotification(1, 0); 432 } else if (successOrNot === 0) { 433 this.handleTerminate(); 434 } 435 }).catch((err: BusinessError) => { 436 console.error(TAG, `failed to cancel notification. Code is ${err.code}, message is ${err.message}`) 437 this.handleTerminate(); 438 }); 439 this.transfering = false; 440 if (this.fileFd != -1) { 441 fs.close(this.fileFd); 442 this.fileFd = -1; 443 } 444 } 445 446 subscriberLiveViewNotification(): void { 447 let subscriber: notificationManager.SystemLiveViewSubscriber = { 448 onResponse: (id: number, option: notificationManager.ButtonOptions) => { 449 switch (option.buttonName) { 450 case this.cancelTransEvent: { 451 console.info(TAG, 'cancel transfer.'); 452 this.tapEventIsAllowed = true; 453 this.oppProfile.cancelTransfer(); 454 this.cancelSendProgressNotification(0); 455 break; 456 } 457 default: { 458 break; 459 } 460 } 461 } 462 }; 463 try { 464 notificationManager.subscribeSystemLiveView(subscriber); 465 } catch(e) { 466 console.error(TAG, 'subscriberLiveViewNotification fail'); 467 } 468 } 469 470 getFormatString(resource: Resource, value1: string, value2: string): string { 471 let result = this.context.resourceManager.getStringSync(resource.id); 472 result = result.replace('%1$s', value1); 473 result = result.replace('%2$s', value2); 474 return result; 475 } 476 477 getFormatPlural(resource: Resource, value: number): string { 478 let result = this.context.resourceManager.getPluralStringValueSync(resource.id, value); 479 result = result.replace('%d', value.toString()); 480 return result; 481 } 482 483 async startContinuousTask() { 484 let wantAgentObj: WantAgent; 485 wantAgentObj = await this.getNotificationWantAgent('ohos.event.notification.BT.BACK_RUNNING'); 486 backgroundTaskManager.startBackgroundRunning(this.context, 487 backgroundTaskManager.BackgroundMode.BLUETOOTH_INTERACTION, wantAgentObj).then(() => { 488 console.info(TAG, `Succeeded in operationing startBackgroundRunning.`); 489 }).catch((err: BusinessError) => { 490 console.error(TAG, `Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`); 491 }); 492 } 493 494 stopContinuousTask() { 495 backgroundTaskManager.stopBackgroundRunning(this.context).then(() => { 496 console.info(TAG, `Succeeded in operationing stopBackgroundRunning.`); 497 }).catch((err: BusinessError) => { 498 console.error(TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`); 499 }); 500 } 501} 502