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 uiExtensionHost from '@ohos.uiExtensionHost'; 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}; 52 53async function handleDialogQuitException(want: Want): Promise<void> { 54 CommonEventManager.publish( 55 COMMON_EVENT_NAME, 56 { 57 code: DialogStatus.DIALOG_CRASHED, 58 data: want.parameters.bundleName.toString(), 59 parameters: { 60 bundleName: want.parameters.bundleName.toString(), 61 bundleUid: want.parameters.bundleUid.toString() 62 } 63 } as CommonEventManager.CommonEventPublishData, 64 () => { console.info(TAG, 'publish DIALOG_CRASHED succeeded'); } 65 ); 66} 67 68interface NotificationConfig { 69 deviceInfo: DeviceInfo; 70} 71 72interface DeviceInfo { 73 isWatch: boolean; 74 isPc: boolean; 75} 76 77export class EnableNotificationDialog { 78 static ENABLE_NOTIFICATION_DIALOG_NAME = 'EnableNotificationDialog'; 79 static DIALOG_PATH = 'pages/notificationDialog'; 80 static WATCH_DIALOG_PATH = 'pages/watchNotificationDialog'; 81 static PC_DIALOG_PATH = 'pages/pcNotificationDialog'; 82 static TRANSPARANT_COLOR = '#00000000'; 83 static SCENEBOARD_BUNDLE = 'com.ohos.sceneboard'; 84 static SYSTEMUI_BUNDLE = 'com.ohos.systemui'; 85 86 id: number; 87 want: Want; 88 window: window.Window; 89 extensionWindow:uiExtensionHost.UIExtensionHostWindowProxy; 90 storage: LocalStorage; 91 stageModel: boolean; 92 subWindow: window.Window; 93 94 constructor(id: number, want: Want, stageModel: boolean) { 95 this.id = id; 96 this.want = want; 97 this.stageModel = stageModel; 98 this.window = undefined; 99 this.extensionWindow = undefined; 100 } 101 102 103 async createUiExtensionWindow(session: UIExtensionContentSession, stageModel: boolean): Promise<void> { 104 try { 105 let extensionWindow = session.getUIExtensionHostWindowProxy(); 106 this.extensionWindow = extensionWindow; 107 let shouldHide = true; 108 109 this.storage = new LocalStorage({ 110 'dialog': this, 111 'session': session 112 }); 113 114 let path = EnableNotificationDialog.DIALOG_PATH; 115 let hasConfig = true; 116 try { 117 let filePaths = await configPolicy.getCfgFiles(Constants.CCM_CONFIG_PATH); 118 if (filePaths.length === 0) { 119 console.info(TAG, 'not get any configFile'); 120 hasConfig = false; 121 } 122 for (let i = 0; i < filePaths.length; i++) { 123 let res = fs.accessSync(filePaths[i]); 124 if (res) { 125 let fileContent = fs.readTextSync(filePaths[i]); 126 let config: NotificationConfig = JSON.parse(fileContent); 127 if (config.deviceInfo !== undefined) { 128 let deviceInfo: DeviceInfo = config.deviceInfo; 129 if (deviceInfo.isWatch !== undefined) { 130 path = EnableNotificationDialog.WATCH_DIALOG_PATH; 131 console.info(TAG, 'watch request'); 132 } 133 if (deviceInfo.isPc !== undefined) { 134 path = EnableNotificationDialog.PC_DIALOG_PATH; 135 console.info(TAG, 'pc request'); 136 } 137 } 138 } 139 } 140 } catch (err) { 141 console.error(TAG, 'Failed get ccm files'); 142 } 143 144 if (stageModel && hasConfig) { 145 let subWindowOpts : window.SubWindowOptions = { 146 'title': '', 147 decorEnabled: false, 148 isModal: true, 149 isTopmost: true 150 }; 151 let subWindow = await extensionWindow.createSubWindowWithOptions('subWindowForHost' + Date(), subWindowOpts); 152 this.subWindow = subWindow; 153 await this.sleep(200); 154 let windowRect = extensionWindow.properties?.uiExtensionHostWindowProxyRect; 155 console.info(TAG, `size : ${windowRect?.left} ${windowRect?.top} ${windowRect?.width} ${windowRect?.height}`); 156 await subWindow.moveWindowTo(windowRect?.left, windowRect?.top); 157 await subWindow.resize(windowRect?.width, windowRect?.height); 158 await subWindow.loadContent(path, this.storage); 159 try { 160 await subWindow.hideNonSystemFloatingWindows(true); 161 } catch (err) { 162 console.error(TAG, 'subWindow hideNonSystemFloatingWindows failed!'); 163 } 164 await subWindow.setWindowBackgroundColor(EnableNotificationDialog.TRANSPARANT_COLOR); 165 await subWindow.showWindow(); 166 } else { 167 await session.loadContent(path, this.storage); 168 try { 169 await extensionWindow.hideNonSecureWindows(shouldHide); 170 } catch (err) { 171 console.error(TAG, 'window hideNonSecureWindows failed!'); 172 } 173 await session.setWindowBackgroundColor(EnableNotificationDialog.TRANSPARANT_COLOR); 174 } 175 } catch (err) { 176 console.error(TAG, 'window create failed!'); 177 throw new Error('Failed to create window'); 178 } 179 } 180 181 async sleep(ms: number): Promise<void> { 182 return new Promise(resolve => setTimeout(resolve, ms)); 183 } 184 185 async publishButtonClickedEvent(enabled: boolean): Promise<void> { 186 CommonEventManager.publish( 187 COMMON_EVENT_NAME, 188 { 189 code: enabled ? DialogStatus.ALLOW_CLICKED : DialogStatus.DENY_CLICKED, 190 data: this.want.parameters.bundleName.toString(), 191 parameters: { 192 bundleName: this.want.parameters.bundleName.toString(), 193 bundleUid: this.want.parameters.bundleUid.toString() 194 } 195 } as CommonEventManager.CommonEventPublishData, 196 () => { console.info(TAG, 'publish CLICKED succeeded'); } 197 ); 198 } 199 200 async destroyException(): Promise<void> { 201 await handleDialogQuitException(this.want); 202 } 203 204 async destroy(): Promise<void> { 205 if (this.window !== undefined) { 206 emitter.emit(enableNotificationDialogDestroyedEvent, { 207 data: { 208 'id': this.id 209 } 210 }); 211 await this.destroyWindow(); 212 } 213 } 214 215 async destroyWindow(): Promise<void> { 216 await this.window.destroyWindow(); 217 this.window = undefined; 218 } 219}; 220 221 222class NotificationDialogServiceExtensionAbility extends UIExtensionAbility { 223 224 onCreate() { 225 console.log(TAG, `UIExtAbility onCreate`); 226 AppStorage.setOrCreate('context', this.context); 227 AppStorage.setOrCreate('isUpdate', UPDATE_INIT); 228 AppStorage.setOrCreate('clicked', false); 229 this.subscribe(); 230 231 } 232 233 async onSessionCreate(want: Want, session: UIExtensionContentSession) { 234 try { 235 let stageModel = false; 236 let bundleName = want.parameters['ohos.aafwk.param.callerBundleName']; 237 let bundleUid = want.parameters['ohos.aafwk.param.callerUid']; 238 if (bundleName !== EnableNotificationDialog.SCENEBOARD_BUNDLE && 239 bundleName !== EnableNotificationDialog.SYSTEMUI_BUNDLE) { 240 want.parameters.bundleName = bundleName; 241 want.parameters.bundleUid = bundleUid; 242 stageModel = true; 243 } else { 244 stageModel = false; 245 } 246 console.log(TAG, `UIExtAbility onSessionCreate bundleName ${want.parameters.bundleName}` + 247 `uid ${want.parameters.bundleUid}`); 248 let dialog = new EnableNotificationDialog(1, want, stageModel); 249 await dialog.createUiExtensionWindow(session, stageModel); 250 AppStorage.setOrCreate('dialog', dialog); 251 } catch (err) { 252 console.error(TAG, `Failed to handle onSessionCreate`); 253 await handleDialogQuitException(want); 254 this.context.terminateSelf(); 255 } 256 } 257 258 onForeground() { 259 console.log(TAG, `UIExtAbility onForeground`); 260 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 261 262 if (dialog?.subWindow !== undefined) { 263 try { 264 dialog?.subWindow?.hideNonSystemFloatingWindows(true); 265 } catch (err) { 266 console.error(TAG, 'onForeground hideNonSystemFloatingWindows failed!'); 267 } 268 } else { 269 try { 270 dialog?.extensionWindow?.hideNonSecureWindows(true); 271 } catch (err) { 272 console.error(TAG, 'onForeground hideNonSecureWindows failed!'); 273 } 274 } 275 } 276 277 onBackground() { 278 console.log(TAG, `UIExtAbility onBackground`); 279 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 280 281 if (dialog?.subWindow !== undefined) { 282 try { 283 dialog?.subWindow?.hideNonSystemFloatingWindows(false); 284 } catch (err) { 285 console.error(TAG, 'onBackground hideNonSystemFloatingWindows failed!'); 286 } 287 } else { 288 try { 289 dialog?.extensionWindow?.hideNonSecureWindows(false); 290 } catch (err) { 291 console.error(TAG, 'onBackground hideNonSecureWindows failed!'); 292 } 293 } 294 } 295 296 async onSessionDestroy(session: UIExtensionContentSession): Promise<void> { 297 console.log(TAG, `UIExtAbility onSessionDestroy`); 298 if (AppStorage.get('clicked') === false) { 299 console.log(TAG, `UIExtAbility onSessionDestroy unclick destory`); 300 let dialog = AppStorage.get<EnableNotificationDialog>('dialog'); 301 await dialog?.destroyException(); 302 } 303 } 304 305 async onDestroy(): Promise<void> { 306 console.info(TAG, 'UIExtAbility onDestroy.'); 307 await this.unsubscribe(); 308 await this.sleep(500); 309 this.context.terminateSelf(); 310 } 311 312 async sleep(ms: number): Promise<void> { 313 return new Promise(resolve => setTimeout(resolve, ms)); 314 } 315 316 async subscribe(): Promise<void> { 317 await CommonEventManager.createSubscriber( 318 { events: ['usual.event.BUNDLE_RESOURCES_CHANGED'] }) 319 .then((subscriber:CommonEventManager.CommonEventSubscriber) => { 320 eventSubscriber = subscriber; 321 }) 322 .catch((err) => { 323 console.log(TAG, `subscriber createSubscriber error code is ${err.code}, message is ${err.message}`); 324 }); 325 326 if (eventSubscriber === null) { 327 console.log(TAG, 'need create subscriber'); 328 return; 329 } 330 CommonEventManager.subscribe(eventSubscriber, (err, data) => { 331 if (err?.code) { 332 console.error(TAG, `subscribe callBack err= ${JSON.stringify(err)}`); 333 } else { 334 console.log(TAG, `subscribe callBack data= ${JSON.stringify(data)}`); 335 if (data.parameters?.bundleResourceChangeType !== 1) { 336 return; 337 } 338 console.log(TAG, `BUNDLE_RESOURCES_CHANGED-language change`); 339 let isUpdate:number = AppStorage.get('isUpdate'); 340 if (isUpdate === undefined || isUpdate > UPDATE_BOUNDARY) { 341 AppStorage.setOrCreate('isUpdate', UPDATE_NUM); 342 } else { 343 AppStorage.setOrCreate('isUpdate', ++isUpdate); 344 } 345 } 346 }); 347 } 348 349 async unsubscribe(): Promise<void> { 350 try { 351 if (eventSubscriber != null) { 352 CommonEventManager.unsubscribe(eventSubscriber, (err) => {}); 353 } 354 } catch (err) { 355 console.info('ubsubscribe fail'); 356 } 357 } 358} 359 360 361export default NotificationDialogServiceExtensionAbility; 362