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