1/* 2 * Copyright (c) 2023 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 display from '@ohos.display'; 17import emitter from '@ohos.events.emitter'; 18import extension from '@ohos.app.ability.ServiceExtensionAbility'; 19import window from '@ohos.window'; 20import CommonEventManager from '@ohos.commonEventManager'; 21import type Want from '@ohos.app.ability.Want'; 22import UIExtensionAbility from '@ohos.app.ability.UIExtensionAbility'; 23import UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession'; 24import uiExtension from '@ohos.arkui.uiExtension'; 25import StartOptions from '@ohos.app.ability.StartOptions'; 26import configPolicy from '@ohos.configPolicy'; 27import fs from '@ohos.file.fs'; 28import Constants from '../common/constant'; 29 30 31 32const TAG = 'NotificationDialog_Service '; 33 34const UPDATE_INIT = 1; 35const UPDATE_NUM = 1; 36const UPDATE_BOUNDARY = 100; 37 38let eventSubscriber:CommonEventManager.CommonEventSubscriber; 39 40const enableNotificationDialogDestroyedEvent = { 41 eventId: 1, 42 priority: emitter.EventPriority.LOW 43}; 44 45const COMMON_EVENT_NAME = 'OnNotificationServiceDialogClicked'; 46enum DialogStatus { 47 ALLOW_CLICKED, 48 DENY_CLICKED, 49 DIALOG_CRASHED, 50 DIALOG_SERVICE_DESTROYED, 51 REMOVE_BUNDLE, 52 DIALOG_OPEN 53}; 54 55async function handleDialogQuitException(want: Want): Promise<void> { 56 CommonEventManager.publish( 57 COMMON_EVENT_NAME, 58 { 59 code: DialogStatus.DIALOG_CRASHED, 60 data: want.parameters.bundleName.toString(), 61 parameters: { 62 bundleName: want.parameters.bundleName.toString(), 63 bundleUid: want.parameters.bundleUid.toString() 64 } 65 } as CommonEventManager.CommonEventPublishData, 66 () => { console.info(TAG, 'publish DIALOG_CRASHED succeeded'); } 67 ); 68} 69 70interface NotificationConfig { 71 deviceInfo: DeviceInfo; 72} 73 74interface DeviceInfo { 75 isWatch: boolean; 76 isPc: boolean; 77} 78 79export class EnableNotificationDialog { 80 static ENABLE_NOTIFICATION_DIALOG_NAME = 'EnableNotificationDialog'; 81 static DIALOG_PATH = 'pages/notificationDialog'; 82 static WATCH_DIALOG_PATH = 'pages/watchNotificationDialog'; 83 static PC_DIALOG_PATH = 'pages/pcNotificationDialog'; 84 static TRANSPARANT_COLOR = '#00000000'; 85 static SCENEBOARD_BUNDLE = 'com.ohos.sceneboard'; 86 static SYSTEMUI_BUNDLE = 'com.ohos.systemui'; 87 88 id: number; 89 want: Want; 90 window: window.Window; 91 extensionWindow:uiExtension.WindowProxy; 92 storage: LocalStorage; 93 stageModel: boolean; 94 subWindow: window.Window; 95 initSubWindowSize: boolean; 96 innerLake: boolean; 97 98 constructor(id: number, want: Want, stageModel: boolean, innerLake: boolean) { 99 this.id = id; 100 this.want = want; 101 this.stageModel = stageModel; 102 this.window = undefined; 103 this.extensionWindow = undefined; 104 this.initSubWindowSize = false; 105 this.innerLake = innerLake; 106 } 107 108 109 async createUiExtensionWindow(session: UIExtensionContentSession, stageModel: boolean): Promise<void> { 110 try { 111 let extensionWindow = session.getUIExtensionWindowProxy(); 112 this.extensionWindow = extensionWindow; 113 let shouldHide = true; 114 115 this.storage = new LocalStorage({ 116 'dialog': this, 117 'session': session 118 }); 119 120 let path = EnableNotificationDialog.DIALOG_PATH; 121 let hasConfig = true; 122 let isPcDevice = false; 123 try { 124 let filePaths = await configPolicy.getCfgFiles(Constants.CCM_CONFIG_PATH); 125 if (filePaths.length === 0) { 126 console.info(TAG, 'not get any configFile'); 127 hasConfig = false; 128 } 129 for (let i = 0; i < filePaths.length; i++) { 130 let res = fs.accessSync(filePaths[i]); 131 if (res) { 132 let fileContent = fs.readTextSync(filePaths[i]); 133 let config: NotificationConfig = JSON.parse(fileContent); 134 if (config.deviceInfo !== undefined) { 135 let deviceInfo: DeviceInfo = config.deviceInfo; 136 if (deviceInfo.isWatch !== undefined) { 137 path = EnableNotificationDialog.WATCH_DIALOG_PATH; 138 console.info(TAG, 'watch request'); 139 } 140 if (deviceInfo.isPc !== undefined) { 141 path = EnableNotificationDialog.PC_DIALOG_PATH; 142 isPcDevice = true; 143 console.info(TAG, 'pc request'); 144 } 145 } 146 } 147 } 148 } catch (err) { 149 console.error(TAG, 'Failed get ccm files'); 150 } 151 152 if (stageModel && hasConfig) { 153 let subWindowOpts : window.SubWindowOptions = { 154 'title': '', 155 decorEnabled: false, 156 isModal: true, 157 isTopmost: true 158 }; 159 let subWindow = await extensionWindow.createSubWindowWithOptions('subWindowForHost' + Date(), subWindowOpts); 160 this.subWindow = subWindow; 161 162 if(isPcDevice) { 163 let hasDisalogRectInfo = false; 164 let waiteTimes = 0; 165 extensionWindow.on('rectChange', uiExtension.RectChangeReason.HOST_WINDOW_RECT_CHANGE, (data):void => { 166 console.info(TAG, `windowRectChange ts event ${data.rect?.left},${data.rect?.top}, ${data.rect?.width}, ${data.rect?.height}`); 167 hasDisalogRectInfo = true; 168 }); 169 while(!hasDisalogRectInfo && waiteTimes < 10){ 170 waiteTimes ++; 171 await this.sleep(200); 172 } 173 if(hasDisalogRectInfo) { 174 let windowRect = extensionWindow.properties?.uiExtensionHostWindowProxyRect; 175 console.info(TAG, `size : ${windowRect?.left} ${windowRect?.top} ${windowRect?.width} ${windowRect?.height}`); 176 await subWindow.moveWindowToGlobal(windowRect?.left, windowRect?.top); 177 await subWindow.resize(windowRect?.width, windowRect?.height); 178 hasDisalogRectInfo = false; 179 } else { 180 console.info(TAG,'waite send windwow info fail'); 181 throw new Error('Failed to create window'); 182 } 183 } else { 184 let windowRect = extensionWindow.properties?.uiExtensionHostWindowProxyRect; 185 console.info(TAG, `size : ${windowRect?.left} ${windowRect?.top} ${windowRect?.width} ${windowRect?.height}`); 186 if (windowRect.width > 0 && windowRect.height > 0) { 187 console.log(TAG, `valid rect data`); 188 await subWindow.moveWindowToGlobal(windowRect?.left, windowRect?.top); 189 await subWindow.resize(windowRect?.width, windowRect?.height); 190 this.initSubWindowSize = true; 191 } 192 } 193 await subWindow.loadContent(path, this.storage); 194 try { 195 await subWindow.hideNonSystemFloatingWindows(true); 196 } catch (err) { 197 console.error(TAG, 'subWindow hideNonSystemFloatingWindows failed!'); 198 } 199 await subWindow.setWindowBackgroundColor(EnableNotificationDialog.TRANSPARANT_COLOR); 200 await subWindow.showWindow(); 201 } else { 202 await session.loadContent(path, this.storage); 203 try { 204 await extensionWindow.hideNonSecureWindows(shouldHide); 205 } catch (err) { 206 console.error(TAG, 'window hideNonSecureWindows failed!'); 207 } 208 await session.setWindowBackgroundColor(EnableNotificationDialog.TRANSPARANT_COLOR); 209 } 210 } catch (err) { 211 console.error(TAG, 'window create failed!'); 212 throw new Error('Failed to create window'); 213 } 214 } 215 216 async sleep(ms: number): Promise<void> { 217 return new Promise(resolve => setTimeout(resolve, ms)); 218 } 219 220 async publishButtonClickedEvent(enabled: boolean): Promise<void> { 221 CommonEventManager.publish( 222 COMMON_EVENT_NAME, 223 { 224 code: enabled ? DialogStatus.ALLOW_CLICKED : DialogStatus.DENY_CLICKED, 225 data: this.want.parameters.bundleName.toString(), 226 parameters: { 227 bundleName: this.want.parameters.bundleName.toString(), 228 bundleUid: this.want.parameters.bundleUid.toString() 229 } 230 } as CommonEventManager.CommonEventPublishData, 231 () => { console.info(TAG, 'publish CLICKED succeeded'); } 232 ); 233 } 234 235 async dialogOpenEvent(): Promise<void> { 236 CommonEventManager.publish( 237 COMMON_EVENT_NAME, 238 { 239 code: DialogStatus.DIALOG_OPEN, 240 data: this.want.parameters.bundleName.toString(), 241 parameters: { 242 bundleName: this.want.parameters.bundleName.toString(), 243 bundleUid: this.want.parameters.bundleUid.toString() 244 } 245 } as CommonEventManager.CommonEventPublishData, 246 () => { console.info(TAG, 'publish DIALOG OPEN event succeeded'); } 247 ); 248 } 249 250 async destroyException(): Promise<void> { 251 await handleDialogQuitException(this.want); 252 } 253 254 async destroy(): Promise<void> { 255 if (this.window !== undefined) { 256 emitter.emit(enableNotificationDialogDestroyedEvent, { 257 data: { 258 'id': this.id 259 } 260 }); 261 await this.destroyWindow(); 262 } 263 } 264 265 async destroyWindow(): Promise<void> { 266 await this.window.destroyWindow(); 267 this.window = undefined; 268 } 269}; 270 271 272class NotificationDialogServiceExtensionAbility extends UIExtensionAbility { 273 274 onCreate() { 275 console.log(TAG, `UIExtAbility onCreate`); 276 AppStorage.setOrCreate('context', this.context); 277 AppStorage.setOrCreate('isUpdate', UPDATE_INIT); 278 AppStorage.setOrCreate('clicked', false); 279 this.subscribe(); 280 281 } 282 283 async onSessionCreate(want: Want, session: UIExtensionContentSession) { 284 try { 285 let stageModel = false; 286 let bundleName = want.parameters['ohos.aafwk.param.callerBundleName']; 287 let bundleUid = want.parameters['ohos.aafwk.param.callerUid']; 288 let innerLake = false; 289 if (bundleName !== EnableNotificationDialog.SCENEBOARD_BUNDLE && 290 bundleName !== EnableNotificationDialog.SYSTEMUI_BUNDLE) { 291 want.parameters.bundleName = bundleName; 292 want.parameters.bundleUid = bundleUid; 293 stageModel = true; 294 console.log(TAG, `stage model`); 295 } else { 296 stageModel = false; 297 innerLake = Boolean(want.parameters.innerLake); 298 console.log(TAG, ` un stage model innerLake = , ${innerLake}`); 299 } 300 console.log(TAG, `UIExtAbility onSessionCreate bundleName ${want.parameters.bundleName}` + 301 `uid ${want.parameters.bundleUid}`); 302 let dialog = new EnableNotificationDialog(1, want, stageModel, innerLake); 303 await dialog.createUiExtensionWindow(session, stageModel); 304 AppStorage.setOrCreate('dialog', dialog); 305 } catch (err) { 306 console.error(TAG, `Failed to handle onSessionCreate`); 307 await handleDialogQuitException(want); 308 this.context.terminateSelf(); 309 } 310 } 311 312 onForeground() { 313 console.log(TAG, `UIExtAbility onForeground`); 314 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 315 316 if (dialog?.subWindow !== undefined) { 317 try { 318 dialog?.subWindow?.hideNonSystemFloatingWindows(true); 319 } catch (err) { 320 console.error(TAG, 'onForeground hideNonSystemFloatingWindows failed!'); 321 } 322 } else { 323 try { 324 dialog?.extensionWindow?.hideNonSecureWindows(true); 325 } catch (err) { 326 console.error(TAG, 'onForeground hideNonSecureWindows failed!'); 327 } 328 } 329 } 330 331 onBackground() { 332 console.log(TAG, `UIExtAbility onBackground`); 333 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 334 335 if (dialog?.subWindow !== undefined) { 336 try { 337 dialog?.subWindow?.hideNonSystemFloatingWindows(false); 338 } catch (err) { 339 console.error(TAG, 'onBackground hideNonSystemFloatingWindows failed!'); 340 } 341 } else { 342 try { 343 dialog?.extensionWindow?.hideNonSecureWindows(false); 344 } catch (err) { 345 console.error(TAG, 'onBackground hideNonSecureWindows failed!'); 346 } 347 } 348 } 349 350 async onSessionDestroy(session: UIExtensionContentSession): Promise<void> { 351 console.log(TAG, `UIExtAbility onSessionDestroy`); 352 if (AppStorage.get('clicked') === false) { 353 console.log(TAG, `UIExtAbility onSessionDestroy unclick destory`); 354 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 355 await dialog?.destroyException(); 356 } 357 } 358 359 async onDestroy(): Promise<void> { 360 console.info(TAG, 'UIExtAbility onDestroy.'); 361 await this.unsubscribe(); 362 await this.sleep(500); 363 this.context.terminateSelf(); 364 } 365 366 async sleep(ms: number): Promise<void> { 367 return new Promise(resolve => setTimeout(resolve, ms)); 368 } 369 370 async subscribe(): Promise<void> { 371 await CommonEventManager.createSubscriber( 372 { events: ['usual.event.BUNDLE_RESOURCES_CHANGED'] }) 373 .then((subscriber:CommonEventManager.CommonEventSubscriber) => { 374 eventSubscriber = subscriber; 375 }) 376 .catch((err) => { 377 console.log(TAG, `subscriber createSubscriber error code is ${err.code}, message is ${err.message}`); 378 }); 379 380 if (eventSubscriber === null) { 381 console.log(TAG, 'need create subscriber'); 382 return; 383 } 384 CommonEventManager.subscribe(eventSubscriber, (err, data) => { 385 if (err?.code) { 386 console.error(TAG, `subscribe callBack err= ${JSON.stringify(err)}`); 387 } else { 388 console.log(TAG, `subscribe callBack data= ${JSON.stringify(data)}`); 389 if (data.parameters?.bundleResourceChangeType !== 1) { 390 return; 391 } 392 console.log(TAG, `BUNDLE_RESOURCES_CHANGED-language change`); 393 let isUpdate:number = AppStorage.get('isUpdate'); 394 if (isUpdate === undefined || isUpdate > UPDATE_BOUNDARY) { 395 AppStorage.setOrCreate('isUpdate', UPDATE_NUM); 396 } else { 397 AppStorage.setOrCreate('isUpdate', ++isUpdate); 398 } 399 } 400 }); 401 } 402 403 async unsubscribe(): Promise<void> { 404 try { 405 if (eventSubscriber != null) { 406 CommonEventManager.unsubscribe(eventSubscriber, (err) => {}); 407 } 408 } catch (err) { 409 console.info('ubsubscribe fail'); 410 } 411 } 412} 413 414 415export default NotificationDialogServiceExtensionAbility; 416