diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 92bb7a0933..9fde8db383 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -17,18 +17,11 @@ package com.android.launcher3.model; import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; -import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed; import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_PROFILE_DELETED; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_SHORTCUT_NOT_FOUND; -import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_WIDGETS_DISABLED; import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL; import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION; @@ -37,16 +30,10 @@ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; -import static com.android.launcher3.util.PackageManagerHelper.isSystemApp; -import android.annotation.SuppressLint; import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -56,12 +43,10 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; -import android.graphics.Point; import android.os.Bundle; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; @@ -69,10 +54,9 @@ import android.util.LongSparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherPrefs; @@ -95,13 +79,11 @@ import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; -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.InstallSessionHelper; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.qsb.QsbContainerView; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult; @@ -113,9 +95,7 @@ import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.WidgetManagerHelper; -import com.android.launcher3.widget.custom.CustomWidgetManager; import java.util.ArrayList; import java.util.Collections; @@ -452,43 +432,19 @@ public class LoaderTask implements Runnable { mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME); try { final LongSparseArray unlockedUsers = new LongSparseArray<>(); - - mUserManagerState.init(mUserCache, mUserManager); - - for (UserHandle user : mUserCache.getUserProfiles()) { - long serialNo = mUserCache.getSerialNumberForUser(user); - boolean userUnlocked = mUserManager.isUserUnlocked(user); - - // We can only query for shortcuts when the user is unlocked. - if (userUnlocked) { - QueryResult pinnedShortcuts = new ShortcutRequest(context, user) - .query(ShortcutRequest.PINNED); - if (pinnedShortcuts.wasSuccess()) { - for (ShortcutInfo shortcut : pinnedShortcuts) { - mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), - shortcut); - } - if (pinnedShortcuts.isEmpty()) { - FileLog.d(TAG, "No pinned shortcuts found for user " + user); - } - } else { - // Shortcut manager can fail due to some race condition when the - // lock state changes too frequently. For the purpose of the loading - // shortcuts, consider the user is still locked. - FileLog.d(TAG, "Shortcut request failed for user " - + user + ", user may still be locked."); - userUnlocked = false; - } - } - unlockedUsers.put(serialNo, userUnlocked); - } + queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers); List> iconRequestInfos = new ArrayList<>(); + WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger, + restoreEventLogger, mUserManagerState, mLauncherApps, mPendingPackages, + mShortcutKeyToPinnedShortcuts, mApp, mIconCache, mBgDataModel, + mWidgetProvidersMap, mIsRestoreFromBackup, installingPkgs, isSdCardReady, + tempPackageKey, widgetHelper, pmHelper, iconRequestInfos, unlockedUsers, + isSafeMode, allDeepShortcuts); + while (!mStopped && c.moveToNext()) { - processWorkspaceItem(c, memoryLogger, restoreEventLogger, installingPkgs, - isSdCardReady, tempPackageKey, widgetHelper, pmHelper, - iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts); + itemProcessor.processItem(); } tryLoadWorkspaceIconsInBulk(iconRequestInfos); } finally { @@ -513,502 +469,85 @@ public class LoaderTask implements Runnable { // Remove dead items mItemsDeleted = c.commitDeleted(); - // Sort the folder items, update ranks, and make sure all preview items are high res. - List verifiers = - mApp.getInvariantDeviceProfile().supportedProfiles.stream().map( - FolderGridOrganizer::new).toList(); - for (FolderInfo folder : mBgDataModel.folders) { - Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); - verifiers.forEach(verifier -> verifier.setFolderInfo(folder)); - int size = folder.contents.size(); - - // Update ranks here to ensure there are no gaps caused by removed folder items. - // Ranks are the source of truth for folder items, so cellX and cellY can be - // ignored for now. Database will be updated once user manually modifies folder. - for (int rank = 0; rank < size; ++rank) { - WorkspaceItemInfo info = folder.contents.get(rank); - // rank is used differently in app pairs, so don't reset - if (folder.itemType != ITEM_TYPE_APP_PAIR) { - info.rank = rank; - } - - if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION - && verifiers.stream().anyMatch( - verifier -> verifier.isItemInPreview(info.rank))) { - mIconCache.getTitleAndIcon(info, false); - } - } - } + processFolderItems(); c.commitRestoredItems(); } } - private void processWorkspaceItem(LoaderCursor c, - LoaderMemoryLogger memoryLogger, - @Nullable LauncherRestoreEventLogger restoreEventLogger, - HashMap installingPkgs, - boolean isSdCardReady, - PackageUserKey tempPackageKey, - WidgetManagerHelper widgetHelper, - PackageManagerHelper pmHelper, - List> iconRequestInfos, - LongSparseArray unlockedUsers, - boolean isSafeMode, - List allDeepShortcuts) { + /** + * Initialized the UserManagerState, and determines which users are unlocked. Additionally, if + * the user is unlocked, it queries LauncherAppsService for pinned shortcuts and stores the + * result in a class variable to be used in other methods while processing workspace items. + * + * @param context used to query LauncherAppsService + * @param unlockedUsers this param is changed, and the updated value is used outside this method + */ + @WorkerThread + private void queryPinnedShortcutsForUnlockedUsers(Context context, + LongSparseArray unlockedUsers) { + mUserManagerState.init(mUserCache, mUserManager); - try { - if (c.user == null) { - // User has been deleted, remove the item. - c.markDeleted("User of this item has been deleted"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_PROFILE_DELETED); + for (UserHandle user : mUserCache.getUserProfiles()) { + long serialNo = mUserCache.getSerialNumberForUser(user); + boolean userUnlocked = mUserManager.isUserUnlocked(user); + + // We can only query for shortcuts when the user is unlocked. + if (userUnlocked) { + QueryResult pinnedShortcuts = new ShortcutRequest(context, user) + .query(ShortcutRequest.PINNED); + if (pinnedShortcuts.wasSuccess()) { + for (ShortcutInfo shortcut : pinnedShortcuts) { + mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), + shortcut); + } + if (pinnedShortcuts.isEmpty()) { + FileLog.d(TAG, "No pinned shortcuts found for user " + user); + } + } else { + // Shortcut manager can fail due to some race condition when the + // lock state changes too frequently. For the purpose of the loading + // shortcuts, consider the user is still locked. + FileLog.d(TAG, "Shortcut request failed for user " + + user + ", user may still be locked."); + userUnlocked = false; } - return; } + unlockedUsers.put(serialNo, userUnlocked); + } - boolean allowMissingTarget = false; - switch (c.itemType) { - case Favorites.ITEM_TYPE_APPLICATION: - case Favorites.ITEM_TYPE_DEEP_SHORTCUT: - Intent intent = c.parseIntent(); - if (intent == null) { - c.markDeleted("Invalid or null intent"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_MISSING_INFO); - } - return; - } + } - int disabledState = mUserManagerState.isUserQuiet(c.serialNumber) - ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0; - ComponentName cn = intent.getComponent(); - String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); + /** + * After all items have been processed and added to the BgDataModel, this method can correctly + * rank items inside folders and load the correct miniature preview icons to be shown when the + * folder is collapsed. + */ + @WorkerThread + private void processFolderItems() { + // Sort the folder items, update ranks, and make sure all preview items are high res. + List verifiers = mApp.getInvariantDeviceProfile().supportedProfiles + .stream().map(FolderGridOrganizer::new).toList(); + for (FolderInfo folder : mBgDataModel.folders) { + Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); + verifiers.forEach(verifier -> verifier.setFolderInfo(folder)); + int size = folder.contents.size(); - if (TextUtils.isEmpty(targetPkg)) { - c.markDeleted("Shortcuts can't have null package"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_MISSING_INFO); - } - return; - } + // Update ranks here to ensure there are no gaps caused by removed folder items. + // Ranks are the source of truth for folder items, so cellX and cellY can be + // ignored for now. Database will be updated once user manually modifies folder. + for (int rank = 0; rank < size; ++rank) { + WorkspaceItemInfo info = folder.contents.get(rank); + // rank is used differently in app pairs, so don't reset + if (folder.itemType != ITEM_TYPE_APP_PAIR) { + info.rank = rank; + } - // If there is no target package, it's an implicit intent - // (legacy shortcut) which is always valid - boolean validTarget = TextUtils.isEmpty(targetPkg) - || mLauncherApps.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 (mLauncherApps.isActivityEnabled(cn, c.user)) { - // no special handling necessary for this item - c.markRestored(); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestored(c.itemType); - } - } else { - // Gracefully try to find a fallback activity. - intent = pmHelper.getAppLaunchIntent(targetPkg, c.user); - if (intent != null) { - c.restoreFlag = 0; - c.updater().put( - Favorites.INTENT, - intent.toUri(0)).commit(); - cn = intent.getComponent(); - } else { - c.markDeleted("Intent null, unable to find a launch target"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_MISSING_INFO); - } - return; - } - } - } - // else if cn == null => can't infer much, leave it - // else if !validPkg => could be restored icon or missing sd-card - - if (!TextUtils.isEmpty(targetPkg) && !validTarget) { - // Points to a valid app (superset of cn != null) but the apk - // is not available. - - if (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); - if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installingPkgs.containsKey(tempPackageKey)) { - // App restore has started. Update the flag - c.restoreFlag |= 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); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED); - } - return; - } - } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { - // Package is present but not available. - disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE; - // Add the icon on the workspace anyway. - allowMissingTarget = true; - } else if (!isSdCardReady) { - // SdCard is not ready yet. Package might get available, - // once it is ready. - Log.d(TAG, "Missing package, will check later: " + targetPkg); - mPendingPackages.add(new 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); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED); - } - return; - } - } - - if ((c.restoreFlag & 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(); - } - - boolean useLowResIcon = !c.isOnWorkspaceOrHotseat(); - - WorkspaceItemInfo info; - if (c.restoreFlag != 0) { - // Already verified above that user is same as default user - info = c.getRestoredItemInfo(intent); - } else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) { - info = c.getAppShortcutInfo( - intent, allowMissingTarget, useLowResIcon, false); - } else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - ShortcutKey key = ShortcutKey.fromIntent(intent, c.user); - if (unlockedUsers.get(c.serialNumber)) { - ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key); - if (pinnedShortcut == null) { - // The shortcut is no longer valid. - c.markDeleted("Pinned shortcut not found from request." - + " package=" + key.getPackageName() + ", user=" + c.user); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_SHORTCUT_NOT_FOUND); - } - return; - } - info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext()); - // If the pinned deep shortcut is no longer published, - // use the last saved icon instead of the default. - mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon); - - if (pmHelper.isAppSuspended( - pinnedShortcut.getPackage(), info.user)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; - } - intent = info.getIntent(); - allDeepShortcuts.add(pinnedShortcut); - } else { - // Create a shortcut info in disabled mode for now. - info = c.loadSimpleWorkspaceItem(); - info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; - } - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestored(c.itemType); - } - } else { // item type == ITEM_TYPE_SHORTCUT - info = c.loadSimpleWorkspaceItem(); - - // Shortcuts are only available on the primary profile - if (!TextUtils.isEmpty(targetPkg) - && pmHelper.isAppSuspended(targetPkg, c.user)) { - disabledState |= FLAG_DISABLED_SUSPENDED; - } - info.options = c.getOptions(); - - // 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.getAction() != null - && intent.getCategories() != null - && intent.getAction().equals(Intent.ACTION_MAIN) - && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | 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.getRank(); - info.spanX = 1; - info.spanY = 1; - info.runtimeStatusFlags |= disabledState; - if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE; - } - LauncherActivityInfo activityInfo = c.getLauncherActivityInfo(); - if (activityInfo != null) { - info.setProgressLevel( - PackageManagerHelper.getLoadingProgress(activityInfo), - PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); - } - - if ((c.restoreFlag != 0 - || (enableSupportForArchiving() - && activityInfo != null - && activityInfo.getApplicationInfo().isArchived)) - && !TextUtils.isEmpty(targetPkg)) { - tempPackageKey.update(targetPkg, c.user); - SessionInfo si = installingPkgs.get(tempPackageKey); - if (si == null) { - info.runtimeStatusFlags - &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; - } else if (activityInfo == null - // For archived apps, include progress info in case there is - // a pending install session post restart of device. - || (enableSupportForArchiving() - && activityInfo.getApplicationInfo().isArchived)) { - int installProgress = (int) (si.getProgress() * 100); - - info.setProgressLevel(installProgress, - PackageInstallInfo.STATUS_INSTALLING); - } - } - - c.checkAndAddItem(info, mBgDataModel, memoryLogger); - } else { - throw new RuntimeException("Unexpected null WorkspaceItemInfo"); - } - break; - - case Favorites.ITEM_TYPE_FOLDER: - case Favorites.ITEM_TYPE_APP_PAIR: - FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id); - c.applyCommonProperties(folderInfo); - - folderInfo.itemType = c.itemType; - // Do not trim the folder label, as is was set by the user. - folderInfo.title = c.getString(c.mTitleIndex); - folderInfo.spanX = 1; - folderInfo.spanY = 1; - folderInfo.options = c.getOptions(); - - // no special handling required for restored folders - c.markRestored(); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestored(c.itemType); - } - c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger); - break; - - case Favorites.ITEM_TYPE_APPWIDGET: - if (WidgetsModel.GO_DISABLE_WIDGETS) { - c.markDeleted("Only legacy shortcuts can have null package"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_WIDGETS_DISABLED); - } - return; - } - // Follow through - case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: - // Read all Launcher-specific widget details - boolean customWidget = c.itemType - == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - - int appWidgetId = c.getAppWidgetId(); - String savedProvider = c.getAppWidgetProvider(); - final ComponentName component; - - if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) { - component = QsbContainerView.getSearchComponentName(mApp.getContext()); - if (component == null) { - c.markDeleted("Discarding SearchWidget without packagename "); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_MISSING_INFO); - } - return; - } - } else { - component = ComponentName.unflattenFromString(savedProvider); - } - final boolean isIdValid = - !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); - final boolean wasProviderReady = - !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY); - - ComponentKey providerKey = new ComponentKey(component, c.user); - if (!mWidgetProvidersMap.containsKey(providerKey)) { - if (customWidget) { - mWidgetProvidersMap.put(providerKey, CustomWidgetManager.INSTANCE - .get(mApp.getContext()).getWidgetProvider(component)); - } else { - mWidgetProvidersMap.put(providerKey, - widgetHelper.findProvider(component, c.user)); - } - } - final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey); - - final boolean isProviderReady = isValidProvider(provider); - if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) { - c.markDeleted("Deleting widget that isn't installed anymore: " + provider); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED); - } - } else { - LauncherAppWidgetInfo appWidgetInfo; - if (isProviderReady) { - appWidgetInfo = - new LauncherAppWidgetInfo(appWidgetId, provider.provider); - - // The provider is available. So the widget is either - // available or not available. We do not need to track - // any future restore updates. - int status = c.restoreFlag - & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED - & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; - if (!wasProviderReady) { - // If provider was not previously ready, update status and UI flag. - - // Id would be valid only if the widget restore broadcast received. - if (isIdValid) { - status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - } - } - appWidgetInfo.restoreStatus = status; - } else { - Log.v(TAG, "Widget restore pending id=" + c.id - + " appWidgetId=" + appWidgetId - + " status=" + c.restoreFlag); - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component); - appWidgetInfo.restoreStatus = c.restoreFlag; - - tempPackageKey.update(component.getPackageName(), c.user); - SessionInfo si = installingPkgs.get(tempPackageKey); - Integer installProgress = si == null - ? null - : (int) (si.getProgress() * 100); - - if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installProgress != null) { - // App restore has started. Update the flag - appWidgetInfo.restoreStatus - |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; - } else if (!isSafeMode) { - c.markDeleted("Unrestored widget removed: " + component); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED); - } - return; - } - - appWidgetInfo.installProgress = - installProgress == null ? 0 : installProgress; - } - if (appWidgetInfo.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { - appWidgetInfo.bindOptions = c.parseIntent(); - } - - c.applyCommonProperties(appWidgetInfo); - appWidgetInfo.spanX = c.getSpanX(); - appWidgetInfo.spanY = c.getSpanY(); - appWidgetInfo.options = c.getOptions(); - appWidgetInfo.user = c.user; - appWidgetInfo.sourceContainer = c.getAppWidgetSource(); - - if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { - c.markDeleted("Widget has invalid size: " - + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_INVALID_LOCATION); - } - return; - } - LauncherAppWidgetProviderInfo widgetProviderInfo = - widgetHelper.getLauncherAppWidgetInfo(appWidgetId, - appWidgetInfo.getTargetComponent()); - if (widgetProviderInfo != null - && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX - || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) { - FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent() - + " minSizes not meet: span=" + appWidgetInfo.spanX - + "x" + appWidgetInfo.spanY + " minSpan=" - + widgetProviderInfo.minSpanX + "x" - + widgetProviderInfo.minSpanY); - logWidgetInfo(mApp.getInvariantDeviceProfile(), widgetProviderInfo); - } - if (!c.isOnWorkspaceOrHotseat()) { - c.markDeleted("Widget found where container != CONTAINER_DESKTOP" - + "nor CONTAINER_HOTSEAT - ignoring!"); - if (mIsRestoreFromBackup && restoreEventLogger != null) { - restoreEventLogger.logSingleFavoritesItemRestoreFailed( - c.itemType, RESTORE_ERROR_INVALID_LOCATION); - } - return; - } - - if (!customWidget) { - String providerName = appWidgetInfo.providerName.flattenToString(); - if (!providerName.equals(savedProvider) - || (appWidgetInfo.restoreStatus != c.restoreFlag)) { - c.updater() - .put(Favorites.APPWIDGET_PROVIDER, - providerName) - .put(Favorites.RESTORED, - appWidgetInfo.restoreStatus) - .commit(); - } - } - - if (appWidgetInfo.restoreStatus - != LauncherAppWidgetInfo.RESTORE_COMPLETED) { - appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo( - mApp.getContext(), - appWidgetInfo.providerName, - appWidgetInfo.user); - mIconCache.getTitleAndIconForApp( - appWidgetInfo.pendingItemInfo, false); - } - - c.checkAndAddItem(appWidgetInfo, mBgDataModel); - } - break; + if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION + && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) { + mIconCache.getTitleAndIcon(info, false); + } } - } catch (Exception e) { - Log.e(TAG, "Desktop items loading interrupted", e); } } @@ -1206,52 +745,6 @@ public class LoaderTask implements Runnable { && (provider.provider.getPackageName() != null); } - @SuppressLint("NewApi") // Already added API check. - private static void logWidgetInfo(InvariantDeviceProfile idp, - LauncherAppWidgetProviderInfo widgetProviderInfo) { - Point cellSize = new Point(); - for (DeviceProfile deviceProfile : 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); - } - - StringBuilder widgetDimension = new 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"); - if (Utilities.ATLEAST_S) { - 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()); - } - private static void logASplit(String label) { if (DEBUG) { Log.d(TAG, label); diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt new file mode 100644 index 0000000000..5389d38ab9 --- /dev/null +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -0,0 +1,672 @@ +/* + * 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 androidx.annotation.VisibleForTesting +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.Utilities +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger +import com.android.launcher3.icons.IconCache +import com.android.launcher3.logging.FileLog +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.qsb.QsbContainerView +import com.android.launcher3.shortcuts.ShortcutKey +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.WidgetManagerHelper +import com.android.launcher3.widget.custom.CustomWidgetManager + +/** + * 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 restoreEventLogger: LauncherRestoreEventLogger?, + private val userManagerState: UserManagerState, + private val launcherApps: LauncherApps, + private val pendingPackages: MutableSet, + private val shortcutKeyToPinnedShortcuts: Map, + private val app: LauncherAppState, + private val iconCache: IconCache, + private val bgDataModel: BgDataModel, + private val widgetProvidersMap: MutableMap, + private val isRestoreFromBackup: Boolean, + private val installingPkgs: HashMap, + private val isSdCardReady: Boolean, + private val tempPackageKey: PackageUserKey, + private val widgetHelper: WidgetManagerHelper, + private val pmHelper: PackageManagerHelper, + private val iconRequestInfos: MutableList>, + private val unlockedUsers: LongSparseArray, + private val isSafeMode: Boolean, + private val allDeepShortcuts: MutableList +) { + /** + * 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") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_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 launcher’s 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 + * user’s 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 launcher’s data model. + */ + @SuppressLint("NewApi") + @VisibleForTesting + fun processAppOrDeepShortcut() { + var allowMissingTarget = false + var intent = c.parseIntent() + if (intent == null) { + c.markDeleted("Invalid or null intent") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO + ) + } + return + } + var disabledState = + if (userManagerState.isUserQuiet(c.serialNumber)) + WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER + else 0 + var cn = intent.component + val targetPkg = if (cn == null) intent.getPackage() else cn.packageName + if (TextUtils.isEmpty(targetPkg)) { + c.markDeleted("Shortcuts can't have null package") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO + ) + } + return + } + + // If there is no target package, it's an implicit intent + // (legacy shortcut) which is always valid + var validTarget = + (TextUtils.isEmpty(targetPkg) || 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() + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType) + } + } else { + // Gracefully try to find a fallback activity. + intent = pmHelper.getAppLaunchIntent(targetPkg, c.user) + if (intent != null) { + c.restoreFlag = 0 + c.updater().put(Favorites.INTENT, intent.toUri(0)).commit() + } else { + c.markDeleted("Intent null, unable to find a launch target") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO + ) + } + return + } + } + } + // 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" + ) + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED + ) + } + return + } + } + } + pmHelper.isAppOnSdcard(targetPkg!!, c.user) -> { + // 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") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED + ) + } + 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}" + ) + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_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. + iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon) + if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) { + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED + } + intent = info.getIntent() + allDeepShortcuts.add(pinnedShortcut) + } else { + // Create a shortcut info in disabled mode for now. + info = c.loadSimpleWorkspaceItem() + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER + } + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType) + } + } + else -> { // item type == ITEM_TYPE_SHORTCUT + info = c.loadSimpleWorkspaceItem() + + // Shortcuts are only available on the primary profile + if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg!!, c.user)) { + 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 && !PackageManagerHelper.isSystemApp(app.context, intent)) { + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE + } + val activityInfo = c.launcherActivityInfo + if (activityInfo != null) { + info.setProgressLevel( + PackageManagerHelper.getLoadingProgress(activityInfo), + PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING + ) + } + 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 // For archived apps, include progress info in case there is + // a pending install session post restart of device. + || + (Flags.enableSupportForArchiving() && + activityInfo.applicationInfo.isArchived) + ) { + val installProgress = (si.getProgress() * 100).toInt() + info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING) + } + } + c.checkAndAddItem(info, bgDataModel, memoryLogger) + } else { + throw RuntimeException("Unexpected null WorkspaceItemInfo") + } + } + + /** + * Loads the folder information from the database and formats it into a FolderInfo. Some of the + * processing for folder content items is done in LoaderTask after all the items in the + * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel. + */ + @VisibleForTesting + fun processFolderOrAppPair() { + val folderInfo = + bgDataModel.findOrMakeFolder(c.id).apply { + c.applyCommonProperties(this) + itemType = c.itemType + // Do not trim the folder label, as is was set by the user. + title = c.getString(c.mTitleIndex) + spanX = 1 + spanY = 1 + options = c.options + } + + // no special handling required for restored folders + c.markRestored() + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType) + } + c.checkAndAddItem(folderInfo, 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 launcher’s 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 user’s 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 Widget’s 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. + */ + @VisibleForTesting + fun processWidget() { + if (WidgetsModel.GO_DISABLE_WIDGETS && c.itemType == Favorites.ITEM_TYPE_APPWIDGET) { + c.markDeleted("Only legacy shortcuts can have null package") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_WIDGETS_DISABLED + ) + } + return + } + + // Read all Launcher-specific widget details + val customWidget = (c.itemType == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET) + val appWidgetId = c.appWidgetId + val savedProvider = c.appWidgetProvider + val component: ComponentName? + if (c.options and LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET != 0) { + component = QsbContainerView.getSearchComponentName(app.context) + if (component == null) { + c.markDeleted("Discarding SearchWidget without package name") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO + ) + } + return + } + } else { + component = ComponentName.unflattenFromString(savedProvider) + } + val isIdValid = !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) + val wasProviderReady = !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) + val providerKey = ComponentKey(component, c.user) + if (!widgetProvidersMap.containsKey(providerKey)) { + if (customWidget) { + widgetProvidersMap[providerKey] = + CustomWidgetManager.INSTANCE[app.context].getWidgetProvider(component) + } else { + widgetProvidersMap[providerKey] = widgetHelper.findProvider(component, c.user) + } + } + val provider = widgetProvidersMap[providerKey] + val isProviderReady = LoaderTask.isValidProvider(provider) + if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) { + c.markDeleted("Deleting widget that isn't installed anymore: $provider") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED + ) + } + } else { + val appWidgetInfo: LauncherAppWidgetInfo + if (isProviderReady) { + appWidgetInfo = LauncherAppWidgetInfo(appWidgetId, provider!!.provider) + + // The provider is available. So the widget is either + // available or not available. We do not need to track + // any future restore updates. + var status = + (c.restoreFlag and + LauncherAppWidgetInfo.FLAG_RESTORE_STARTED.inv() and + LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY.inv()) + if (!wasProviderReady) { + // If provider was not previously ready, update status and UI flag. + + // Id would be valid only if the widget restore broadcast received. + if (isIdValid) { + status = status or LauncherAppWidgetInfo.FLAG_UI_NOT_READY + } + } + appWidgetInfo.restoreStatus = status + } else { + Log.v( + TAG, + "Widget restore pending id=${c.id} appWidgetId=$appWidgetId status=${c.restoreFlag}" + ) + appWidgetInfo = LauncherAppWidgetInfo(appWidgetId, component) + appWidgetInfo.restoreStatus = c.restoreFlag + tempPackageKey.update(component!!.packageName, c.user) + val si = installingPkgs[tempPackageKey] + val installProgress = if (si == null) null else (si.getProgress() * 100).toInt() + when { + c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) -> { + // Restore has started once. + } + installProgress != null -> { + // App restore has started. Update the flag + appWidgetInfo.restoreStatus = + appWidgetInfo.restoreStatus or + LauncherAppWidgetInfo.FLAG_RESTORE_STARTED + } + !isSafeMode -> { + c.markDeleted("Unrestored widget removed: $component") + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED + ) + } + return + } + } + appWidgetInfo.installProgress = installProgress ?: 0 + } + if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { + appWidgetInfo.bindOptions = c.parseIntent() + } + c.applyCommonProperties(appWidgetInfo) + appWidgetInfo.spanX = c.spanX + appWidgetInfo.spanY = c.spanY + appWidgetInfo.options = c.options + appWidgetInfo.user = c.user + appWidgetInfo.sourceContainer = c.appWidgetSource + if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { + c.markDeleted( + "Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}" + ) + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION + ) + } + return + } + val widgetProviderInfo = + widgetHelper.getLauncherAppWidgetInfo(appWidgetId, appWidgetInfo.targetComponent) + if ( + widgetProviderInfo != null && + (appWidgetInfo.spanX < widgetProviderInfo.minSpanX || + appWidgetInfo.spanY < widgetProviderInfo.minSpanY) + ) { + FileLog.d( + TAG, + "Widget ${widgetProviderInfo.component} minSizes not meet:" + + " span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY}" + + " minSpan=${widgetProviderInfo.minSpanX}x${widgetProviderInfo.minSpanY}" + ) + logWidgetInfo(app.invariantDeviceProfile, widgetProviderInfo) + } + if (!c.isOnWorkspaceOrHotseat) { + c.markDeleted( + "Widget found where container != CONTAINER_DESKTOP" + + " nor CONTAINER_HOTSEAT - ignoring!" + ) + if (isRestoreFromBackup) { + restoreEventLogger?.logSingleFavoritesItemRestoreFailed( + c.itemType, + LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION + ) + } + return + } + if (!customWidget) { + val providerName = appWidgetInfo.providerName.flattenToString() + if (providerName != savedProvider || appWidgetInfo.restoreStatus != c.restoreFlag) { + c.updater() + .put(Favorites.APPWIDGET_PROVIDER, providerName) + .put(Favorites.RESTORED, appWidgetInfo.restoreStatus) + .commit() + } + } + if (appWidgetInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) { + appWidgetInfo.pendingItemInfo = + WidgetsModel.newPendingItemInfo( + app.context, + appWidgetInfo.providerName, + appWidgetInfo.user + ) + iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false) + } + c.checkAndAddItem(appWidgetInfo, bgDataModel) + } + } + + companion object { + private val TAG = WorkspaceItemProcessor::class.java.simpleName + 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") + if (Utilities.ATLEAST_S) { + 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()) + } + } +}