1# Widget Host Development (for System Applications Only) 2 3## Widget Overview 4 5A widget is a set of UI components that display important information or operations specific to an application. It provides users with direct access to a desired application service, without the need to open the application first. 6 7A widget usually appears as a part of the UI of another application (which currently can only be a system application) and provides basic interactive features such as opening a UI page or sending a message. The widget host is responsible for displaying the service widget. 8 9- Before you get started, it would be helpful if you have a basic understanding of the following concepts: 10 11 - Widget provider: an atomic service that controls the widget content to display, how widget components are laid out, and how they interact with users. 12 13 - Widget host: an application that displays the widget content and controls the widget location. 14 15 - Widget Manager: a resident agent that provides widget management features such as periodic widget updates. 16 17  18 19## When to Use 20 21Carry out the following operations to develop the widget host based on the stage model: 22 23- Use **FormComponent**. 24- Use the APIs provided by the **formHost** module to delete or update widgets. 25 26## Using FormComponent 27 28**FormComponent** is a component used to display widgets. For details, see [FormComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-formcomponent-sys.md). 29 30> **NOTE** 31> 32> - This component is supported since API version 7. Updates will be marked with a superscript to indicate their earliest API version. 33> 34> - This component functions as the widget host. 35> 36> - To use this component, you must have the system signature. 37> 38> - The APIs provided by this component are system APIs. 39 40When a widget is added through **FormComponent**, the [onAddForm](../reference/apis-form-kit/js-apis-app-form-formExtensionAbility.md#onaddform) API in **FormExtensionAbility** of the widget provider is called. 41 42### Temporary and Normal Widgets 43 44The **temporary** field in **FormComponent** specifies whether a widget is a temporary or normal widget. The value **true** indicates a temporary widget, and **false** indicates a normal widget. 45 46- Normal widget: a widget persistently used by the widget host, for example, a widget added to the home screen. 47 48- Temporary widget: a widget temporarily used by the widget host, for example, the widget displayed when you swipe up on a widget application. 49 50Data of a temporary widget will be deleted on the Widget Manager if the widget framework is killed and restarted. The widget provider, however, is not notified of the deletion and still keeps the data. Therefore, the widget provider needs to clear the data of temporary widgets proactively if the data has been kept for a long period of time. If the widget host has converted a temporary widget into a normal one, the widget provider should change the widget data from temporary storage to persistent storage. Otherwise, the widget data may be deleted by mistake. 51 52## Using formHost APIs 53 54The **formHost** module provides a series of APIs for the widget host to update and delete widgets. For details, see the [API reference](../reference/apis-form-kit/js-apis-app-form-formHost-sys.md). 55 56## Example 57 58```ts 59//Index.ets 60import { HashMap, HashSet } from '@kit.ArkTS'; 61import { formHost, formInfo, formObserver } from '@kit.FormKit'; 62import { bundleMonitor } from '@kit.AbilityKit'; 63import { BusinessError } from '@kit.BasicServicesKit'; 64 65@Entry 66@Component 67struct formHostSample { 68 // Enumerated values of the widget size. 69 static FORM_DIMENSIONS_MAP = [ 70 '1*2', 71 '2*2', 72 '2*4', 73 '4*4', 74 '2*1', 75 '1*1', 76 '6*4', 77 ] 78 79 // Simulate the widget sizes. 80 static FORM_SIZE = [ 81 [120, 60], // 1*2 82 [120, 120], // 2*2 83 [240, 120], // 2*4 84 [240, 240], // 4*4 85 [60, 120], // 2*1 86 [60, 60], // 1*1 87 [240, 360], // 6*4 88 ] 89 90 @State message: Resource | string = $r('app.string.Host'); 91 formCardHashMap: HashMap<string, formInfo.FormInfo> = new HashMap(); 92 @State showFormPicker: boolean = false; 93 @State operation: Resource | string = $r('app.string.formOperation'); 94 @State index: number = 2; 95 @State space: number = 8; 96 @State arrowPosition: ArrowPosition = ArrowPosition.END; 97 formIds: HashSet<string> = new HashSet(); 98 currentFormKey: string = ''; 99 focusFormInfo: formInfo.FormInfo = { 100 bundleName: '', 101 moduleName: '', 102 abilityName: '', 103 name: '', 104 displayName: '', 105 displayNameId: 0, 106 description: '', 107 descriptionId: 0, 108 type: formInfo.FormType.eTS, 109 jsComponentName: '', 110 colorMode: formInfo.ColorMode.MODE_AUTO, 111 isDefault: false, 112 updateEnabled: false, 113 formVisibleNotify: true, 114 scheduledUpdateTime: '', 115 formConfigAbility: '', 116 updateDuration: 0, 117 defaultDimension: 6, 118 supportDimensions: [], 119 supportedShapes: [], 120 customizeData: {}, 121 isDynamic: false, 122 transparencyEnabled: false 123 } 124 formInfoRecord: TextCascadePickerRangeContent[] = []; 125 pickerBtnMsg: Resource | string = $r('app.string.formType'); 126 @State showForm: boolean = true; 127 @State selectFormId: string = '0'; 128 @State pickDialogIndex: number = 0; 129 130 aboutToAppear(): void { 131 try { 132 // Check whether the system is ready. 133 formHost.isSystemReady().then(() => { 134 console.log('formHost isSystemReady success'); 135 136 // Subscribe to events indicating that a widget becomes invisible and events indicating that a widget becomes visible. 137 let notifyInvisibleCallback = (data: formInfo.RunningFormInfo[]) => { 138 console.log(`form change invisibility, data: ${JSON.stringify(data)}`); 139 } 140 let notifyVisibleCallback = (data: formInfo.RunningFormInfo[]) => { 141 console.log(`form change visibility, data: ${JSON.stringify(data)}`); 142 } 143 formObserver.on('notifyInvisible', notifyInvisibleCallback); 144 formObserver.on('notifyVisible', notifyVisibleCallback); 145 146 // Subscribe to bundle installation events. 147 try { 148 bundleMonitor.on('add', (bundleChangeInfo) => { 149 console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); 150 this.getAllBundleFormsInfo(); 151 }) 152 } catch (errData) { 153 let message = (errData as BusinessError).message; 154 let errCode = (errData as BusinessError).code; 155 console.log(`errData is errCode:${errCode} message:${message}`); 156 } 157 // Subscribe to bundle update events. 158 try { 159 bundleMonitor.on('update', (bundleChangeInfo) => { 160 console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); 161 this.getAllBundleFormsInfo(); 162 }) 163 } catch (errData) { 164 let message = (errData as BusinessError).message; 165 let errCode = (errData as BusinessError).code; 166 console.log(`errData is errCode:${errCode} message:${message}`); 167 } 168 // Subscribe to bundle uninstall events. 169 try { 170 bundleMonitor.on('remove', (bundleChangeInfo) => { 171 console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); 172 this.getAllBundleFormsInfo(); 173 }) 174 } catch (errData) { 175 let message = (errData as BusinessError).message; 176 let errCode = (errData as BusinessError).code; 177 console.log(`errData is errCode:${errCode} message:${message}`); 178 } 179 }).catch((error: BusinessError) => { 180 console.error(`error, code: ${error.code}, message: ${error.message}`); 181 }); 182 } 183 catch (error) { 184 console.error(`catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 185 } 186 } 187 188 aboutToDisappear(): void { 189 // Delete all widgets. 190 this.formIds.forEach((id) => { 191 console.log('delete all form') 192 formHost.deleteForm(id); 193 }); 194 // Unsubscribe from bundle installation events. 195 try { 196 bundleMonitor.off('add'); 197 } catch (errData) { 198 let message = (errData as BusinessError).message; 199 let errCode = (errData as BusinessError).code; 200 console.log(`errData is errCode:${errCode} message:${message}`); 201 } 202 // Unsubscribe from bundle update events. 203 try { 204 bundleMonitor.off('update'); 205 } catch (errData) { 206 let message = (errData as BusinessError).message; 207 let errCode = (errData as BusinessError).code; 208 console.log(`errData is errCode:${errCode} message:${message}`); 209 } 210 // Unsubscribe from bundle uninstall events. 211 try { 212 bundleMonitor.off('remove'); 213 } catch (errData) { 214 let message = (errData as BusinessError).message; 215 let errCode = (errData as BusinessError).code; 216 console.log(`errData is errCode:${errCode} message:${message}`); 217 } 218 // Unsubscribe from events indicating that a widget becomes invisible and events indicating that a widget becomes visible. 219 formObserver.off('notifyInvisible'); 220 formObserver.off('notifyVisible'); 221 } 222 223 // Save the information of all widgets to formHapRecordMap. 224 getAllBundleFormsInfo() { 225 this.formCardHashMap.clear(); 226 this.showFormPicker = false; 227 let formHapRecordMap: HashMap<string, formInfo.FormInfo[]> = new HashMap(); 228 this.formInfoRecord = []; 229 formHost.getAllFormsInfo().then((formList: Array<formInfo.FormInfo>) => { 230 console.log('getALlFormsInfo size:' + formList.length) 231 for (let formItemInfo of formList) { 232 let formBundleName = formItemInfo.bundleName; 233 if (formHapRecordMap.hasKey(formBundleName)) { 234 formHapRecordMap.get(formBundleName).push(formItemInfo) 235 } else { 236 let formInfoList: formInfo.FormInfo[] = [formItemInfo]; 237 formHapRecordMap.set(formBundleName, formInfoList); 238 } 239 } 240 for (let formBundle of formHapRecordMap.keys()) { 241 let bundleFormInfo: TextCascadePickerRangeContent = { 242 text: formBundle, 243 children: [] 244 } 245 let bundleFormList: formInfo.FormInfo[] = formHapRecordMap.get(formBundle); 246 bundleFormList.forEach((formItemInfo) => { 247 let dimensionName = formHostSample.FORM_DIMENSIONS_MAP[formItemInfo.defaultDimension - 1]; 248 bundleFormInfo.children?.push({ text: formItemInfo.name + '#' + dimensionName }); 249 this.formCardHashMap.set(formBundle + "#" + formItemInfo.name + '#' + dimensionName, formItemInfo); 250 }) 251 this.formInfoRecord.push(bundleFormInfo); 252 } 253 this.formCardHashMap.forEach((formItem: formInfo.FormInfo) => { 254 console.info(`formCardHashmap: ${JSON.stringify(formItem)}`); 255 }) 256 this.showFormPicker = true; 257 }) 258 } 259 260 build() { 261 Column() { 262 Text(this.message) 263 .fontSize(30) 264 .fontWeight(FontWeight.Bold) 265 266 Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 }) 267 268 Row() { 269 // Click to query information about all widgets. 270 Button($r('app.string.inquiryForm')) 271 .onClick(() => { 272 this.getAllBundleFormsInfo(); 273 }) 274 275 // After the user clicks a button, a selection page is displayed. After the user clicks OK, the selected widget of the default size is added. 276 Button($r('app.string.selectAddForm')) 277 .enabled(this.showFormPicker) 278 .onClick(() => { 279 console.info("TextPickerDialog: show()") 280 TextPickerDialog.show({ 281 range: this.formInfoRecord, 282 selected: this.pickDialogIndex, 283 canLoop: false, 284 disappearTextStyle: { color: Color.Red, font: { size: 10, weight: FontWeight.Lighter } }, 285 textStyle: { color: Color.Black, font: { size: 12, weight: FontWeight.Normal } }, 286 selectedTextStyle: { color: Color.Blue, font: { size: 12, weight: FontWeight.Bolder } }, 287 onAccept: (result: TextPickerResult) => { 288 this.currentFormKey = result.value[0] + "#" + result.value[1]; 289 this.pickDialogIndex = result.index[0] 290 console.info(`TextPickerDialog onAccept: ${this.currentFormKey}, ${this.pickDialogIndex}`); 291 if (!this.formCardHashMap.hasKey(this.currentFormKey)) { 292 console.error(`invalid formItemInfo by form key`) 293 return; 294 } 295 this.showForm = true; 296 this.focusFormInfo = this.formCardHashMap.get(this.currentFormKey); 297 }, 298 onCancel: () => { 299 console.info("TextPickerDialog : onCancel()") 300 }, 301 onChange: (result: TextPickerResult) => { 302 this.pickerBtnMsg = result.value[0] + '#' + result.value[1]; 303 console.info("TextPickerDialog:onChange:" + this.pickerBtnMsg) 304 } 305 }) 306 }) 307 .margin({ left: 10 }) 308 } 309 .margin({ left: 10 }) 310 311 Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 }) 312 313 if(this.showForm){ 314 Text(this.pickerBtnMsg) 315 .margin({ top: 10, bottom: 10 }) 316 } 317 318 if (this.showForm) { 319 Text('formId: ' + this.selectFormId) 320 .margin({ top: 10, bottom: 10 }) 321 322 // FormComponent 323 FormComponent({ 324 id: Number.parseInt(this.selectFormId), 325 name: this.focusFormInfo.name, 326 bundle: this.focusFormInfo.bundleName, 327 ability: this.focusFormInfo.abilityName, 328 module: this.focusFormInfo.moduleName, 329 dimension: this.focusFormInfo.defaultDimension, 330 temporary: false, 331 }) 332 .size({ 333 width: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][0], 334 height: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][1], 335 }) 336 .borderColor(Color.Black) 337 .borderRadius(10) 338 .borderWidth(1) 339 .onAcquired((form: FormCallbackInfo) => { 340 console.log(`onAcquired: ${JSON.stringify(form)}`) 341 this.selectFormId = form.id.toString(); 342 this.formIds.add(this.selectFormId); 343 }) 344 .onRouter(() => { 345 console.log(`onRouter`) 346 }) 347 .onError((error) => { 348 console.error(`onError: ${JSON.stringify(error)}`) 349 this.showForm = false; 350 }) 351 .onUninstall((info: FormCallbackInfo) => { 352 this.showForm = false; 353 console.error(`onUninstall: ${JSON.stringify(info)}`) 354 this.formIds.remove(this.selectFormId); 355 }) 356 357 // A select list that displays some formHost APIs 358 Row() { 359 Select([{ value: $r('app.string.deleteForm') }, 360 { value: $r('app.string.updateForm') }, 361 { value: $r('app.string.visibleForms') }, 362 { value: $r('app.string.invisibleForms') }, 363 { value: $r('app.string.enableFormsUpdate') }, 364 { value: $r('app.string.disableFormsUpdate') }, 365 ]) 366 .selected(this.index) 367 .value(this.operation) 368 .font({ size: 16, weight: 500 }) 369 .fontColor('#182431') 370 .selectedOptionFont({ size: 16, weight: 400 }) 371 .optionFont({ size: 16, weight: 400 }) 372 .space(this.space) 373 .arrowPosition(this.arrowPosition) 374 .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 }) 375 .optionWidth(200) 376 .optionHeight(300) 377 .onSelect((index: number, text?: string | Resource) => { 378 console.info('Select:' + index) 379 this.index = index; 380 if (text) { 381 this.operation = text; 382 } 383 }) 384 385 // Operate the widget based on what selected in the select list. 386 Button($r('app.string.execute'), { 387 type: ButtonType.Capsule 388 }) 389 .fontSize(16) 390 .onClick(() => { 391 switch (this.index) { 392 case 0: 393 try { 394 formHost.deleteForm(this.selectFormId, (error: BusinessError) => { 395 if (error) { 396 console.error(`deleteForm error, code: ${error.code}, message: ${error.message}`); 397 } else { 398 console.log('formHost deleteForm success'); 399 } 400 }); 401 } catch (error) { 402 console.error(`deleteForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 403 } 404 this.showForm = false; 405 this.selectFormId = ''; 406 break; 407 case 1: 408 try { 409 formHost.requestForm(this.selectFormId, (error: BusinessError) => { 410 if (error) { 411 console.error(`requestForm error, code: ${error.code}, message: ${error.message}`); 412 } 413 }); 414 } catch (error) { 415 console.error(`requestForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 416 } 417 break; 418 case 2: 419 try { 420 formHost.notifyVisibleForms([this.selectFormId], (error: BusinessError) => { 421 if (error) { 422 console.error(`notifyVisibleForms error, code: ${error.code}, message: ${error.message}`); 423 } else { 424 console.info('notifyVisibleForms success'); 425 } 426 }); 427 } catch (error) { 428 console.error(`notifyVisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 429 } 430 break; 431 case 3: 432 try { 433 formHost.notifyInvisibleForms([this.selectFormId], (error: BusinessError) => { 434 if (error) { 435 console.error(`notifyInvisibleForms error, code: ${error.code}, message: ${error.message}`); 436 } else { 437 console.info('notifyInvisibleForms success'); 438 } 439 }); 440 } catch (error) { 441 console.error(`notifyInvisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 442 } 443 break; 444 case 4: 445 try { 446 formHost.enableFormsUpdate([this.selectFormId], (error: BusinessError) => { 447 if (error) { 448 console.error(`enableFormsUpdate error, code: ${error.code}, message: ${error.message}`); 449 } 450 }); 451 } catch (error) { 452 console.error(`enableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 453 } 454 break; 455 case 5: 456 try { 457 formHost.disableFormsUpdate([this.selectFormId], (error: BusinessError) => { 458 if (error) { 459 console.error(`disableFormsUpdate error, code: ${error.code}, message: ${error.message}`); 460 } else { 461 console.info('disableFormsUpdate success'); 462 } 463 }); 464 } catch (error) { 465 console.error(`disableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); 466 } 467 break; 468 } 469 }) 470 } 471 .margin({ 472 top: 20, 473 bottom: 10 474 }) 475 } 476 } 477 } 478} 479``` 480 481 482