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 bundleResourceManager from '@ohos.bundle.bundleResourceManager'; 17import display from '@ohos.display'; 18import window from '@ohos.window'; 19import { 20 titleTrim, 21 calContainerWidth, 22 getFontSizeScale, 23 sourceToVp, 24 getLimitFontSize } from '../common/utils'; 25import Constants from '../common/constant'; 26import fs from '@ohos.file.fs'; 27import configPolicy from '@ohos.configPolicy'; 28import { EnableNotificationDialog } from '../ServiceExtAbility/NotificationServiceExtAbility'; 29import { Callback} from '@ohos.base'; 30import UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession'; 31import { MeasureOptions } from '@ohos.measure'; 32import { MeasureUtils } from '@ohos.arkui.UIContext'; 33import common from '@ohos.app.ability.common'; 34 35const TAG = 'NotificationDialog_Service '; 36const permission: Record<string, Resource> = { 37 'label': $r('app.string.group_label_notification'), 38 'icon': $r('app.media.ic_public_ring'), 39 'reason': $r('app.string.reason'), 40}; 41const bottomPopoverTypes = ['default', 'phone']; 42 43let storage = LocalStorage.getShared(); 44 45@Extend(Button) function customizeButton() { 46 .backgroundColor(Color.Transparent) 47 .fontColor($r('app.color.button_text')) 48 .fontSize( 49 getLimitFontSize( 50 Constants.TEXT_MIDDLE_FONT_SIZE, 51 getFontSizeScale(getContext(this) as common.UIAbilityContext, Constants.FONT_SCALE_MAX)) 52 ) 53 .fontWeight(FontWeight.Medium) 54 .height(Constants.BUTTON_HEIGHT) 55 .flexGrow(Constants.FLEX_GROW) 56 .width('50%') 57} 58 59@Entry(storage) 60@Component 61struct NotificationDialogPage { 62 @StorageLink('isUpdate') isUpdate: number = 0; 63 64 privacyDialogController: CustomDialogController = new CustomDialogController({ 65 builder: PermissionDialog({ isUpdate: $isUpdate }), 66 autoCancel: false, 67 alignment: DialogAlignment.Center, 68 customStyle: true, 69 maskColor: $r('app.color.mask_thin'), 70 onWillDismiss: (dismissDialogAction: DismissDialogAction) => { 71 console.info(TAG, `dialog onWillDismiss reason= : ${JSON.stringify(dismissDialogAction.reason)}`); 72 } 73 }); 74 75 build() {} 76 77 aboutToAppear() { 78 this.privacyDialogController.open(); 79 } 80 81 onPageShow() { 82 } 83} 84 85@CustomDialog 86struct PermissionDialog { 87 @State appName: string = ''; 88 @State naviHeight: number = 0; 89 @State isBottomPopover: boolean = true; 90 @StorageLink('clicked') clicked: boolean = false; 91 @Link @Watch('updateOnPageShow') isUpdate: number; 92 dialog?: EnableNotificationDialog; 93 session?: UIExtensionContentSession; 94 controller?: CustomDialogController; 95 @State titleContainerWidth: string | number = 'auto'; 96 @State maxHeight: string|number = '85%'; 97 98 build() { 99 Row(){ 100 Flex({ justifyContent: FlexAlign.Center, alignItems: this.isBottomPopover ? ItemAlign.End : ItemAlign.Center }) { 101 Column() { 102 Scroll() { 103 Column() { 104 Row() { 105 Image(permission.icon) 106 .width(Constants.DIALOG_ICON_WIDTH) 107 .height(Constants.DIALOG_ICON_HEIGHT) 108 .margin({ 109 top: Constants.DIALOG_ICON_MARGIN_TOP 110 }) 111 } 112 Row() { 113 Flex({ justifyContent: FlexAlign.Center }) { 114 Text($r('app.string.group_label_notification', this.appName)) 115 .fontSize($r('sys.float.ohos_id_text_size_headline8')) 116 .fontColor($r('app.color.text_primary')) 117 .fontWeight(FontWeight.Bold) 118 .minFontSize( 119 getLimitFontSize(Constants.TITLE_MIN_FONT_SIZE, 120 getFontSizeScale(getContext(this) as common.UIAbilityContext, Constants.FONT_SCALE_MAX)) 121 ) 122 .maxFontSize( 123 getLimitFontSize(sourceToVp($r('sys.float.ohos_id_text_size_headline8')), 124 getFontSizeScale(getContext(this) as common.UIAbilityContext, Constants.FONT_SCALE_MAX)) 125 ) 126 .heightAdaptivePolicy(TextHeightAdaptivePolicy.MAX_LINES_FIRST) 127 .maxLines(2) 128 .textOverflow({overflow: TextOverflow.Ellipsis}) 129 .width(this.titleContainerWidth) 130 .textAlign(TextAlign.Center) 131 } 132 .margin({ 133 top: Constants.DIALOG_REQ_MARGIN_TOP, 134 bottom:Constants.DIALOG_REQ_MARGIN_BUTTOM, 135 left: Constants.DIALOG_REQ_MARGIN_LEFT, 136 right: Constants.DIALOG_REQ_MARGIN_RIGHT 137 }) 138 .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => { 139 let containerWidth = newValue.width as number; 140 let options: MeasureOptions = { 141 textContent: $r('app.string.group_label_notification', this.appName), 142 fontSize: getLimitFontSize(sourceToVp($r('sys.float.ohos_id_text_size_headline8')), 143 getFontSizeScale(getContext(this) as common.UIAbilityContext, Constants.FONT_SCALE_MAX)), 144 fontWeight: FontWeight.Bold, 145 }; 146 this.titleContainerWidth = calContainerWidth(containerWidth, options, 147 Constants.CROSS_LINE_RATIO, this.getUIContext().getMeasureUtils()); 148 console.info(TAG, `onSizeChange titleContainerWidth: ${this.titleContainerWidth}`); 149 }) 150 } 151 Row() { 152 Flex({ justifyContent: FlexAlign.Center }) { 153 Text() { 154 Span(permission.reason) 155 } 156 .fontSize(Constants.DIALOG_DESP_FONT_SIZE) 157 .fontWeight(FontWeight.Regular) 158 .fontColor($r('app.color.text_primary')) 159 .lineHeight(Constants.DIALOG_DESP_LINE_HEIGHT) 160 } 161 .margin({ 162 top: Constants.DIALOG_DESP_MARGIN_TOP, 163 left: Constants.DIALOG_DESP_MARGIN_LEFT, 164 right: Constants.DIALOG_DESP_MARGIN_RIGHT, 165 bottom: Constants.DIALOG_DESP_MARGIN_BOTTOM 166 }) 167 } 168 Row() { 169 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 170 Button($r('app.string.BAN')) 171 .onClick(async (): Promise<void> => { 172 await this.enableNotification(false); 173 }) 174 .customizeButton() 175 Divider() 176 .color($r('app.color.comp_divider')) 177 .vertical(true) 178 .height(Constants.DIVIDER_HEIGHT) 179 .strokeWidth(Constants.DIVIDER_WIDTH) 180 .margin({left: Constants.BUTTON_LEFT, right: Constants.BUTTON_RIGHT}) 181 Button($r('app.string.ALLOW')) 182 .onClick(async (): Promise<void> => { 183 await this.enableNotification(true); 184 }) 185 .customizeButton() 186 } 187 .margin({ left: Constants.BUTTON_MARGIN_LEFT, right: Constants.BUTTON_MARGIN_RIGHT }) 188 } 189 } 190 } 191 } 192 .borderRadius(Constants.DIALOG_PRIVACY_BORDER_RADIUS) 193 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 194 .width(Constants.FULL_WIDTH) 195 .padding({ bottom: Constants.DIALOG_PADDING_BOTTOM }) 196 .clip(true) 197 } 198 } 199 .margin({ 200 left: this.isBottomPopover ? Constants.DIALOG_MARGIN_VERTICAL : Constants.DIALOG_MARGIN, 201 right: this.isBottomPopover ? Constants.DIALOG_MARGIN_VERTICAL : Constants.DIALOG_MARGIN, 202 bottom: this.isBottomPopover ? this.naviHeight : 0 203 }) 204 .constraintSize({ 205 maxHeight: this.maxHeight, 206 maxWidth: Constants.MAX_DIALOG_WIDTH 207 }) 208 } 209 210 async updateApplicationName(bundleName: string): Promise<void> { 211 console.info(TAG, `updateApplicationName bundleName: ${bundleName}`); 212 try { 213 let bundleFlags = bundleResourceManager.ResourceFlag.GET_RESOURCE_INFO_ALL; 214 let resourceInfo = bundleResourceManager.getBundleResourceInfo(bundleName, bundleFlags); 215 console.info(TAG, `applicationName name : ${JSON.stringify(resourceInfo.label)}`); 216 let appName = resourceInfo.label; 217 this.appName = titleTrim(appName); 218 console.info(TAG, `hap label: ${this.appName}`); 219 } catch (err) { 220 console.error(TAG, `applicationName error : ${err?.code}`); 221 } 222 } 223 224 updateAvoidWindow(): void { 225 let type = window.AvoidAreaType.TYPE_SYSTEM; 226 try { 227 this.dialog?.extensionWindow.on('avoidAreaChange', (data): void => { 228 if (data.type == window.AvoidAreaType.TYPE_SYSTEM) { 229 console.info(TAG, `avoidAreaChange: ${JSON.stringify(data)}`); 230 this.naviHeight = data.area.bottomRect.height; 231 } 232 }); 233 let avoidArea = this.dialog?.extensionWindow.getWindowAvoidArea(type); 234 if (avoidArea != undefined){ 235 console.info(TAG, `avoidArea: ${JSON.stringify(avoidArea)}`); 236 this.naviHeight = avoidArea.bottomRect.height; 237 } 238 } catch (err) { 239 console.error(TAG, `Failed to obtain the area. Cause: ${err?.code}`); 240 } 241 } 242 243 updateSubWindowSize(): void { 244 try { 245 this.dialog?.extensionWindow.on('windowSizeChange', (data):void => { 246 let windowRect = this.dialog?.extensionWindow.properties?.uiExtensionHostWindowProxyRect; 247 console.info(TAG, `windowSizeChange event, size = ${windowRect?.left}-${windowRect?.top}-${windowRect?.width}-${windowRect?.height}`); 248 if (!this.dialog?.initSubWindowSize && windowRect.width > 0 && windowRect.height > 0) { 249 console.info(TAG, `windowSizeChange first time update`); 250 this.dialog?.subWindow?.moveWindowToGlobal(windowRect?.left, windowRect?.top); 251 this.dialog?.subWindow?.resize(windowRect?.width, windowRect?.height); 252 this.dialog.initSubWindowSize = true; 253 } 254 this.updatemaxHeight(); 255 }); 256 } catch (err) { 257 console.error(TAG, `updateSubWindowSize error. Cause: ${err?.code}`); 258 } 259 } 260 261 updatemaxHeight(): void { 262 try { 263 let windowRect = this.dialog?.extensionWindow.properties?.uiExtensionHostWindowProxyRect; 264 let height = windowRect?.height; 265 let navigationArea = this.dialog?.extensionWindow.getWindowAvoidArea( 266 window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); 267 let navigationHeight = navigationArea?.bottomRect?.height; 268 let systemArea = this.dialog?.extensionWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); 269 let statusBarHeight = systemArea?.topRect?.height; 270 console.info(TAG, `windowHeight ${windowRect?.height} navigationHeight ${navigationHeight} statusBarHeight ${statusBarHeight} `); 271 if (height > 0 && (height - navigationHeight - statusBarHeight) > 0 ) { 272 this.maxHeight = px2vp((height - navigationHeight - statusBarHeight) * 0.9); 273 } 274 } catch (err) { 275 console.error(TAG, `updatemaxHeight error. Cause: ${err?.code}`); 276 } 277 } 278 279 async updateIsBottomPopover(): Promise<void> { 280 let dis = display.getDefaultDisplaySync(); 281 let isVertical = dis.width <= dis.height; 282 try { 283 if (display.isFoldable()) { 284 let foldStatus = display.getFoldStatus(); 285 if (foldStatus == display.FoldStatus.FOLD_STATUS_EXPANDED || 286 foldStatus == display.FoldStatus.FOLD_STATUS_HALF_FOLDED) { 287 this.isBottomPopover = false; 288 return; 289 } 290 } 291 } catch (err) { 292 console.error(TAG, 'Failed to get the device foldable status. Code: ${err?.code}'); 293 } 294 295 // read ccm configs 296 let isBottomPopoverTemp = false; 297 try { 298 let filePaths = await configPolicy.getCfgFiles(Constants.CCM_CONFIG_PATH); 299 for (let i = 0; i < filePaths.length; i++) { 300 let res = fs.accessSync(filePaths[i]); 301 if (res) { 302 let fileContent = fs.readTextSync(filePaths[i]); 303 let config: NotificationConfig = JSON.parse(fileContent); 304 if (config.notificationAuthorizationWindow != undefined) { 305 let windowConfig: NotificationAuthorizationWindow = config.notificationAuthorizationWindow; 306 if (windowConfig.isBottomPopover != undefined) { 307 isBottomPopoverTemp = windowConfig.isBottomPopover; 308 } 309 } 310 } 311 } 312 } catch (error) { 313 console.log(TAG, 'Failed get ccm files, Cause: ${err?.code}'); 314 } 315 this.isBottomPopover = isBottomPopoverTemp && isVertical; 316 } 317 318 async updateStatus(): Promise<void> { 319 let bundleNameObj = this.dialog?.want.parameters?.bundleName; 320 let bundleName = bundleNameObj ? bundleNameObj.toString() : ''; 321 await this.updateApplicationName(bundleName); 322 await this.updateIsBottomPopover(); 323 } 324 325 async updateOnPageShow(): Promise<void> { 326 if (this.isUpdate > 0) { 327 await this.updateStatus(); 328 } 329 } 330 331 async aboutToAppear(): Promise<void> { 332 this.dialog = storage.get('dialog') as EnableNotificationDialog; 333 this.session = storage.get('session') as UIExtensionContentSession; 334 this.updateAvoidWindow(); 335 this.updateSubWindowSize(); 336 this.updatemaxHeight(); 337 try { 338 await this.updateStatus(); 339 } catch (err) { 340 console.error(TAG, `aboutToAppear error : ${err?.code}`); 341 await this.dialog?.destroyException(); 342 await this.session?.terminateSelf(); 343 } 344 } 345 346 async registerFoldableCallback(): Promise<void> { 347 let callback: Callback<display.FoldDisplayMode> = async (data: display.FoldDisplayMode) => { 348 try { 349 let win = this.dialog.window; 350 let dis = display.getDefaultDisplaySync(); 351 await win.moveWindowTo(0, 0); 352 await win.resize(dis.width, dis.height); 353 await this.updateStatus(); 354 } catch (err) { 355 console.error(TAG, 'Failed to touch callback. Code: ${err?.code}'); 356 } 357 }; 358 try { 359 display.on('foldDisplayModeChange', callback); 360 } catch (err) { 361 console.error(TAG, 'Failed to register callback. Code: ${err?.code}'); 362 } 363 } 364 365 async aboutToDisappear(): Promise<void> { 366 console.info(TAG, `aboutToDisappear`); 367 this.session?.terminateSelf(); 368 } 369 370 async enableNotification(enabled: boolean): Promise<void> { 371 console.info(TAG, `NotificationDialog enableNotification: ${enabled}`); 372 try { 373 await this.dialog?.publishButtonClickedEvent(enabled); 374 this.clicked = true; 375 } catch (err) { 376 console.error(TAG, `NotificationDialog enable error, code is ${err?.code}`); 377 await this.dialog?.destroyException(); 378 } finally { 379 await this.dialog?.subWindow?.destroyWindow(); 380 this.session?.terminateSelf(); 381 } 382 } 383} 384 385interface NotificationConfig { 386 notificationAuthorizationWindow: NotificationAuthorizationWindow; 387} 388 389interface NotificationAuthorizationWindow { 390 isBottomPopover: boolean; 391} 392