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