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 image from '@ohos.multimedia.image'; 20import wantAgent from '@ohos.app.ability.wantAgent'; 21import { WantAgent } from '@ohos.wantAgent'; 22import { BusinessError } from '@ohos.base'; 23import { DrawableDescriptor } from '@ohos.arkui.drawableDescriptor'; 24import opp from '@ohos.bluetooth.opp'; 25import commonEventManager from '@ohos.commonEventManager'; 26import backgroundTaskManager from '@ohos.backgroundTaskManager'; 27import fs from '@ohos.file.fs'; 28import systemParameterEnhance from '@ohos.systemParameterEnhance'; 29 30const TAG: string = '[BT_SEND_SERVICE]==>' 31 32const EVENT_TYPE_TRANSACTION: number = 6; 33const BT_TRANSACTION_EVENT: string = 'usual.event.bluetooth.REPORT.TRANSACTION'; 34const RESULT_SUCCESS: number = 0; 35const RESULT_ERROR_UNSUPPORTED_TYPE: number = 1; 36const RESULT_ERROR_BAD_REQUEST: number = 2; 37const RESULT_ERROR_NOT_ACCEPTABLE: number = 3; 38const RESULT_ERROR_CANCELED: number = 4; 39const RESULT_ERROR_CONNECTION_FAILED: number = 5; 40const RESULT_ERROR_TRANSFER_FAILED: number = 6; 41const RESULT_ERROR_UNKNOWN: number = 7; 42const STATUS_PENDING: number = 0; 43const STATUS_RUNNING: number = 1; 44const STATUS_FINISH: number = 2; 45 46const btTransactionStatisticsType = { 47 TRANSACTION_TYPE_OPP_SEND : 1, 48 TRANSACTION_TYPE_OPP_RECEIVE : 2, 49}; 50 51const btTransactionStatisticsResult = { 52 TRANSACTION_RESULT_TOTAL : 1, 53 TRANSACTION_RESULT_SUCCESS : 2, 54 TRANSACTION_RESULT_FAIL : 3, 55}; 56 57const btTransactionStatisticsSceneCode = { 58 TRANSACTION_SCENECODE_NA : 0, 59 TRANSACTION_SCENECODE_1 : 1, 60 TRANSACTION_SCENECODE_2 : 2, 61 TRANSACTION_SCENECODE_3 : 3, 62 TRANSACTION_SCENECODE_4 : 4, 63 TRANSACTION_SCENECODE_5 : 5, 64 TRANSACTION_SCENECODE_6 : 6, 65 TRANSACTION_SCENECODE_7 : 7, 66 TRANSACTION_SCENECODE_8 : 8, 67 TRANSACTION_SCENECODE_9 : 9, 68 TRANSACTION_SCENECODE_10 : 10, 69 TRANSACTION_SCENECODE_11 : 11, 70 TRANSACTION_SCENECODE_12 : 12, 71 TRANSACTION_SCENECODE_13 : 13, 72 TRANSACTION_SCENECODE_14 : 14, 73 TRANSACTION_SCENECODE_15 : 15, 74 TRANSACTION_SCENECODE_16 : 16, 75}; 76 77export default class BluetoothSendUIAbility extends UIAbility { 78 private oppProfile = opp.createOppServerProfile(); 79 private timeInterval: number = 0; 80 private capsuleNotificationID: number = 200; 81 private cancelTransEvent: string = 'ohos.event.notification.BT.TAP_CANCEL'; 82 private subscriber: commonEventManager.CommonEventSubscriber | null = null; 83 private subscribeinfo: commonEventManager.CommonEventSubscribeInfo = { 84 events: [ 85 'ohos.event.notification.BT.LIVEVIEW_REMOVE' 86 ] 87 }; 88 private timerIdCreate: number; 89 private timerIdCreated: boolean = false; 90 private totalCount: number = 0; 91 private currentCount: number = 0; 92 private successCount: number = 0; 93 private failedCount: number = 0; 94 private isEnabledLive2: boolean = false; 95 96 onCreate(want: Want): void { 97 console.info(TAG, 'BluetoothSendUIAbility onCreate'); 98 this.timerIdCreate = setTimeout(() => { 99 console.info(TAG, 'onCreate 10 seconds but nothing received'); 100 this.handleTerminate(); 101 }, 10000); 102 this.timerIdCreated = true; 103 this.subscribeTransferState(); 104 this.readyReceiveEvent(); 105 this.startContinuousTask(); 106 } 107 108 onDestroy(): void { 109 console.info(TAG, 'BluetoothSendUIAbility onDestroy'); 110 } 111 112 onForeground(): void { 113 console.info(TAG, 'BluetoothSendUIAbility onForeground'); 114 } 115 116 handleTerminate() { 117 this.stopContinuousTask(); 118 commonEventManager.unsubscribe(this.subscriber); 119 this.context.terminateSelf(); 120 } 121 122 reportBtChrDataByResult(dataResult: number) { 123 if (this.successCount + this.failedCount > this.totalCount) { 124 return; 125 } 126 if (dataResult == RESULT_SUCCESS) { 127 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_SUCCESS, 128 1, btTransactionStatisticsSceneCode.TRANSACTION_SCENECODE_NA, 0); 129 } else if ((dataResult == RESULT_ERROR_UNSUPPORTED_TYPE || 130 dataResult == RESULT_ERROR_BAD_REQUEST) && this.totalCount > 0) { 131 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_FAIL, 132 this.totalCount, dataResult, this.totalCount); 133 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_TOTAL, 134 this.totalCount - 1, btTransactionStatisticsSceneCode.TRANSACTION_SCENECODE_NA, 0); 135 } else if (dataResult == RESULT_ERROR_CANCELED || 136 dataResult == RESULT_ERROR_TRANSFER_FAILED) { 137 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_FAIL, 1, dataResult, 1); 138 } else { 139 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_FAIL, 1, dataResult, 1); 140 } 141 this.reportBtTransactionChr(btTransactionStatisticsResult.TRANSACTION_RESULT_TOTAL, 142 1, btTransactionStatisticsSceneCode.TRANSACTION_SCENECODE_NA, 0); 143 } 144 145 reportBtTransactionChr(result: number, resultCount: number, sceneCode: number, sceneCodeCount: number) { 146 const options: commonEventManager.CommonEventPublishData = { 147 code: 0, 148 data: 'message', 149 subscriberPermissions: [], 150 isOrdered: true, 151 isSticky: false, 152 parameters: { 'transactionType': btTransactionStatisticsType.TRANSACTION_TYPE_OPP_SEND, 'result': result, 153 'resultCount': resultCount, 'sceneCode': sceneCode, 'sceneCodeCount': sceneCodeCount} 154 } 155 commonEventManager.publish(BT_TRANSACTION_EVENT, options, (err) => { 156 if (err) { 157 console.info(TAG, 'get bt transaction event publish failed.' + JSON.stringify(err)); 158 } else { 159 console.info(TAG, 'get bt transaction event publish success.'); 160 } 161 }) 162 } 163 164 readyReceiveEvent() { 165 try { 166 console.info(TAG, 'readyReceiveEvent'); 167 this.subscriber = commonEventManager.createSubscriberSync(this.subscribeinfo); 168 try { 169 commonEventManager.subscribe(this.subscriber, 170 (err: BusinessError, data: commonEventManager.CommonEventData) => { 171 if (err) { 172 console.error(TAG, `subscribe failed, code is ${err.code}, message is ${err.message}`); 173 this.handleTerminate(); 174 } else { 175 this.handleReceivedEvent(data); 176 } 177 }); 178 } catch (error) { 179 let err: BusinessError = error as BusinessError; 180 console.error(TAG, `subscribe failed, code is ${err.code}, message is ${err.message}`); 181 this.handleTerminate(); 182 } 183 } catch (error) { 184 let err: BusinessError = error as BusinessError; 185 console.error(TAG, `createSubscriber failed, code is ${err.code}, message is ${err.message}`); 186 this.handleTerminate(); 187 } 188 } 189 190 handleReceivedEvent(data: commonEventManager.CommonEventData) { 191 console.info(TAG, 'handleReceivedEvent: ' + data.event); 192 switch (data.event) { 193 case 'ohos.event.notification.BT.LIVEVIEW_REMOVE': { 194 this.oppProfile.cancelTransfer(); 195 this.handleTerminate(); 196 break; 197 } 198 default: { 199 break; 200 } 201 } 202 } 203 204 subscribeTransferState() { 205 try { 206 this.subscriberLiveViewNotification(); 207 this.oppProfile.on('transferStateChange', (data: opp.OppTransferInformation) => { 208 this.totalCount = data.totalCount; 209 console.info(TAG, 'data.status = ' + data.status + ' data.currentCount = ' + data.currentCount); 210 if (this.timerIdCreated) { 211 this.timerIdCreated = false; 212 clearTimeout(this.timerIdCreate); 213 } 214 if (data.status == STATUS_RUNNING) { 215 this.pullUpSendProgressNotification(data.currentBytes / data.totalBytes * 100, 216 this.getFileName(data.filePath)); 217 this.currentCount = data.currentCount; 218 } else if (data.status == STATUS_FINISH && data.totalCount > 0) { 219 data.result == RESULT_SUCCESS ? this.successCount++ : this.failedCount++; 220 this.reportBtChrDataByResult(data.result); 221 if (data.currentCount == data.totalCount || data.result == RESULT_ERROR_UNSUPPORTED_TYPE) { 222 this.currentCount++; 223 this.cancelSendProgressNotification(); 224 this.pullUpReceiveResultNotification(); 225 console.info(TAG, 'transferStateChange' + data.result); 226 this.oppProfile.off('transferStateChange'); 227 } 228 console.info(TAG, 'transfer finished'); 229 let dirPath = this.context.filesDir; 230 fs.rmdirSync(dirPath); 231 } 232 }); 233 console.log(TAG, 'oppProfile.transferStateChange'); 234 } catch (err) { 235 console.error(TAG, 'subscribeTransferState err'); 236 this.handleTerminate(); 237 } 238 } 239 240 async getNotificationWantAgent(info: string): Promise<WantAgent> { 241 let wantAgentObjUse: WantAgent; 242 let wantAgentInfo: wantAgent.WantAgentInfo = { 243 wants: [ 244 { 245 action: info, 246 } 247 ], 248 actionType: wantAgent.OperationType.SEND_COMMON_EVENT, 249 requestCode: 0, 250 wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG], 251 }; 252 wantAgentObjUse = await wantAgent.getWantAgent(wantAgentInfo); 253 console.info(TAG, 'getNotificationWantAgent success for ' + info); 254 return wantAgentObjUse; 255 } 256 257 async publishFinishNotification(wantAgentObj: WantAgent, successNum: number, failNum: number) { 258 console.info(TAG, 'publishFinishNotification'); 259 let notificationRequest: notificationManager.NotificationRequest = { 260 content: { 261 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, 262 normal: { 263 title: this.context.resourceManager.getStringSync($r('app.string.bluetooth_send_finish_title').id), 264 text: this.getFormatString($r('app.string.bluetooth_send_finish_text'), 265 this.getFormatPlural($r('app.plural.bluetooth_send_finish_success_text', successNum, successNum), successNum), 266 this.getFormatPlural($r('app.plural.bluetooth_send_finish_fail_text', failNum, failNum), failNum)) 267 } 268 }, 269 id: 2, 270 notificationSlotType: notificationManager.SlotType.SERVICE_INFORMATION, 271 wantAgent: wantAgentObj, 272 tapDismissed: true, 273 }; 274 notificationManager.publish(notificationRequest).then(() => { 275 console.info(TAG, 'publishFinishNotification success'); 276 }).catch((err: BusinessError) => { 277 console.error(TAG, 'publishFinishNotification fail'); 278 this.handleTerminate(); 279 }); 280 } 281 282 async pullUpReceiveResultNotification() { 283 console.info(TAG, 'pullUpNotification successCount' + this.successCount + 'failedCount' + this.failedCount); 284 if (this.successCount + this.failedCount != this.totalCount && this.totalCount >= this.successCount) { 285 this.failedCount = this.totalCount - this.successCount; 286 } 287 let wantAgentObj: WantAgent; 288 wantAgentObj = await this.getNotificationWantAgent('ohos.event.notification.BT.FINISH_SEND'); 289 await this.publishFinishNotification(wantAgentObj, this.successCount, this.failedCount); 290 this.handleTerminate(); 291 } 292 293 async publishTransProgessNotification(imagePixelMapButton: image.PixelMap, imagePixelMapCapsule: image.PixelMap, 294 wantAgentObjRemove: WantAgent, percent: number, name: string, currentCount: number, totalCount: number) { 295 console.info(TAG, 'publishTransProgessNotification'); 296 let notificationRequest: notificationManager.NotificationRequest = { 297 notificationSlotType: notificationManager.SlotType.LIVE_VIEW, 298 id: this.capsuleNotificationID, 299 content: { 300 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW, 301 systemLiveView: { 302 title: this.getFormatNum($r('app.string.bluetooth_send_count_text'), currentCount, totalCount), 303 text: name, 304 typeCode: 8, 305 button: { 306 names: [this.cancelTransEvent], 307 iconsResource: [$r('app.media.public_cancel_filled')], 308 }, 309 capsule: { 310 title: 'bluetooth', 311 icon: imagePixelMapCapsule, 312 backgroundColor: '#0A59F7', 313 }, 314 progress: { 315 maxValue: 100, 316 currentValue: percent, 317 isPercentage: true, 318 }, 319 } 320 }, 321 tapDismissed: false, 322 removalWantAgent: wantAgentObjRemove 323 }; 324 325 notificationManager.publish(notificationRequest).then(() => { 326 console.info(TAG, 'publishTransProgessNotification success'); 327 if (percent == 100 && this.currentCount > this.totalCount) { 328 this.cancelSendProgressNotification(); 329 } 330 }).catch((err: BusinessError) => { 331 console.error(TAG, 'publishTransProgessNotification fail'); 332 }); 333 } 334 335 async createSendProgressNotification(name: string, progress: number, currentCount: number, totalCount: number) { 336 let notificationRequest: notificationManager.NotificationRequest = { 337 notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, 338 id: this.capsuleNotificationID, 339 template: { 340 name: 'downloadTemplate', 341 data: { 342 title: this.getFormatNum($r('app.string.bluetooth_send_count_text'), currentCount, totalCount), 343 fileName: name, 344 progressValue: progress, 345 progressMaxValue: 100 346 } 347 }, 348 content: { 349 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, 350 normal: { 351 title: this.getFormatNum($r('app.string.bluetooth_send_count_text'), currentCount, totalCount), 352 text: name 353 } 354 }, 355 extraInfo: { 356 } 357 }; 358 359 notificationManager.publish(notificationRequest).then(() => { 360 console.info(TAG, 'createSendProgressNotification success'); 361 if (progress == 100 && this.currentCount > this.totalCount) { 362 this.cancelSendProgressNotification(); 363 } 364 }).catch((err: BusinessError) => { 365 console.error(TAG, 'createSendProgressNotification fail'); 366 }); 367 } 368 369 async pullUpSendProgressNotification(percent: number, name: string) { 370 const currentDate: Date = new Date(); 371 const currentTimeInMsUsingGetTime: number = currentDate.getTime(); 372 if (percent !== 100 && (currentTimeInMsUsingGetTime - this.timeInterval) < 1000) { 373 return; 374 } 375 this.timeInterval = currentTimeInMsUsingGetTime; 376 377 console.info(TAG, 'ready to pullUpSendProgressNotification'); 378 this.isEnabledLive2 = systemParameterEnhance.getSync('persist.systemui.live2', 'false') == 'true'; 379 console.info(TAG, 'this.isEnabledLive2 = ' + this.isEnabledLive2); 380 381 let imagePixelMapButton: image.PixelMap | undefined = undefined; 382 let imagePixelMapCapsule: image.PixelMap | undefined = undefined; 383 try { 384 let drawableDescriptor1: DrawableDescriptor = this.context.resourceManager.getDrawableDescriptor($r('app.media.public_cancel_filled').id); 385 imagePixelMapButton = drawableDescriptor1.getPixelMap(); 386 let drawableDescriptor2: DrawableDescriptor; 387 if (!this.isEnabledLive2) { 388 drawableDescriptor2 = 389 this.context.resourceManager.getDrawableDescriptor($r('app.media.foreground').id); 390 } else { 391 drawableDescriptor2 = 392 this.context.resourceManager.getDrawableDescriptor($r('app.media.foregroundSmall').id); 393 } 394 imagePixelMapCapsule = drawableDescriptor2.getPixelMap(); 395 } catch (error) { 396 let code = (error as BusinessError).code; 397 let message = (error as BusinessError).message; 398 console.error(TAG, `getDrawableDescriptor failed, error code is ${code}, message is ${message}`); 399 return; 400 } 401 402 await this.createSendProgressNotification(name, percent, this.currentCount, this.totalCount); 403 } 404 405 cancelSendProgressNotification() { 406 console.info(TAG, 'cancelSendProgressNotification ready to cancel.'); 407 notificationManager.cancel(this.capsuleNotificationID).then(() => { 408 console.info(TAG, 'Succeeded in canceling notification.'); 409 }).catch((err: BusinessError) => { 410 console.error(TAG, `failed to cancel notification. Code is ${err.code}, message is ${err.message}`) 411 this.handleTerminate(); 412 }); 413 } 414 415 subscriberLiveViewNotification(): void { 416 let subscriber: notificationManager.SystemLiveViewSubscriber = { 417 onResponse: (id: number, option: notificationManager.ButtonOptions) => { 418 switch (option.buttonName) { 419 case this.cancelTransEvent: { 420 console.info(TAG, 'cancel transfer.'); 421 this.oppProfile.cancelTransfer(); 422 this.cancelSendProgressNotification(); 423 break; 424 } 425 default: { 426 break; 427 } 428 } 429 } 430 }; 431 try { 432 notificationManager.subscribeSystemLiveView(subscriber); 433 } catch (e) { 434 console.error(TAG, 'subscriberLiveViewNotification fail'); 435 } 436 } 437 438 getFileName(filePath: string): string { 439 let extension = filePath.substring(filePath.lastIndexOf('/') + 1); 440 return extension; 441 } 442 443 getFormatString(resource: Resource, value1: string, value2: string): string { 444 let result = this.context.resourceManager.getStringSync(resource.id); 445 result = result.replace('%1$s', value1); 446 result = result.replace('%2$s', value2); 447 return result; 448 } 449 450 getFormatNum(resource: Resource, value1: number, value2: number): string { 451 let result = this.context.resourceManager.getStringSync(resource.id); 452 result = result.replace('%1$d', value1.toString()); 453 result = result.replace('%2$d', value2.toString()); 454 return result; 455 } 456 457 getFormatPlural(resource: Resource, value: number): string { 458 let result = this.context.resourceManager.getPluralStringValueSync(resource.id, value); 459 result = result.replace('%d', value.toString()); 460 return result; 461 } 462 463 async startContinuousTask() { 464 let wantAgentObj: WantAgent; 465 wantAgentObj = await this.getNotificationWantAgent('ohos.event.notification.BT.BACK_RUNNING'); 466 backgroundTaskManager.startBackgroundRunning(this.context, 467 backgroundTaskManager.BackgroundMode.BLUETOOTH_INTERACTION, wantAgentObj).then(() => { 468 console.info(TAG, `Succeeded in operationing startBackgroundRunning.`); 469 }).catch((err: BusinessError) => { 470 console.error(TAG, `Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`); 471 }); 472 } 473 474 stopContinuousTask() { 475 backgroundTaskManager.stopBackgroundRunning(this.context).then(() => { 476 console.info(TAG, `Succeeded in operationing stopBackgroundRunning.`); 477 }).catch((err: BusinessError) => { 478 console.error(TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`); 479 }); 480 } 481}