1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.model 17 18 import android.annotation.SuppressLint 19 import android.appwidget.AppWidgetProviderInfo 20 import android.content.ComponentName 21 import android.content.Intent 22 import android.content.pm.LauncherApps 23 import android.content.pm.PackageInstaller 24 import android.content.pm.ShortcutInfo 25 import android.graphics.Point 26 import android.text.TextUtils 27 import android.util.Log 28 import android.util.LongSparseArray 29 import com.android.launcher3.Flags 30 import com.android.launcher3.InvariantDeviceProfile 31 import com.android.launcher3.LauncherAppState 32 import com.android.launcher3.LauncherSettings.Favorites 33 import com.android.launcher3.Utilities 34 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError 35 import com.android.launcher3.config.FeatureFlags 36 import com.android.launcher3.logging.FileLog 37 import com.android.launcher3.model.data.AppInfo 38 import com.android.launcher3.model.data.AppPairInfo 39 import com.android.launcher3.model.data.FolderInfo 40 import com.android.launcher3.model.data.IconRequestInfo 41 import com.android.launcher3.model.data.ItemInfoWithIcon 42 import com.android.launcher3.model.data.LauncherAppWidgetInfo 43 import com.android.launcher3.model.data.WorkspaceItemInfo 44 import com.android.launcher3.pm.PackageInstallInfo 45 import com.android.launcher3.pm.UserCache 46 import com.android.launcher3.shortcuts.ShortcutKey 47 import com.android.launcher3.util.ApiWrapper 48 import com.android.launcher3.util.ComponentKey 49 import com.android.launcher3.util.PackageManagerHelper 50 import com.android.launcher3.util.PackageUserKey 51 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo 52 import com.android.launcher3.widget.WidgetInflater 53 import com.android.launcher3.widget.util.WidgetSizes 54 55 /** 56 * This items is used by LoaderTask to process items that have been loaded from the Launcher's DB. 57 * This data, stored in the Favorites table, needs to be processed in order to be shown on the Home 58 * Page. 59 * 60 * This class processes each of those items: App Shortcuts, Widgets, Folders, etc., one at a time. 61 */ 62 class WorkspaceItemProcessor( 63 private val c: LoaderCursor, 64 private val memoryLogger: LoaderMemoryLogger?, 65 private val userCache: UserCache, 66 private val userManagerState: UserManagerState, 67 private val launcherApps: LauncherApps, 68 private val pendingPackages: MutableSet<PackageUserKey>, 69 private val shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo>, 70 private val app: LauncherAppState, 71 private val bgDataModel: BgDataModel, 72 private val widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?>, 73 private val installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>, 74 private val isSdCardReady: Boolean, 75 private val widgetInflater: WidgetInflater, 76 private val pmHelper: PackageManagerHelper, 77 private val iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>, 78 private val unlockedUsers: LongSparseArray<Boolean>, 79 private val allDeepShortcuts: MutableList<ShortcutInfo> 80 ) { 81 82 private val isSafeMode = app.isSafeModeEnabled 83 private val tempPackageKey = PackageUserKey(null, null) 84 private val iconCache = app.iconCache 85 86 /** 87 * This is the entry point for processing 1 workspace item. This method is like the midfielder 88 * that delegates the actual processing to either processAppShortcut, processFolder, or 89 * processWidget depending on what type of item is being processed. 90 * 91 * All the parameters are expected to be shared between many repeated calls of this method, one 92 * for each workspace item. 93 */ processItemnull94 fun processItem() { 95 try { 96 if (c.user == null) { 97 // User has been deleted, remove the item. 98 c.markDeleted( 99 "User has been deleted for item id=${c.id}", 100 RestoreError.PROFILE_DELETED 101 ) 102 return 103 } 104 when (c.itemType) { 105 Favorites.ITEM_TYPE_APPLICATION, 106 Favorites.ITEM_TYPE_DEEP_SHORTCUT -> processAppOrDeepShortcut() 107 Favorites.ITEM_TYPE_FOLDER, 108 Favorites.ITEM_TYPE_APP_PAIR -> processFolderOrAppPair() 109 Favorites.ITEM_TYPE_APPWIDGET, 110 Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> processWidget() 111 } 112 } catch (e: Exception) { 113 Log.e(TAG, "Desktop items loading interrupted", e) 114 } 115 } 116 117 /** 118 * This method verifies that an app shortcut should be shown on the home screen, updates the 119 * database accordingly, formats the data in such a way that it is ready to be added to the data 120 * model, and then adds it to the launcher’s data model. 121 * 122 * In this method, verification means that an an app shortcut database entry is required to: 123 * Have a Launch Intent. This is how the app component symbolized by the shortcut is launched. 124 * Have a Package Name. Not be in a funky “Restoring, but never actually restored” state. Not 125 * have null or missing ShortcutInfos or ItemInfos in other data models. 126 * 127 * If any of the above are found to be true, the database entry is deleted, and not shown on the 128 * user’s home screen. When an app is verified, it is marked as restored, meaning that the app 129 * is viable to show on the home screen. 130 * 131 * In order to accommodate different types and versions of App Shortcuts, different properties 132 * and flags are set on the ItemInfo objects that are added to the data model. For example, 133 * icons that are not a part of the workspace or hotseat are marked as using low resolution icon 134 * bitmaps. Currently suspended app icons are marked as such. Installing packages are also 135 * marked as such. Lastly, after applying common properties to the ItemInfo, it is added to the 136 * data model to be bound to the launcher’s data model. 137 */ 138 @SuppressLint("NewApi") processAppOrDeepShortcutnull139 private fun processAppOrDeepShortcut() { 140 var allowMissingTarget = false 141 var intent = c.parseIntent() 142 if (intent == null) { 143 c.markDeleted("Null intent from db for item id=${c.id}", RestoreError.MISSING_INFO) 144 return 145 } 146 var disabledState = 147 if (userManagerState.isUserQuiet(c.serialNumber)) 148 WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER 149 else 0 150 val cn = intent.component 151 val targetPkg = cn?.packageName ?: intent.getPackage() 152 if (targetPkg.isNullOrEmpty()) { 153 c.markDeleted("No target package for item id=${c.id}", RestoreError.MISSING_INFO) 154 return 155 } 156 var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user) 157 158 // If it's a deep shortcut, we'll use pinned shortcuts to restore it 159 if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) { 160 // If the apk is present and the shortcut points to a specific component. 161 162 // If the component is already present 163 if (launcherApps.isActivityEnabled(cn, c.user)) { 164 // no special handling necessary for this item 165 c.markRestored() 166 } else { 167 // Gracefully try to find a fallback activity. 168 FileLog.d( 169 TAG, 170 "Activity not enabled for id=${c.id}, component=$cn, user=${c.user}." + 171 " Will attempt to find fallback Activity for targetPkg=$targetPkg." 172 ) 173 intent = pmHelper.getAppLaunchIntent(targetPkg, c.user) 174 if (intent != null) { 175 c.restoreFlag = 0 176 c.updater().put(Favorites.INTENT, intent.toUri(0)).commit() 177 } else { 178 c.markDeleted( 179 "No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." + 180 " Unable to create launch Intent.", 181 RestoreError.MISSING_INFO 182 ) 183 return 184 } 185 } 186 } 187 if (intent.`package` == null) { 188 intent.`package` = targetPkg 189 } 190 // else if cn == null => can't infer much, leave it 191 // else if !validPkg => could be restored icon or missing sd-card 192 when { 193 !TextUtils.isEmpty(targetPkg) && !validTarget -> { 194 // Points to a valid app (superset of cn != null) but the apk 195 // is not available. 196 when { 197 c.restoreFlag != 0 -> { 198 // Package is not yet available but might be 199 // installed later. 200 FileLog.d(TAG, "package not yet restored: $targetPkg") 201 tempPackageKey.update(targetPkg, c.user) 202 when { 203 c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> { 204 // Restore has started once. 205 } 206 installingPkgs.containsKey(tempPackageKey) -> { 207 // App restore has started. Update the flag 208 c.restoreFlag = 209 c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED 210 FileLog.d(TAG, "restore started for installing app: $targetPkg") 211 c.updater().put(Favorites.RESTORED, c.restoreFlag).commit() 212 } 213 else -> { 214 c.markDeleted( 215 "removing app that is not restored and not installing. package: $targetPkg", 216 RestoreError.APP_NOT_INSTALLED 217 ) 218 return 219 } 220 } 221 } 222 pmHelper.isAppOnSdcard(targetPkg, c.user) -> { 223 // Package is present but not available. 224 disabledState = 225 disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE 226 // Add the icon on the workspace anyway. 227 allowMissingTarget = true 228 } 229 !isSdCardReady -> { 230 // SdCard is not ready yet. Package might get available, 231 // once it is ready. 232 Log.d(TAG, "Missing package, will check later: $targetPkg") 233 pendingPackages.add(PackageUserKey(targetPkg, c.user)) 234 // Add the icon on the workspace anyway. 235 allowMissingTarget = true 236 } 237 else -> { 238 // Do not wait for external media load anymore. 239 c.markDeleted( 240 "Invalid package removed: $targetPkg", 241 RestoreError.APP_NOT_INSTALLED 242 ) 243 return 244 } 245 } 246 } 247 } 248 if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) { 249 validTarget = false 250 } 251 if (validTarget) { 252 // The shortcut points to a valid target (either no target 253 // or something which is ready to be used) 254 c.markRestored() 255 } 256 val useLowResIcon = !c.isOnWorkspaceOrHotseat 257 val info: WorkspaceItemInfo? 258 when { 259 c.restoreFlag != 0 -> { 260 // Already verified above that user is same as default user 261 info = c.getRestoredItemInfo(intent) 262 } 263 c.itemType == Favorites.ITEM_TYPE_APPLICATION -> 264 info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false) 265 c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT -> { 266 val key = ShortcutKey.fromIntent(intent, c.user) 267 if (unlockedUsers[c.serialNumber]) { 268 val pinnedShortcut = shortcutKeyToPinnedShortcuts[key] 269 if (pinnedShortcut == null) { 270 // The shortcut is no longer valid. 271 c.markDeleted( 272 "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}", 273 RestoreError.SHORTCUT_NOT_FOUND 274 ) 275 return 276 } 277 info = WorkspaceItemInfo(pinnedShortcut, app.context) 278 // If the pinned deep shortcut is no longer published, 279 // use the last saved icon instead of the default. 280 iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon) 281 if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) { 282 info.runtimeStatusFlags = 283 info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED 284 } 285 intent = info.getIntent() 286 allDeepShortcuts.add(pinnedShortcut) 287 } else { 288 // Create a shortcut info in disabled mode for now. 289 info = c.loadSimpleWorkspaceItem() 290 info.runtimeStatusFlags = 291 info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER 292 } 293 } 294 else -> { // item type == ITEM_TYPE_SHORTCUT 295 info = c.loadSimpleWorkspaceItem() 296 297 // Shortcuts are only available on the primary profile 298 if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg, c.user)) { 299 disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED 300 } 301 info.options = c.options 302 303 // App shortcuts that used to be automatically added to Launcher 304 // didn't always have the correct intent flags set, so do that here 305 if ( 306 intent.action != null && 307 intent.categories != null && 308 intent.action == Intent.ACTION_MAIN && 309 intent.categories.contains(Intent.CATEGORY_LAUNCHER) 310 ) { 311 intent.addFlags( 312 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 313 ) 314 } 315 } 316 } 317 if (info != null) { 318 if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 319 // Skip deep shortcuts; their title and icons have already been 320 // loaded above. 321 iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon)) 322 } 323 c.applyCommonProperties(info) 324 info.intent = intent 325 info.rank = c.rank 326 info.spanX = 1 327 info.spanY = 1 328 info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState 329 if (isSafeMode && !PackageManagerHelper.isSystemApp(app.context, intent)) { 330 info.runtimeStatusFlags = 331 info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE 332 } 333 val activityInfo = c.launcherActivityInfo 334 if (activityInfo != null) { 335 AppInfo.updateRuntimeFlagsForActivityTarget( 336 info, 337 activityInfo, 338 userCache.getUserInfo(c.user), 339 ApiWrapper.INSTANCE[app.context], 340 pmHelper 341 ) 342 } 343 if ( 344 (c.restoreFlag != 0 || 345 Flags.enableSupportForArchiving() && 346 activityInfo != null && 347 activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg) 348 ) { 349 tempPackageKey.update(targetPkg, c.user) 350 val si = installingPkgs[tempPackageKey] 351 if (si == null) { 352 info.runtimeStatusFlags = 353 info.runtimeStatusFlags and 354 ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE.inv() 355 } else if ( 356 activityInfo == null || 357 (Flags.enableSupportForArchiving() && 358 activityInfo.applicationInfo.isArchived) 359 ) { 360 // For archived apps, include progress info in case there is 361 // a pending install session post restart of device. 362 val installProgress = (si.getProgress() * 100).toInt() 363 info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING) 364 } 365 } 366 c.checkAndAddItem(info, bgDataModel, memoryLogger) 367 } else { 368 throw RuntimeException("Unexpected null WorkspaceItemInfo") 369 } 370 } 371 372 /** 373 * Loads CollectionInfo information from the database and formats it. This function runs while 374 * LoaderTask is still active; some of the processing for folder content items is done after all 375 * the items in the workspace have been loaded. The loaded and formatted CollectionInfo is then 376 * stored in the BgDataModel. 377 */ processFolderOrAppPairnull378 private fun processFolderOrAppPair() { 379 var collection = bgDataModel.findOrMakeFolder(c.id) 380 // If we generated a placeholder Folder before this point, it may need to be replaced with 381 // an app pair. 382 if (c.itemType == Favorites.ITEM_TYPE_APP_PAIR && collection is FolderInfo) { 383 if (!FeatureFlags.enableAppPairs()) { 384 // If app pairs are not enabled, stop loading. 385 Log.e(TAG, "app pairs flag is off, did not load app pair") 386 return 387 } 388 389 val folderInfo: FolderInfo = collection 390 val newAppPair = AppPairInfo() 391 // Move the placeholder's contents over to the new app pair. 392 folderInfo.getContents().forEach(newAppPair::add) 393 collection = newAppPair 394 // Remove the placeholder and add the app pair into the data model. 395 bgDataModel.collections.remove(c.id) 396 bgDataModel.collections.put(c.id, collection) 397 } 398 399 c.applyCommonProperties(collection) 400 // Do not trim the folder label, as is was set by the user. 401 collection.title = c.getString(c.mTitleIndex) 402 collection.spanX = 1 403 collection.spanY = 1 404 if (collection is FolderInfo) { 405 collection.options = c.options 406 } else { 407 // An app pair may be inside another folder, so it needs to preserve rank information. 408 collection.rank = c.rank 409 } 410 411 c.markRestored() 412 c.checkAndAddItem(collection, bgDataModel, memoryLogger) 413 } 414 415 /** 416 * This method, similar to processAppShortcut above, verifies that a widget should be shown on 417 * the home screen, updates the database accordingly, formats the data in such a way that it is 418 * ready to be added to the data model, and then adds it to the launcher’s data model. 419 * 420 * It verifies that: Widgets are not disabled due to the Launcher variety being of the `Go` 421 * type. Search Widgets have a package name. The app behind the widget is still installed on the 422 * device. The app behind the widget is not in a funky “Restoring, but never actually restored” 423 * state. The widget has a valid size. The widget is in the workspace or the hotseat. If any of 424 * the above are found to be true, the database entry is deleted, and the widget is not shown on 425 * the user’s home screen. When a widget is verified, it is marked as restored, meaning that the 426 * widget is viable to show on the home screen. 427 * 428 * Common properties are applied to the Widget’s Info object, and other information as well 429 * depending on the type of widget. Custom widgets are treated differently than non-custom 430 * widgets, installing / restoring widgets are treated differently, etc. 431 */ processWidgetnull432 private fun processWidget() { 433 val component = ComponentName.unflattenFromString(c.appWidgetProvider)!! 434 val appWidgetInfo = LauncherAppWidgetInfo(c.appWidgetId, component) 435 c.applyCommonProperties(appWidgetInfo) 436 appWidgetInfo.spanX = c.spanX 437 appWidgetInfo.spanY = c.spanY 438 appWidgetInfo.options = c.options 439 appWidgetInfo.user = c.user 440 appWidgetInfo.sourceContainer = c.appWidgetSource 441 appWidgetInfo.restoreStatus = c.restoreFlag 442 if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { 443 c.markDeleted( 444 "processWidget: Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}" + 445 ", id=${c.id}," + 446 ", appWidgetId=${c.appWidgetId}," + 447 ", component=${component}", 448 RestoreError.INVALID_LOCATION 449 ) 450 return 451 } 452 if (!c.isOnWorkspaceOrHotseat) { 453 c.markDeleted( 454 "processWidget: invalid Widget container != CONTAINER_DESKTOP nor CONTAINER_HOTSEAT." + 455 " id=${c.id}," + 456 ", appWidgetId=${c.appWidgetId}," + 457 ", component=${component}," + 458 ", container=${c.container}", 459 RestoreError.INVALID_LOCATION 460 ) 461 return 462 } 463 if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { 464 appWidgetInfo.bindOptions = c.parseIntent() 465 } 466 val inflationResult = widgetInflater.inflateAppWidget(appWidgetInfo) 467 var shouldUpdate = inflationResult.isUpdate 468 val lapi = inflationResult.widgetInfo 469 FileLog.d( 470 TAG, 471 "processWidget: id=${c.id}" + 472 ", appWidgetId=${c.appWidgetId}" + 473 ", inflationResult=$inflationResult" 474 ) 475 when (inflationResult.type) { 476 WidgetInflater.TYPE_DELETE -> { 477 c.markDeleted(inflationResult.reason, inflationResult.restoreErrorType) 478 return 479 } 480 WidgetInflater.TYPE_PENDING -> { 481 tempPackageKey.update(component.packageName, c.user) 482 val si = installingPkgs[tempPackageKey] 483 484 if ( 485 !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && 486 !isSafeMode && 487 (si == null) && 488 (lapi == null) && 489 !(Flags.enableSupportForArchiving() && 490 pmHelper.isAppArchived(component.packageName)) 491 ) { 492 // Restore never started 493 c.markDeleted( 494 "processWidget: Unrestored Pending widget removed:" + 495 " id=${c.id}" + 496 ", appWidgetId=${c.appWidgetId}" + 497 ", component=${component}" + 498 ", restoreFlag:=${c.restoreFlag}", 499 RestoreError.APP_NOT_INSTALLED 500 ) 501 return 502 } else if ( 503 !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && si != null 504 ) { 505 shouldUpdate = true 506 appWidgetInfo.restoreStatus = 507 appWidgetInfo.restoreStatus or LauncherAppWidgetInfo.FLAG_RESTORE_STARTED 508 } 509 appWidgetInfo.installProgress = 510 if (si == null) 0 else (si.getProgress() * 100).toInt() 511 appWidgetInfo.pendingItemInfo = 512 WidgetsModel.newPendingItemInfo( 513 app.context, 514 appWidgetInfo.providerName, 515 appWidgetInfo.user 516 ) 517 iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false) 518 } 519 WidgetInflater.TYPE_REAL -> 520 WidgetSizes.updateWidgetSizeRangesAsync( 521 appWidgetInfo.appWidgetId, 522 lapi, 523 app.context, 524 appWidgetInfo.spanX, 525 appWidgetInfo.spanY 526 ) 527 } 528 529 if (shouldUpdate) { 530 c.updater() 531 .put(Favorites.APPWIDGET_PROVIDER, component.flattenToString()) 532 .put(Favorites.APPWIDGET_ID, appWidgetInfo.appWidgetId) 533 .put(Favorites.RESTORED, appWidgetInfo.restoreStatus) 534 .commit() 535 } 536 if (lapi != null) { 537 widgetProvidersMap[ComponentKey(lapi.provider, lapi.user)] = inflationResult.widgetInfo 538 if (appWidgetInfo.spanX < lapi.minSpanX || appWidgetInfo.spanY < lapi.minSpanY) { 539 FileLog.d( 540 TAG, 541 " processWidget: Widget ${lapi.component} minSizes not met: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}," + 542 " id: ${c.id}," + 543 " appWidgetId: ${c.appWidgetId}," + 544 " component=${component}" 545 ) 546 logWidgetInfo(app.invariantDeviceProfile, lapi) 547 } 548 } 549 c.checkAndAddItem(appWidgetInfo, bgDataModel) 550 } 551 552 companion object { 553 private const val TAG = "WorkspaceItemProcessor" 554 logWidgetInfonull555 private fun logWidgetInfo( 556 idp: InvariantDeviceProfile, 557 widgetProviderInfo: LauncherAppWidgetProviderInfo 558 ) { 559 val cellSize = Point() 560 for (deviceProfile in idp.supportedProfiles) { 561 deviceProfile.getCellSize(cellSize) 562 FileLog.d( 563 TAG, 564 "DeviceProfile available width: ${deviceProfile.availableWidthPx}," + 565 " available height: ${deviceProfile.availableHeightPx}," + 566 " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," + 567 " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," + 568 " cellSize: $cellSize" 569 ) 570 } 571 val widgetDimension = StringBuilder() 572 widgetDimension 573 .append("Widget dimensions:\n") 574 .append("minResizeWidth: ") 575 .append(widgetProviderInfo.minResizeWidth) 576 .append("\n") 577 .append("minResizeHeight: ") 578 .append(widgetProviderInfo.minResizeHeight) 579 .append("\n") 580 .append("defaultWidth: ") 581 .append(widgetProviderInfo.minWidth) 582 .append("\n") 583 .append("defaultHeight: ") 584 .append(widgetProviderInfo.minHeight) 585 .append("\n") 586 if (Utilities.ATLEAST_S) { 587 widgetDimension 588 .append("targetCellWidth: ") 589 .append(widgetProviderInfo.targetCellWidth) 590 .append("\n") 591 .append("targetCellHeight: ") 592 .append(widgetProviderInfo.targetCellHeight) 593 .append("\n") 594 .append("maxResizeWidth: ") 595 .append(widgetProviderInfo.maxResizeWidth) 596 .append("\n") 597 .append("maxResizeHeight: ") 598 .append(widgetProviderInfo.maxResizeHeight) 599 .append("\n") 600 } 601 FileLog.d(TAG, widgetDimension.toString()) 602 } 603 } 604 } 605