Files
Lawnchair/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
T
Stefan Andonian 6df13d3b8e Decouple Backup / Restore Error Codes
Bug: 381315244
Test: Simply looked at emitted error codes and changed names if they were used in multiple locations.
Flag: EXEMPT bug fix
Change-Id: Iab4a8d6ff410682f448b2ac32e44b766d79a7616
2024-11-27 16:48:28 -08:00

610 lines
28 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model
import android.annotation.SuppressLint
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.PackageInstaller
import android.content.pm.ShortcutInfo
import android.graphics.Point
import android.text.TextUtils
import android.util.Log
import android.util.LongSparseArray
import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.icons.CacheableShortcutInfo
import com.android.launcher3.logging.FileLog
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.AppPairInfo
import com.android.launcher3.model.data.FolderInfo
import com.android.launcher3.model.data.IconRequestInfo
import com.android.launcher3.model.data.ItemInfoWithIcon
import com.android.launcher3.model.data.LauncherAppWidgetInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.PackageInstallInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.shortcuts.ShortcutKey
import com.android.launcher3.util.ApiWrapper
import com.android.launcher3.util.ApplicationInfoWrapper
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.PackageManagerHelper
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widget.WidgetInflater
import com.android.launcher3.widget.util.WidgetSizes
/**
* This items is used by LoaderTask to process items that have been loaded from the Launcher's DB.
* This data, stored in the Favorites table, needs to be processed in order to be shown on the Home
* Page.
*
* This class processes each of those items: App Shortcuts, Widgets, Folders, etc., one at a time.
*/
class WorkspaceItemProcessor(
private val c: LoaderCursor,
private val memoryLogger: LoaderMemoryLogger?,
private val userCache: UserCache,
private val userManagerState: UserManagerState,
private val launcherApps: LauncherApps,
private val pendingPackages: MutableSet<PackageUserKey>,
private val shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo>,
private val app: LauncherAppState,
private val bgDataModel: BgDataModel,
private val widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?>,
private val installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>,
private val isSdCardReady: Boolean,
private val widgetInflater: WidgetInflater,
private val pmHelper: PackageManagerHelper,
private val iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>,
private val unlockedUsers: LongSparseArray<Boolean>,
private val allDeepShortcuts: MutableList<CacheableShortcutInfo>,
) {
private val isSafeMode = app.isSafeModeEnabled
private val tempPackageKey = PackageUserKey(null, null)
private val iconCache = app.iconCache
/**
* This is the entry point for processing 1 workspace item. This method is like the midfielder
* that delegates the actual processing to either processAppShortcut, processFolder, or
* processWidget depending on what type of item is being processed.
*
* All the parameters are expected to be shared between many repeated calls of this method, one
* for each workspace item.
*/
fun processItem() {
try {
if (c.user == null) {
// User has been deleted, remove the item.
c.markDeleted(
"User has been deleted for item id=${c.id}",
RestoreError.PROFILE_DELETED,
)
return
}
when (c.itemType) {
Favorites.ITEM_TYPE_APPLICATION,
Favorites.ITEM_TYPE_DEEP_SHORTCUT -> processAppOrDeepShortcut()
Favorites.ITEM_TYPE_FOLDER,
Favorites.ITEM_TYPE_APP_PAIR -> processFolderOrAppPair()
Favorites.ITEM_TYPE_APPWIDGET,
Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> processWidget()
}
} catch (e: Exception) {
Log.e(TAG, "Desktop items loading interrupted", e)
}
}
/**
* This method verifies that an app shortcut should be shown on the home screen, updates the
* database accordingly, formats the data in such a way that it is ready to be added to the data
* model, and then adds it to the launchers data model.
*
* In this method, verification means that an an app shortcut database entry is required to:
* Have a Launch Intent. This is how the app component symbolized by the shortcut is launched.
* Have a Package Name. Not be in a funky “Restoring, but never actually restored” state. Not
* have null or missing ShortcutInfos or ItemInfos in other data models.
*
* If any of the above are found to be true, the database entry is deleted, and not shown on the
* users home screen. When an app is verified, it is marked as restored, meaning that the app
* is viable to show on the home screen.
*
* In order to accommodate different types and versions of App Shortcuts, different properties
* and flags are set on the ItemInfo objects that are added to the data model. For example,
* icons that are not a part of the workspace or hotseat are marked as using low resolution icon
* bitmaps. Currently suspended app icons are marked as such. Installing packages are also
* marked as such. Lastly, after applying common properties to the ItemInfo, it is added to the
* data model to be bound to the launchers data model.
*/
@SuppressLint("NewApi")
private fun processAppOrDeepShortcut() {
var allowMissingTarget = false
var intent = c.parseIntent()
if (intent == null) {
c.markDeleted("Null intent from db for item id=${c.id}", RestoreError.APP_NO_DB_INTENT)
return
}
var disabledState =
if (userManagerState.isUserQuiet(c.serialNumber))
WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER
else 0
val cn = intent.component
val targetPkg = cn?.packageName ?: intent.getPackage()
if (targetPkg.isNullOrEmpty()) {
c.markDeleted(
"No target package for item id=${c.id}",
RestoreError.APP_NO_TARGET_PACKAGE,
)
return
}
val appInfoWrapper = ApplicationInfoWrapper(app.context, targetPkg, c.user)
var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user)
// If it's a deep shortcut, we'll use pinned shortcuts to restore it
if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
// If the apk is present and the shortcut points to a specific component.
// If the component is already present
if (launcherApps.isActivityEnabled(cn, c.user)) {
// no special handling necessary for this item
c.markRestored()
} else {
// Gracefully try to find a fallback activity.
FileLog.d(
TAG,
"Activity not enabled for id=${c.id}, component=$cn, user=${c.user}." +
" Will attempt to find fallback Activity for targetPkg=$targetPkg.",
)
intent = pmHelper.getAppLaunchIntent(targetPkg, c.user)
if (intent != null) {
c.restoreFlag = 0
c.updater().put(Favorites.INTENT, intent.toUri(0)).commit()
} else {
c.markDeleted(
"No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." +
" Unable to create launch Intent.",
RestoreError.APP_NO_LAUNCH_INTENT,
)
return
}
}
}
if (intent.`package` == null) {
intent.`package` = targetPkg
}
// else if cn == null => can't infer much, leave it
// else if !validPkg => could be restored icon or missing sd-card
when {
!TextUtils.isEmpty(targetPkg) && !validTarget -> {
// Points to a valid app (superset of cn != null) but the apk
// is not available.
when {
c.restoreFlag != 0 -> {
// Package is not yet available but might be
// installed later.
FileLog.d(TAG, "package not yet restored: $targetPkg")
tempPackageKey.update(targetPkg, c.user)
when {
c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
// Restore has started once.
}
installingPkgs.containsKey(tempPackageKey) -> {
// App restore has started. Update the flag
c.restoreFlag =
c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
FileLog.d(TAG, "restore started for installing app: $targetPkg")
c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
}
else -> {
c.markDeleted(
"removing app that is not restored and not installing. package: $targetPkg",
RestoreError.APP_NOT_RESTORED_OR_INSTALLING,
)
return
}
}
}
appInfoWrapper.isOnSdCard() -> {
// Package is present but not available.
disabledState =
disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE
// Add the icon on the workspace anyway.
allowMissingTarget = true
}
!isSdCardReady -> {
// SdCard is not ready yet. Package might get available,
// once it is ready.
Log.d(TAG, "Missing package, will check later: $targetPkg")
pendingPackages.add(PackageUserKey(targetPkg, c.user))
// Add the icon on the workspace anyway.
allowMissingTarget = true
}
else -> {
// Do not wait for external media load anymore.
c.markDeleted(
"Invalid package removed: $targetPkg",
RestoreError.APP_NOT_INSTALLED_EXTERNAL_MEDIA,
)
return
}
}
}
}
if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
validTarget = false
}
if (validTarget) {
// The shortcut points to a valid target (either no target
// or something which is ready to be used)
c.markRestored()
}
val useLowResIcon = !c.isOnWorkspaceOrHotseat
val info: WorkspaceItemInfo?
when {
c.restoreFlag != 0 -> {
// Already verified above that user is same as default user
info = c.getRestoredItemInfo(intent)
}
c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT -> {
val key = ShortcutKey.fromIntent(intent, c.user)
if (unlockedUsers[c.serialNumber]) {
val pinnedShortcut = shortcutKeyToPinnedShortcuts[key]
if (pinnedShortcut == null) {
// The shortcut is no longer valid.
c.markDeleted(
"Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}",
RestoreError.SHORTCUT_NOT_FOUND,
)
return
}
info = WorkspaceItemInfo(pinnedShortcut, app.context)
// If the pinned deep shortcut is no longer published,
// use the last saved icon instead of the default.
val csi = CacheableShortcutInfo(pinnedShortcut, appInfoWrapper)
iconCache.getShortcutIcon(info, csi, c::loadIcon)
if (appInfoWrapper.isSuspended()) {
info.runtimeStatusFlags =
info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
}
intent = info.getIntent()
allDeepShortcuts.add(csi)
} else {
// Create a shortcut info in disabled mode for now.
info = c.loadSimpleWorkspaceItem()
info.runtimeStatusFlags =
info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER
}
}
else -> { // item type == ITEM_TYPE_SHORTCUT
info = c.loadSimpleWorkspaceItem()
// Shortcuts are only available on the primary profile
if (appInfoWrapper.isSuspended()) {
disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
}
info.options = c.options
// App shortcuts that used to be automatically added to Launcher
// didn't always have the correct intent flags set, so do that here
if (
intent.action != null &&
intent.categories != null &&
intent.action == Intent.ACTION_MAIN &&
intent.categories.contains(Intent.CATEGORY_LAUNCHER)
) {
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
)
}
}
}
if (info != null) {
if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// Skip deep shortcuts; their title and icons have already been
// loaded above.
iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon))
}
c.applyCommonProperties(info)
info.intent = intent
info.rank = c.rank
info.spanX = 1
info.spanY = 1
info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState
if (isSafeMode && !appInfoWrapper.isSystem()) {
info.runtimeStatusFlags =
info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE
}
val activityInfo = c.launcherActivityInfo
if (activityInfo != null) {
AppInfo.updateRuntimeFlagsForActivityTarget(
info,
activityInfo,
userCache.getUserInfo(c.user),
ApiWrapper.INSTANCE[app.context],
pmHelper,
)
}
if (
(c.restoreFlag != 0 ||
Flags.enableSupportForArchiving() &&
activityInfo != null &&
activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg)
) {
tempPackageKey.update(targetPkg, c.user)
val si = installingPkgs[tempPackageKey]
if (si == null) {
info.runtimeStatusFlags =
info.runtimeStatusFlags and
ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE.inv()
} else if (
activityInfo == null ||
(Flags.enableSupportForArchiving() &&
activityInfo.applicationInfo.isArchived)
) {
// For archived apps, include progress info in case there is
// a pending install session post restart of device.
val installProgress = (si.getProgress() * 100).toInt()
info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING)
}
}
c.checkAndAddItem(info, bgDataModel, memoryLogger)
} else {
throw RuntimeException("Unexpected null WorkspaceItemInfo")
}
}
/**
* Loads CollectionInfo information from the database and formats it. This function runs while
* LoaderTask is still active; some of the processing for folder content items is done after all
* the items in the workspace have been loaded. The loaded and formatted CollectionInfo is then
* stored in the BgDataModel.
*/
private fun processFolderOrAppPair() {
var collection = bgDataModel.findOrMakeFolder(c.id)
// If we generated a placeholder Folder before this point, it may need to be replaced with
// an app pair.
if (c.itemType == Favorites.ITEM_TYPE_APP_PAIR && collection is FolderInfo) {
if (!FeatureFlags.enableAppPairs()) {
// If app pairs are not enabled, stop loading.
Log.e(TAG, "app pairs flag is off, did not load app pair")
return
}
val folderInfo: FolderInfo = collection
val newAppPair = AppPairInfo()
// Move the placeholder's contents over to the new app pair.
folderInfo.getContents().forEach(newAppPair::add)
collection = newAppPair
// Remove the placeholder and add the app pair into the data model.
bgDataModel.collections.remove(c.id)
bgDataModel.collections.put(c.id, collection)
}
c.applyCommonProperties(collection)
// Do not trim the folder label, as is was set by the user.
collection.title = c.getString(c.mTitleIndex)
collection.spanX = 1
collection.spanY = 1
if (collection is FolderInfo) {
collection.options = c.options
} else {
// An app pair may be inside another folder, so it needs to preserve rank information.
collection.rank = c.rank
}
c.markRestored()
c.checkAndAddItem(collection, bgDataModel, memoryLogger)
}
/**
* This method, similar to processAppShortcut above, verifies that a widget should be shown on
* the home screen, updates the database accordingly, formats the data in such a way that it is
* ready to be added to the data model, and then adds it to the launchers data model.
*
* It verifies that: Widgets are not disabled due to the Launcher variety being of the `Go`
* type. Search Widgets have a package name. The app behind the widget is still installed on the
* device. The app behind the widget is not in a funky “Restoring, but never actually restored”
* state. The widget has a valid size. The widget is in the workspace or the hotseat. If any of
* the above are found to be true, the database entry is deleted, and the widget is not shown on
* the users home screen. When a widget is verified, it is marked as restored, meaning that the
* widget is viable to show on the home screen.
*
* Common properties are applied to the Widgets Info object, and other information as well
* depending on the type of widget. Custom widgets are treated differently than non-custom
* widgets, installing / restoring widgets are treated differently, etc.
*/
private fun processWidget() {
val component = ComponentName.unflattenFromString(c.appWidgetProvider)!!
val appWidgetInfo = LauncherAppWidgetInfo(c.appWidgetId, component)
c.applyCommonProperties(appWidgetInfo)
appWidgetInfo.spanX = c.spanX
appWidgetInfo.spanY = c.spanY
appWidgetInfo.options = c.options
appWidgetInfo.user = c.user
appWidgetInfo.sourceContainer = c.appWidgetSource
appWidgetInfo.restoreStatus = c.restoreFlag
if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
c.markDeleted(
"processWidget: Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}" +
", id=${c.id}," +
", appWidgetId=${c.appWidgetId}," +
", component=${component}",
RestoreError.INVALID_WIDGET_SIZE,
)
return
}
if (!c.isOnWorkspaceOrHotseat) {
c.markDeleted(
"processWidget: invalid Widget container != CONTAINER_DESKTOP nor CONTAINER_HOTSEAT." +
" id=${c.id}," +
", appWidgetId=${c.appWidgetId}," +
", component=${component}," +
", container=${c.container}",
RestoreError.INVALID_WIDGET_CONTAINER,
)
return
}
if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
appWidgetInfo.bindOptions = c.parseIntent()
}
val inflationResult = widgetInflater.inflateAppWidget(appWidgetInfo)
var shouldUpdate = inflationResult.isUpdate
val lapi = inflationResult.widgetInfo
FileLog.d(
TAG,
"processWidget: id=${c.id}" +
", appWidgetId=${c.appWidgetId}" +
", inflationResult=$inflationResult",
)
when (inflationResult.type) {
WidgetInflater.TYPE_DELETE -> {
c.markDeleted(inflationResult.reason, inflationResult.restoreErrorType)
return
}
WidgetInflater.TYPE_PENDING -> {
tempPackageKey.update(component.packageName, c.user)
val si = installingPkgs[tempPackageKey]
if (
!c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) &&
!isSafeMode &&
(si == null) &&
(lapi == null) &&
!(Flags.enableSupportForArchiving() &&
ApplicationInfoWrapper(app.context, component.packageName, c.user)
.isArchived())
) {
// Restore never started
c.markDeleted(
"processWidget: Unrestored Pending widget removed:" +
" id=${c.id}" +
", appWidgetId=${c.appWidgetId}" +
", component=${component}" +
", restoreFlag:=${c.restoreFlag}",
RestoreError.UNRESTORED_PENDING_WIDGET,
)
return
} else if (
!c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && si != null
) {
shouldUpdate = true
appWidgetInfo.restoreStatus =
appWidgetInfo.restoreStatus or LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
}
appWidgetInfo.installProgress =
if (si == null) 0 else (si.getProgress() * 100).toInt()
appWidgetInfo.pendingItemInfo =
WidgetsModel.newPendingItemInfo(
app.context,
appWidgetInfo.providerName,
appWidgetInfo.user,
)
iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false)
}
WidgetInflater.TYPE_REAL ->
WidgetSizes.updateWidgetSizeRangesAsync(
appWidgetInfo.appWidgetId,
lapi,
app.context,
appWidgetInfo.spanX,
appWidgetInfo.spanY,
)
}
if (shouldUpdate) {
c.updater()
.put(Favorites.APPWIDGET_PROVIDER, component.flattenToString())
.put(Favorites.APPWIDGET_ID, appWidgetInfo.appWidgetId)
.put(Favorites.RESTORED, appWidgetInfo.restoreStatus)
.commit()
}
if (lapi != null) {
widgetProvidersMap[ComponentKey(lapi.provider, lapi.user)] = inflationResult.widgetInfo
if (appWidgetInfo.spanX < lapi.minSpanX || appWidgetInfo.spanY < lapi.minSpanY) {
FileLog.d(
TAG,
" processWidget: Widget ${lapi.component} minSizes not met: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}," +
" id: ${c.id}," +
" appWidgetId: ${c.appWidgetId}," +
" component=${component}",
)
logWidgetInfo(app.invariantDeviceProfile, lapi)
}
}
c.checkAndAddItem(appWidgetInfo, bgDataModel)
}
companion object {
private const val TAG = "WorkspaceItemProcessor"
private fun logWidgetInfo(
idp: InvariantDeviceProfile,
widgetProviderInfo: LauncherAppWidgetProviderInfo,
) {
val cellSize = Point()
for (deviceProfile in idp.supportedProfiles) {
deviceProfile.getCellSize(cellSize)
FileLog.d(
TAG,
"DeviceProfile available width: ${deviceProfile.availableWidthPx}," +
" available height: ${deviceProfile.availableHeightPx}," +
" cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," +
" cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," +
" cellSize: $cellSize",
)
}
val widgetDimension = StringBuilder()
widgetDimension
.append("Widget dimensions:\n")
.append("minResizeWidth: ")
.append(widgetProviderInfo.minResizeWidth)
.append("\n")
.append("minResizeHeight: ")
.append(widgetProviderInfo.minResizeHeight)
.append("\n")
.append("defaultWidth: ")
.append(widgetProviderInfo.minWidth)
.append("\n")
.append("defaultHeight: ")
.append(widgetProviderInfo.minHeight)
.append("\n")
widgetDimension
.append("targetCellWidth: ")
.append(widgetProviderInfo.targetCellWidth)
.append("\n")
.append("targetCellHeight: ")
.append(widgetProviderInfo.targetCellHeight)
.append("\n")
.append("maxResizeWidth: ")
.append(widgetProviderInfo.maxResizeWidth)
.append("\n")
.append("maxResizeHeight: ")
.append(widgetProviderInfo.maxResizeHeight)
.append("\n")
FileLog.d(TAG, widgetDimension.toString())
}
}
}