• 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 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