1/* 2 * Copyright (c) 2022 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 bundle from '@ohos.bundle'; 17import router from '@ohos.router'; 18import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; 19import privacyManager from '@ohos.privacyManager' 20import { backBar } from "../common/components/backBar"; 21import Constants from '../common/utils/constant'; 22import { noNeedDisplayApp, userGrantPermissions, permissionGroupIds } from "../common/model/permissionGroup"; 23import { getPermissionGroup } from '../common/utils/utils' 24 25var TAG = 'PermissionManager_MainAbility:' 26 27@Extend(Image) function customizeImage(width: number, height: number) { 28 .objectFit(ImageFit.Contain) 29 .width(width) 30 .height(height) 31}; 32 33class StringObj { 34 visits: string 35 recent_visit: string 36 morning: string 37 afternoon: string 38 constructor(visits: string, recent_visit: string, morning: string, afternoon: string) { 39 this.visits = visits 40 this.recent_visit = recent_visit 41 this.morning = morning 42 this.afternoon = afternoon 43 } 44} 45 46@Entry 47@Component 48struct permissionRecordPage { 49 @State groups: any[] = [] 50 @State applicationInfos: any[] = [] 51 @State permissionApplications: any[] = [] 52 @State permissionIndex: number = -1 53 @State applicationIndex: number = -1 54 @State strings: StringObj = new StringObj('', '', '', '') 55 @State currentIndex: number = 0 56 @Builder TabBuilder(index: number) { 57 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 58 Text(index ? $r('app.string.application') : $r('app.string.authority')) 59 .fontColor(this.currentIndex == index ? $r('app.color.button_color') : $r('app.color.label_color')) 60 .fontWeight(this.currentIndex == index ? FontWeight.Bold : FontWeight.Regular) 61 .lineHeight(Constants.TEXT_LINE_HEIGHT) 62 if(this.currentIndex == index) { 63 Row().width(Constants.FULL_WIDTH).height(Constants.TAB_DECORATION_HEIGHT) 64 .backgroundColor($r('app.color.button_color')) 65 .position({ y: Constants.TAB_DECORATION_POSITION_Y }) 66 } 67 }.height(Constants.TAB_HEIGHT) 68 } 69 70 @Builder ListItemLayout(item, index, dimension) { 71 ListItem() { 72 Column() { 73 Column() { 74 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 75 Row() { 76 if(dimension) { 77 Image(item.icon) 78 .customizeImage(Constants.MANAGEMENT_IMAGE_WIDTH, Constants.MANAGEMENT_IMAGE_HEIGHT) 79 .margin({ right: Constants.MANAGEMENT_IMAGE_MARGIN_RIGHT_RECORD, left: Constants.MANAGEMENT_IMAGE_MARGIN_LEFT }) 80 }else { 81 Image(item.icon) 82 .customizeImage(Constants.APPLICATION_IMAGE_WIDTH, Constants.APPLICATION_IMAGE_HEIGHT) 83 .margin({ right: Constants.MANAGEMENT_IMAGE_MARGIN_RIGHT }) 84 } 85 Column() { 86 Text(item.groupName) 87 .fontSize(Constants.TEXT_MIDDLE_FONT_SIZE) 88 .fontWeight(FontWeight.Medium) 89 .fontColor($r('app.color.label_color')) 90 .lineHeight(Constants.TEXT_LINE_HEIGHT) 91 .margin({ bottom: Constants.TERTIARY_LABEL_MARGIN_BOTTOM }) 92 if(dimension) { 93 Text(this.strings.visits + item.sum) 94 .fontSize(Constants.TEXT_SMAL_FONT_SIZE) 95 .fontColor($r('app.color.label_color_light')) 96 .lineHeight(Constants.TEXT_SMALL_LINE_HEIGHT) 97 }else { 98 Row() { 99 if (item.permissions) { 100 ForEach(item.permissions, permission => { 101 Image(permission.icon) 102 .customizeImage(Constants.IMAGE_WIDTH_RECORD_APPLICATION, Constants.IMAGE_HEIGHT_RECORD_APPLICATION) 103 .margin({ right: Constants.APPLICATION_TEXT_MARGIN_RIGHT }) 104 }) 105 } 106 } 107 } 108 }.flexGrow(Constants.FLEX_GROW) 109 .alignItems(HorizontalAlign.Start) 110 if(dimension) { 111 if(index == this.permissionIndex) { 112 Image($r('app.media.xiangshangjiantou')) 113 .customizeImage(Constants.IMAGE_WIDTH_RECORD, Constants.IMAGE_HEIGHT_RECORD) 114 }else { 115 Image($r('app.media.xiangxiajiantou')) 116 .customizeImage(Constants.IMAGE_WIDTH_RECORD, Constants.IMAGE_HEIGHT_RECORD) 117 } 118 }else { 119 if(index == this.applicationIndex) { 120 Image($r('app.media.xiangshangjiantou')) 121 .customizeImage(Constants.IMAGE_WIDTH_RECORD, Constants.IMAGE_HEIGHT_RECORD) 122 }else { 123 Image($r('app.media.xiangxiajiantou')) 124 .customizeImage(Constants.IMAGE_WIDTH_RECORD, Constants.IMAGE_HEIGHT_RECORD) 125 } 126 } 127 } 128 .width(Constants.FULL_WIDTH) 129 .height(dimension ? Constants.LISTITEM_HEIGHT_PERMISSION : Constants.LISTITEM_HEIGHT_APPLICATION) 130 } 131 }.onClick(() => { 132 dimension ? 133 (this.permissionIndex = this.permissionIndex == index ? -1 : index) : 134 (this.applicationIndex = this.applicationIndex == index ? -1 : index) 135 if(dimension) { 136 this.permissionApplications = this.applicationInfos.filter(appInfo => { 137 return appInfo.groupNames.includes(item.groupName) 138 }) 139 } 140 }) 141 if(dimension && (index == this.permissionIndex)) { 142 List() { 143 ForEach(this.permissionApplications, (permissionApplication) => { 144 ListItem() { 145 Row() { 146 Image(permissionApplication.icon) 147 .customizeImage(Constants.APPLICATION_IMAGE_WIDTH, Constants.APPLICATION_IMAGE_HEIGHT) 148 .margin({ right: Constants.MANAGEMENT_IMAGE_MARGIN_RIGHT }) 149 Column() { 150 Row().width(Constants.FULL_WIDTH).height(Constants.TEXT_DECORATION_HEIGHT) 151 .backgroundColor($r("app.color.label_color_lightest")) 152 .margin({ bottom: Constants.LISTITEM_MARGIN_BOTTOM_PERMISSION }) 153 Text(permissionApplication.groupName) 154 .fontSize(Constants.TEXT_MIDDLE_FONT_SIZE) 155 .fontWeight(FontWeight.Medium) 156 .fontColor($r('app.color.label_color')) 157 .lineHeight(Constants.TEXT_LINE_HEIGHT) 158 .margin({ bottom: Constants.TERTIARY_LABEL_MARGIN_BOTTOM }) 159 Text(this.strings.visits + this.getAppRecords(permissionApplication, item.groupName, true) + 160 this.strings.recent_visit + this.getTime(this.getAppRecords(permissionApplication, item.groupName, false))) 161 .fontSize(Constants.TEXT_SMAL_FONT_SIZE) 162 .fontColor($r('app.color.label_color_light')) 163 .lineHeight(Constants.TEXT_SMALL_LINE_HEIGHT) 164 }.alignItems(HorizontalAlign.Start) 165 .height(Constants.FULL_HEIGHT) 166 } 167 }.height(Constants.LISTITEM_HEIGHT_APPLICATION) 168 .onClick(() => { 169 router.pushUrl({ 170 url: 'pages/application-secondary', 171 params: { routerData: { 172 'bundleName': permissionApplication.name, 173 'tokenId': permissionApplication.accessTokenId, 174 'iconId': permissionApplication.icon, 175 'labelId': permissionApplication.groupName, 176 'permissions': permissionApplication.reqUserPermissions, 177 'groupId': permissionApplication.groupIds 178 } } 179 }); 180 }) 181 }) 182 } 183 } 184 if(!dimension && (index == this.applicationIndex)) { 185 List() { 186 ForEach(item.permissions, (permission) => { 187 ListItem() { 188 Row() { 189 Image(permission.icon) 190 .customizeImage(Constants.MANAGEMENT_IMAGE_WIDTH, Constants.MANAGEMENT_IMAGE_HEIGHT) 191 .margin({ right: Constants.MANAGEMENT_IMAGE_MARGIN_RIGHT_RECORD, left: Constants.MANAGEMENT_IMAGE_MARGIN_LEFT }) 192 Column() { 193 Row().width(Constants.FULL_WIDTH).height(Constants.TEXT_DECORATION_HEIGHT) 194 .backgroundColor($r("app.color.label_color_lightest")) 195 .margin({ bottom: Constants.LISTITEM_MARGIN_BOTTOM_APPLICATION }) 196 Text(permission.groupName) 197 .fontSize(Constants.TEXT_MIDDLE_FONT_SIZE) 198 .fontWeight(FontWeight.Medium) 199 .fontColor($r('app.color.label_color')) 200 .lineHeight(Constants.TEXT_LINE_HEIGHT) 201 .margin({ bottom: Constants.TERTIARY_LABEL_MARGIN_BOTTOM }) 202 Text(this.strings.visits + permission['count' + item.accessTokenId] 203 + this.strings.recent_visit + this.getTime(permission['lastTime' + item.accessTokenId])) 204 .fontSize(Constants.TEXT_SMAL_FONT_SIZE) 205 .fontColor($r('app.color.label_color_light')) 206 .lineHeight(Constants.TEXT_SMALL_LINE_HEIGHT) 207 }.alignItems(HorizontalAlign.Start) 208 .height(Constants.FULL_HEIGHT) 209 } 210 }.height(Constants.LISTITEM_HEIGHT_PERMISSION) 211 .onClick(() => { 212 router.pushUrl({ 213 url: 'pages/application-secondary', 214 params: { routerData: { 215 'bundleName': item.name, 216 'tokenId': item.accessTokenId, 217 'iconId': item.icon, 218 'labelId': item.groupName, 219 'permissions': item.reqUserPermissions, 220 'groupId': item.groupIds 221 } } 222 }); 223 }) 224 }) 225 } 226 } 227 } 228 }.padding({ left: Constants.DEFAULT_PADDING_START, right: Constants.DEFAULT_PADDING_END, 229 top: Constants.LIST_PADDING_TOP, bottom: Constants.LIST_PADDING_BOTTOM }) 230 .margin({ bottom: Constants.LISTITEM_MARGIN_BOTTOM }) 231 .backgroundColor($r('app.color.default_background_color')) 232 .borderRadius(Constants.BORDER_RADIUS) 233 } 234 235 build() { 236 GridContainer({ gutter: Constants.GUTTER, margin: Constants.GRID_MARGIN }) { 237 Row() { 238 Row() 239 .useSizeType({ 240 xs: { span: Constants.LEFT_XS_SPAN, offset: Constants.LEFT_XS_OFFSET }, 241 sm: { span: Constants.LEFT_SM_SPAN, offset: Constants.LEFT_SM_OFFSET }, 242 md: { span: Constants.LEFT_MD_SPAN, offset: Constants.LEFT_MD_OFFSET }, 243 lg: { span: Constants.LEFT_LG_SPAN, offset: Constants.LEFT_LG_OFFSET } 244 }) 245 .height(Constants.FULL_HEIGHT) 246 Row() { 247 Column() { 248 Row() { 249 backBar( { title: JSON.stringify($r('app.string.permission_access_record')), recordable: false }) 250 } 251 Row() { 252 Column() { 253 Column() { 254 Flex({ justifyContent: FlexAlign.Start }) { 255 Text($r('app.string.record_time_limit')) 256 .margin({ left: Constants.BACKBAR_IMAGE_MARGIN_LEFT }) 257 .fontSize(Constants.TEXT_SMAL_FONT_SIZE) 258 .fontColor($r('app.color.label_color_light')) 259 .lineHeight(Constants.SUBTITLE_LINE_HEIGHT) 260 }.constraintSize({ minHeight: Constants.SUBTITLE_MIN_HEIGHT }) 261 .padding({ top: Constants.SUBTITLE_PADDING_TOP, bottom: Constants.SUBTITLE_PADDING_BOTTOM }) 262 if(this.groups.length) { 263 Stack() { 264 Tabs() { 265 TabContent() { 266 Row() { 267 Column() { 268 Scroll() { 269 Row() { 270 List() { 271 ForEach(this.groups, (item, index) => { 272 this.ListItemLayout(item, index, Constants.PERMISSION) 273 }, item => item.toString()) 274 }.padding({ top: Constants.LIST_PADDING_TOP, bottom: Constants.LIST_PADDING_BOTTOM }) 275 }.padding({ 276 left: Constants.MANAGEMENT_ROW_PADDING_LEFT, 277 right: Constants.MANAGEMENT_ROW_PADDING_RIGHT, 278 top: Constants.MANAGEMENT_ROW_PADDING_TOP 279 }) 280 }.scrollBar(BarState.Off) 281 }.width(Constants.FULL_WIDTH) 282 } 283 }.tabBar(this.TabBuilder(0)) 284 TabContent() { 285 Row() { 286 Column() { 287 Scroll() { 288 Row() { 289 List() { 290 ForEach(this.applicationInfos, (item, index) => { 291 this.ListItemLayout(item, index, Constants.APPLICATION) 292 }, item => item.toString()) 293 }.padding({ top: Constants.LIST_PADDING_TOP, bottom: Constants.LIST_PADDING_BOTTOM }) 294 }.padding({ 295 left: Constants.MANAGEMENT_ROW_PADDING_LEFT, 296 right: Constants.MANAGEMENT_ROW_PADDING_RIGHT, 297 top: Constants.MANAGEMENT_ROW_PADDING_TOP 298 }) 299 }.scrollBar(BarState.Off) 300 }.width(Constants.FULL_WIDTH) 301 } 302 }.tabBar(this.TabBuilder(1)) 303 } 304 .barWidth(Constants.BAR_WIDTH) 305 .barMode(BarMode.Fixed) 306 .onChange((index) => { 307 this.currentIndex = index 308 }) 309 }.height(Constants.FULL_HEIGHT) 310 }else { 311 Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, direction: FlexDirection.Column }) { 312 Image($r('app.media.noRecord')) 313 .customizeImage(Constants.NORECORD_IMAGE_WIDTH, Constants.NORECORD_IMAGE_HEIGHT) 314 .margin({ left: Constants.NORECORD_IMAGE_MARGIN_LEFT }) 315 Text($r('app.string.no_record')).margin({ top: Constants.DIALOG_REQ_MARGIN_TOP }) 316 .fontSize(Constants.TEXT_SMAL_FONT_SIZE) 317 .fontColor($r('app.color.label_color_light')) 318 }.width(Constants.FULL_WIDTH).height(Constants.FULL_HEIGHT) 319 .padding({ bottom: Constants.RECORD_PADDING_BOTTOM }) 320 } 321 } 322 } 323 } 324 .layoutWeight(Constants.LAYOUT_WEIGHT) 325 } 326 } 327 .useSizeType({ 328 xs: { span: Constants.MIDDLE_XS_SPAN, offset: Constants.MIDDLE_XS_OFFSET }, 329 sm: { span: Constants.MIDDLE_SM_SPAN, offset: Constants.MIDDLE_SM_OFFSET }, 330 md: { span: Constants.MIDDLE_MD_SPAN, offset: Constants.MIDDLE_MD_OFFSET }, 331 lg: { span: Constants.MIDDLE_LG_SPAN, offset: Constants.MIDDLE_LG_OFFSET } 332 }) 333 .height(Constants.FULL_HEIGHT) 334 Row() 335 .useSizeType({ 336 xs: { span: Constants.RIGHT_XS_SPAN, offset: Constants.RIGHT_XS_OFFSET }, 337 sm: { span: Constants.RIGHT_SM_SPAN, offset: Constants.RIGHT_SM_OFFSET }, 338 md: { span: Constants.RIGHT_MD_SPAN, offset: Constants.RIGHT_MD_OFFSET }, 339 lg: { span: Constants.RIGHT_LG_SPAN, offset: Constants.RIGHT_LG_OFFSET } 340 }) 341 .height(Constants.FULL_HEIGHT) 342 } 343 .height(Constants.FULL_HEIGHT) 344 .width(Constants.FULL_WIDTH) 345 .backgroundColor($r("sys.color.ohos_id_color_sub_background")) 346 } 347 } 348 349 /** 350 * Get time 351 * @param {Number} The time stamp 352 */ 353 getTime(time, format='MM月DD日 NNHH:mm') { 354 if(this.strings.morning == 'am') { format = 'HH:mm NN MM/DD' } 355 let date = new Date(time * 1000) 356 let config = { 357 MM: date.getMonth() + 1, 358 DD: date.getDate(), 359 NN: date.getHours() >= 12 ? this.strings.afternoon : this.strings.morning, 360 HH: date.getHours() >= 12 ? date.getHours() - 12 : date.getHours(), 361 mm: date.getMinutes() > 10 ? date.getMinutes() : '0' + date.getMinutes(), 362 } 363 364 for(const key in config){ 365 format = format.replace(key,config[key]) 366 } 367 return format 368 } 369 370 /** 371 * Get application record info 372 * @param {Object} application info 373 * @param {String} groupName 374 * @param {Boolean} true: count, false: lastTime 375 */ 376 getAppRecords(appInfo, groupName, option) { 377 var record = appInfo.permissions.filter(permission => { 378 return permission.groupName == groupName 379 }) 380 return option ? record[0]['count' + appInfo.accessTokenId] : record[0]['lastTime' + appInfo.accessTokenId] 381 } 382 383 getStrings() { 384 globalThis.context.resourceManager.getString($r("app.string.visits").id, (err, val) => { 385 this.strings.visits = val 386 }) 387 globalThis.context.resourceManager.getString($r("app.string.recent_visit").id, (err, val) => { 388 this.strings.recent_visit = val 389 }) 390 globalThis.context.resourceManager.getString($r("app.string.morning").id, (err, val) => { 391 this.strings.morning = val 392 }) 393 globalThis.context.resourceManager.getString($r("app.string.afternoon").id, (err, val) => { 394 this.strings.afternoon = val 395 }) 396 } 397 398 getInfo(record, sortFlag) { 399 bundle.getBundleInfo(record.bundleName, Constants.PARMETER_BUNDLE_FLAG).then(async info => { 400 var reqUserPermissions: string[] = []; 401 var reqUserRecords: any[] = []; 402 var permissionGroups: any[] = []; 403 var acManager = abilityAccessCtrl.createAtManager() 404 var appInfo: any = {} 405 for (let j = 0; j < record.permissionRecords.length; j++) { 406 var permission = record.permissionRecords[j].permissionName; 407 try { 408 var flag = await acManager.getPermissionFlags(info.appInfo.accessTokenId, permission) 409 if(flag == Constants.PRE_AUTHORIZATION_NOT_MODIFIED) { 410 continue 411 } 412 } 413 catch(err) { 414 console.log(TAG + 'getPermissionFlags error: ' + JSON.stringify(err)) 415 } 416 if (userGrantPermissions.indexOf(permission) != -1) { 417 reqUserRecords.push(record.permissionRecords[j]) 418 } 419 } 420 for (let k = 0; k < info.reqPermissions.length; k++) { 421 var reqPermission: any = info.reqPermissions[k]; 422 try { 423 var reqFlag = await acManager.getPermissionFlags(info.appInfo.accessTokenId, reqPermission) 424 if(reqFlag == Constants.PRE_AUTHORIZATION_NOT_MODIFIED) { 425 continue 426 } 427 } 428 catch(err) { 429 console.log(TAG + 'getPermissionFlags error: ' + JSON.stringify(err)) 430 } 431 if (userGrantPermissions.indexOf(reqPermission) != -1) { 432 reqUserPermissions.push(reqPermission); 433 } 434 } 435 436 let groupNames = []; 437 let appLastTime = 0; 438 reqUserRecords.forEach(reqUserRecord => { 439 var group = getPermissionGroup(reqUserRecord.permissionName) 440 if(!group) { 441 console.info(TAG + "permission not find:" + reqUserRecord.permissionName) 442 }else { 443 var existing = permissionGroups.find(permissionGroup => permissionGroup.name == group.name) 444 var lastTime = reqUserRecord.lastAccessTime 445 lastTime > appLastTime ? appLastTime = lastTime : '' 446 if(!existing) { 447 group['count' + record.tokenId] = reqUserRecord.accessCount 448 group['lastTime' + record.tokenId] = lastTime 449 permissionGroups.push(group) 450 groupNames.push(group.groupName) 451 }else { 452 existing['count' + record.tokenId] += reqUserRecord.accessCount 453 lastTime > existing['lastTime' + record.tokenId] ? existing['lastTime' + record.tokenId] = lastTime : '' 454 } 455 } 456 }) 457 458 let groupIds = []; 459 for (let i = 0; i < reqUserPermissions.length; i++) { 460 if(groupIds.indexOf(permissionGroupIds[reqUserPermissions[i]]) == -1){ 461 groupIds.push(permissionGroupIds[reqUserPermissions[i]]); 462 } 463 } 464 465 let context = globalThis.context.createBundleContext(info.name) 466 await context.resourceManager.getString(info.appInfo.labelId, (error, value) => { 467 if (value == undefined) { 468 appInfo.groupName = info.appInfo.label; 469 } else { 470 appInfo.groupName = value; 471 } 472 }) 473 474 await context.resourceManager.getMediaBase64(info.appInfo.iconId, (error, value) => { 475 appInfo.icon = value; 476 }) 477 478 appInfo.name = info.appInfo.name 479 appInfo.accessTokenId = info.appInfo.accessTokenId 480 appInfo.reqUserPermissions = reqUserPermissions 481 appInfo.permissions = permissionGroups 482 appInfo.groupNames = groupNames 483 appInfo.groupIds = groupIds 484 appInfo.appLastTime = appLastTime 485 this.applicationInfos.push(appInfo) 486 if(sortFlag) { 487 var appInfos: any[] = [] 488 this.applicationInfos.forEach(item => { appInfos.push(item) }) 489 appInfos.sort((a, b) => { return b.appLastTime - a.appLastTime }) 490 this.applicationInfos = appInfos 491 } 492 }) 493 } 494 495 getAllRecords() { 496 let request = { 497 "tokenId": 0, 498 "isRemote": false, 499 "deviceId": '', 500 "bundleName": '', 501 "permissionNames": [], 502 "beginTime": 0, 503 "endTime": 0, 504 "flag": 1 505 } 506 privacyManager.getPermissionUsedRecord(request).then(async records => { 507 console.info(TAG + "records: " + JSON.stringify(records.bundleRecords)) 508 var groupArray: any[] = [] 509 for (let i = 0; i < records.bundleRecords.length; i++) { 510 var record = records.bundleRecords[i] 511 try { 512 const ret = await bundle.queryAbilityByWant({ 513 bundleName: record.bundleName, 514 action: "action.system.home", 515 entities: ["entity.system.home"] 516 }, bundle.BundleFlag.GET_ABILITY_INFO_WITH_APPLICATION, Constants.USERID); 517 } catch(e) { 518 continue; 519 } 520 if (noNeedDisplayApp.indexOf(record.bundleName) != -1) { 521 continue; 522 } 523 console.info(TAG + "record: " + JSON.stringify(record)) 524 this.getInfo(record, (i + 1) == records.bundleRecords.length) 525 526 record.permissionRecords.forEach(permissionRecord => { 527 var group = getPermissionGroup(permissionRecord.permissionName) 528 if(group) { 529 var exist = groupArray.find(permissionGroup => permissionGroup.name == group.name) 530 var lastTime = permissionRecord.lastAccessTime 531 if(!exist) { 532 group.sum = permissionRecord.accessCount 533 group.recentVisit = lastTime 534 groupArray.push(group) 535 }else { 536 exist.sum += permissionRecord.accessCount 537 lastTime > exist.recentVisit ? exist.recentVisit = lastTime : '' 538 } 539 } 540 }) 541 } 542 groupArray.sort((a, b) => { return b.recentVisit - a.recentVisit }) 543 this.groups = groupArray 544 }) 545 } 546 547 aboutToAppear() { 548 this.getStrings() 549 this.getAllRecords() 550 } 551} 552