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 */ 15import bundleManager from '@ohos.bundle.bundleManager'; 16import Constants from '../common/utils/constant'; 17import rpc from '@ohos.rpc'; 18import window from '@ohos.window'; 19import common from '@ohos.app.ability.common'; 20import { BusinessError } from '@ohos.base'; 21import { CustomContentDialog } from '@ohos.arkui.advanced.Dialog'; 22import { Log, getFontSizeScale } from '../common/utils/utils'; 23import { Param, WantInfo, DialogInfo } from '../common/model/typedef'; 24import { GlobalContext } from '../common/utils/globalContext'; 25import bundleResourceManager from '@ohos.bundle.bundleResourceManager'; 26import deviceInfo from '@ohos.deviceInfo'; 27 28const RESOURCE_TYPE: number = 10003; 29 30@Extend(Text) 31function titleText(isWearable: boolean) { 32 .fontWeight(FontWeight.Bold) 33 .fontColor(isWearable ? '#FFFFFF' : $r('sys.color.font_primary')) 34 .textAlign(TextAlign.Center) 35 .textOverflow({ overflow: TextOverflow.Ellipsis }) 36 .maxLines(Constants.SECURITY_HEADER_MAX_LINES) 37} 38 39@Extend(Button) 40function customizeButton() { 41 .padding({ 42 left: Constants.PADDING_16, 43 right: Constants.PADDING_16, 44 top: Constants.PADDING_10, 45 bottom: Constants.PADDING_10 46 }) 47 .width(Constants.CUSTOMIZE_BUTTON_WIDTH) 48 .margin({ bottom : Constants.MARGIN_12 }) 49} 50 51@Extend(Text) 52function customizeButtonText() { 53 .minFontSize(Constants.CUSTOMIZE_BUTTON_TEXT_MIN_SIZE) 54 .maxFontSize(Constants.CUSTOMIZE_BUTTON_TEXT_MAX_SIZE) 55 .textAlign(TextAlign.Center) 56 .fontSize(Constants.CUSTOMIZE_BUTTON_TEXT_MAX_SIZE) 57 .fontWeight(FontWeight.Medium) 58 .textOverflow({ overflow: TextOverflow.MARQUEE }) 59} 60 61@Builder 62function dialogTitle(index: number, securityParams: Array<Param>, isWearable: boolean) { 63 Column() { 64 if (getFontSizeScale()) { 65 Text(securityParams[index].label) 66 .titleText(isWearable) 67 .fontSize($r('sys.float.Title_S')) 68 } else { 69 Text(securityParams[index].label) 70 .titleText(isWearable) 71 .minFontSize(Constants.TEXT_MIDDLE_FONT_SIZE) 72 .maxFontSize($r('sys.float.Title_S')) 73 .heightAdaptivePolicy(TextHeightAdaptivePolicy.MAX_LINES_FIRST) 74 } 75 } 76 .constraintSize({ minHeight: Constants.HEADLINE_HEIGHT }) 77 .justifyContent(FlexAlign.Center) 78 .padding({ 79 top: Constants.DEFAULT_PADDING_TOP, 80 bottom: Constants.DEFAULT_PADDING_BOTTOM, 81 }) 82} 83 84@Builder 85function dialogContentsArea(params: DialogInfo) { 86 Column() { 87 Column() { 88 Row() { 89 SymbolGlyph($r('sys.symbol.security_shield')) 90 .fontSize(Constants.FONT_SIZE_28) 91 .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) 92 .fontColor(['#E5000000', '#0A59F7']) 93 } 94 .width(Constants.SECURITY_ICON_WIDTH) 95 .height(Constants.SECURITY_ICON_HEIGHT) 96 .justifyContent(FlexAlign.Center) 97 .pixelRound({ start: PixelRoundCalcPolicy.NO_FORCE_ROUND, end: PixelRoundCalcPolicy.NO_FORCE_ROUND }) 98 .border({ 99 width: Constants.BORDER_WIDTH_1, 100 color: $r('app.color.icon_border'), 101 radius: Constants.SECURITY_ICON_WIDTH * 14 / 54 102 }) 103 if (params.index === 1) { 104 Image(params.securityParams[params.index].icon) 105 .width(Constants.IMAGE_LENGTH_20) 106 .height(Constants.IMAGE_LENGTH_20) 107 .syncLoad(true) 108 .position({ x: Constants.IMAGE_POSITION_28, y: Constants.IMAGE_POSITION_28 }) 109 .border({ 110 width: Constants.BORDER_WIDTH_1, 111 color: $r('app.color.icon_border'), 112 radius: Constants.IMAGE_LENGTH_20 * 14 / 54 113 }) 114 } else { 115 Row() { 116 SymbolGlyph($r('sys.symbol.local_fill')) 117 .fontSize(Constants.FONT_SIZE_12) 118 .fontColor([Color.White]) 119 } 120 .width(Constants.IMAGE_LENGTH_20) 121 .height(Constants.IMAGE_LENGTH_20) 122 .justifyContent(FlexAlign.Center) 123 .backgroundColor($r('app.color.local_background_color')) 124 .position({ x: Constants.IMAGE_POSITION_28, y: Constants.IMAGE_POSITION_28 }) 125 .border({ 126 width: Constants.BORDER_WIDTH_1, 127 color: $r('app.color.icon_border'), 128 radius: Constants.IMAGE_LENGTH_20 * 14 / 54 129 }) 130 } 131 } 132 .pixelRound({ start: PixelRoundCalcPolicy.NO_FORCE_ROUND, end: PixelRoundCalcPolicy.NO_FORCE_ROUND }) 133 .backgroundColor($r('app.color.icon_bg')) 134 .borderRadius(Constants.SECURITY_ICON_WIDTH * 14 / 54) 135 .margin(params.isWearable ? { top: Constants.MARGIN_12 } : {}) 136 Column() { // content 137 dialogTitle(params.index, params.securityParams, params.isWearable); 138 Text($r( 139 params.securityParams[params.index].description, 140 params.appName, 141 params.appIndex === 0 ? '' : String(params.appIndex) 142 )) 143 .textAlign(params.isWearable ? TextAlign.Center : TextAlign.Start) 144 .fontColor($r('sys.color.font_primary')) 145 .fontSize($r('sys.float.Body_L')) 146 .maxFontScale(Constants.DIALOG_TEXT_MAX_SCALE) 147 .margin(params.isWearable ? { 148 left: Constants.MARGIN_26, 149 right: Constants.MARGIN_26, 150 bottom: Constants.MARGIN_12 151 } : {}) 152 } 153 } 154 .clip(true) 155} 156 157@CustomDialog 158struct CustomDialogWearable { 159 cancel?: () => void; 160 confirm?: () => void; 161 dialogControllerForWearable: CustomDialogController; 162 @Link index: number; 163 @Link appName: ResourceStr; 164 @Link securityParams: Array<Param>; 165 @Link isWearable: boolean; 166 @Link appIndex: number; 167 168 build() { 169 Scroll() { 170 Column() { 171 dialogContentsArea({ 172 index: this.index, 173 securityParams: this.securityParams, 174 appName: this.appName, 175 isWearable: this.isWearable, 176 appIndex: this.appIndex 177 }) 178 Button({ type: ButtonType.Capsule, stateEffect: true }) { 179 Text($r('app.string.allow')) 180 .customizeButtonText() 181 .fontColor('#FFFFFF') 182 } 183 .customizeButton() 184 .backgroundColor('#1F71FF') 185 .onClick(() => { 186 Log.info('Allow click start.'); 187 this.dialogControllerForWearable?.close(); 188 if (this.confirm) { 189 this.confirm(); 190 } 191 }) 192 Button({ type: ButtonType.Capsule, stateEffect: true }) { 193 Text($r('app.string.cancel')) 194 .customizeButtonText() 195 .fontColor('#5EA1FF') 196 } 197 .customizeButton() 198 .backgroundColor('#405EA1FF') 199 .onClick(() => { 200 Log.info('cancel click start.'); 201 this.dialogControllerForWearable?.close(); 202 if (this.cancel) { 203 this.cancel(); 204 } 205 }) 206 } 207 } 208 .edgeEffect(EdgeEffect.Spring) 209 .backgroundColor(Color.Black) 210 .width(Constants.FULL_WIDTH) 211 .height(Constants.FULL_HEIGHT) 212 } 213} 214 215@Entry({ useSharedStorage: true }) 216@Component 217struct SecurityDialog { 218 private context = this.getUIContext().getHostContext() as common.ServiceExtensionContext; 219 @LocalStorageLink('want') want: WantInfo = new WantInfo([]); 220 @LocalStorageLink('win') win: window.Window = {} as window.Window; 221 @State appName: ResourceStr = 'Application'; 222 @State appIndex: number = 0; 223 @State index: number = 0; 224 @State scrollBarWidth: number = Constants.SCROLL_BAR_WIDTH_DEFAULT; 225 @State isWearable: boolean = false; 226 227 @State securityParams : Array<Param> = [ 228 new Param( 229 $r('app.media.ic_location'), $r('app.string.SecurityTitle_location'), 'app.string.SecurityText_location' 230 ), 231 new Param( 232 $r('app.media.rawfile'), $r('app.string.SecurityTitle_mediaFiles'), 'app.string.SecurityText_mediaFiles' 233 ) 234 ] 235 236 dialogControllerForWearable: CustomDialogController | null = new CustomDialogController({ 237 builder: CustomDialogWearable({ 238 cancel: () => { 239 this.loadCancel(); 240 }, 241 confirm: () => { 242 this.onAccept(); 243 }, 244 index: $index, 245 appName: $appName, 246 isWearable: $isWearable, 247 securityParams: $securityParams, 248 appIndex: $appIndex 249 }), 250 customStyle: true, 251 cancel: () => { 252 this.loadCancel(); 253 }, 254 }) 255 256 dialogController: CustomDialogController | null = new CustomDialogController({ 257 builder: CustomContentDialog({ 258 contentBuilder: () => { 259 this.buildContent(); 260 }, 261 contentAreaPadding: { right: 0 }, 262 buttons: [ 263 { 264 value: $r('app.string.cancel'), 265 buttonStyle: ButtonStyleMode.TEXTUAL, 266 action: () => { 267 this.dialogController?.close(); 268 this.loadCancel(); 269 } 270 }, 271 { 272 value: $r('app.string.allow'), 273 buttonStyle: ButtonStyleMode.TEXTUAL, 274 action: () => { 275 this.dialogController?.close(); 276 this.destruction(); 277 } 278 } 279 ], 280 }), 281 autoCancel: false, 282 cancel: () => { 283 this.loadCancel(); 284 }, 285 }); 286 287 @Builder 288 buildContent(): void { 289 Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { 290 Scroll() { 291 dialogContentsArea({ 292 index: this.index, 293 securityParams: this.securityParams, 294 appName: this.appName, 295 isWearable: this.isWearable, 296 appIndex: this.appIndex 297 }) 298 } 299 .padding({ left: Constants.PADDING_24, right: Constants.PADDING_24 }) 300 .margin({ top: Constants.MARGIN_24 }) 301 .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: false }) 302 .scrollBarWidth(this.scrollBarWidth) 303 .onScrollStart(() => { 304 this.scrollBarWidth = Constants.SCROLL_BAR_WIDTH_ACTIVE; 305 }) 306 .onScrollStop(() => { 307 this.scrollBarWidth = Constants.SCROLL_BAR_WIDTH_DEFAULT; 308 }) 309 } 310 } 311 312 build() {} 313 314 aboutToAppear() { 315 Log.info('onAboutToAppear.'); 316 this.GetAppName(); 317 this.index = this.want.parameters['ohos.user.security.type']; 318 if (deviceInfo.deviceType === 'wearable') { 319 this.dialogControllerForWearable?.open(); 320 this.isWearable = true; 321 } else { 322 this.dialogController?.open(); 323 this.isWearable = false; 324 } 325 } 326 327 aboutToDisappear() { 328 Log.info('aboutToDisappear.'); 329 this.dialogController = null; 330 this.dialogControllerForWearable = null; 331 } 332 333 loadCancel() { 334 Log.info('Callback when the first button is clicked.'); 335 this.win?.destroyWindow(); 336 let dialogSet: Set<String> = GlobalContext.load('dialogSet'); 337 let callerToken: number = this.want.parameters['ohos.caller.uid']; 338 let windId: number = this.want.parameters['ohos.ability.params.windowId']; 339 let token: String = String(callerToken) + '_' + String(windId); 340 dialogSet.delete(token); 341 GlobalContext.store('dialogSet', dialogSet); 342 if (dialogSet.size === 0) { 343 this.context.terminateSelf(); 344 } 345 } 346 347 onAccept() { 348 Log.info('Callback when the second button is clicked.'); 349 this.destruction(); 350 } 351 352 GetAppName() { 353 let uid: number = this.want.parameters['ohos.caller.uid']; 354 try { 355 bundleManager.getAppCloneIdentity(uid).then(cloneInfo => { 356 Log.info(`getAppCloneIdentity: ${JSON.stringify(cloneInfo)}.`); 357 this.appIndex = cloneInfo.appIndex; 358 this.getSelfName(cloneInfo); 359 }).catch((err: BusinessError) => { 360 Log.error(`getAppCloneIdentity failed: ${JSON.stringify(err)}.`); 361 }) 362 } catch (err) { 363 Log.error(`get appName failed: ${JSON.stringify(err)}.`); 364 }; 365 } 366 367 getSelfName(cloneInfo: bundleManager.AppCloneIdentity) { 368 try { 369 bundleManager.getApplicationInfo(cloneInfo.bundleName, bundleManager.ApplicationFlag.GET_APPLICATION_INFO_DEFAULT) 370 .then(data => { 371 data.labelResource.params = []; 372 data.labelResource.type = RESOURCE_TYPE; 373 this.appName = data.labelResource; 374 }).catch((error: BusinessError) => { 375 Log.error(`getApplicationInfo failed. err is ${JSON.stringify(error)}.`); 376 }); 377 } catch (err) { 378 Log.error(`getSelfName failed. err is ${JSON.stringify(err)}.`); 379 }; 380 } 381 382 getCloneName(cloneInfo: bundleManager.AppCloneIdentity) { 383 try { 384 let resourceFlag = bundleResourceManager.ResourceFlag.GET_RESOURCE_INFO_ALL; 385 let resourceInfo = 386 bundleResourceManager.getBundleResourceInfo(cloneInfo.bundleName, resourceFlag, cloneInfo.appIndex); 387 this.appName = resourceInfo?.label; 388 } catch (err) { 389 Log.error(`getCloneName failed. err is ${JSON.stringify(err)}.`); 390 }; 391 } 392 393 destruction() { 394 let option = new rpc.MessageOption(); 395 let data = new rpc.MessageSequence(); 396 let reply = new rpc.MessageSequence(); 397 Promise.all([ 398 data.writeInterfaceToken(Constants.SEC_COMP_DIALOG_CALLBACK), 399 data.writeInt(0) 400 ]).then(() => { 401 let proxy = this.want.parameters['ohos.ability.params.callback'].value as rpc.RemoteObject; 402 if (proxy != undefined) { 403 proxy.sendMessageRequest(Constants.RESULT_CODE, data, reply, option); 404 } 405 }).catch(() => { 406 Log.error('write result failed!'); 407 }).finally(() => { 408 data.reclaim(); 409 reply.reclaim(); 410 this.loadCancel(); 411 }) 412 } 413} 414