• 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]==>'
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}