• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}