diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index f905c5f4fc..6815f97e16 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -25,7 +25,6 @@ import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; @@ -39,9 +38,9 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Predicate; /** @@ -114,15 +113,9 @@ public class TaskbarModelCallbacks implements return modified; } - @Override - public void bindWorkspaceItemsChanged(List updated) { - updateWorkspaceItems(updated, mContext); - } - - @Override - public void bindRestoreItemsChange(HashSet updates) { - updateRestoreItems(updates, mContext); + public void bindItemsUpdated(Set updates) { + updateContainerItems(updates, mContext); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index a85e5e0049..9b7bb1c9cd 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -214,7 +214,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { boolean animate = shouldAnimateIconChange(info); Drawable oldIcon = getIcon(); int oldPlateColor = mPlateColor.currentColor; - applyFromWorkspaceItem(info, null); + applyFromWorkspaceItem(info); setContentDescription( mIsPinned ? info.contentDescription : diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 315096c841..d3684b25e6 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -29,6 +29,7 @@ import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -63,6 +64,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.TextView; import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; @@ -366,11 +368,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mDotScaleAnim.start(); } - @UiThread - public void applyFromWorkspaceItem(WorkspaceItemInfo info) { - applyFromWorkspaceItem(info, null); - } - @Override public void setAccessibilityDelegate(AccessibilityDelegate delegate) { if (delegate instanceof BaseAccessibilityDelegate) { @@ -384,10 +381,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } @UiThread - public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) { + public void applyFromWorkspaceItem(WorkspaceItemInfo info) { applyIconAndLabel(info); setItemInfo(info); - applyLoadingState(icon); + applyDotState(info, false /* animate */); setDownloadStateContentDescription(info, info.getProgressLevel()); } @@ -395,17 +392,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @UiThread public void applyFromApplicationInfo(AppInfo info) { applyIconAndLabel(info); - - // We don't need to check the info since it's not a WorkspaceItemInfo setItemInfo(info); - // Verify high res immediately verifyHighRes(); - if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { - applyProgressLevel(); - } applyDotState(info, false /* animate */); setDownloadStateContentDescription(info, info.getProgressLevel()); } @@ -449,6 +440,50 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @VisibleForTesting @UiThread public void applyIconAndLabel(ItemInfoWithIcon info) { + FastBitmapDrawable oldIcon = mIcon; + if (!canReuseIcon(info)) { + setNonPendingIcon(info); + } + applyLabel(info); + maybeApplyProgressLevel(info, oldIcon); + } + + /** + * Check if we can reuse icon so that any animation is preserved + */ + private boolean canReuseIcon(ItemInfoWithIcon info) { + return mIcon instanceof PreloadIconDrawable p + && p.hasNotCompleted() && p.isSameInfo(info.bitmap); + } + + /** + * Apply progress level to the icon if necessary + */ + private void maybeApplyProgressLevel(ItemInfoWithIcon info, FastBitmapDrawable oldIcon) { + if (!shouldApplyProgressLevel(info, oldIcon)) { + return; + } + PreloadIconDrawable pendingIcon = applyProgressLevel(info); + boolean isNoLongerPending = info instanceof WorkspaceItemInfo wii + ? !wii.hasPromiseIconUi() : !info.isArchived(); + if (isNoLongerPending && info.getProgressLevel() == 100 && pendingIcon != null) { + pendingIcon.maybePerformFinishedAnimation( + (oldIcon instanceof PreloadIconDrawable p) ? p : pendingIcon, + () -> setNonPendingIcon( + (getTag() instanceof ItemInfoWithIcon iiwi) ? iiwi : info)); + } + } + + /** + * Check if progress level should be applied to the icon + */ + private boolean shouldApplyProgressLevel(ItemInfoWithIcon info, FastBitmapDrawable oldIcon) { + return (info.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0 + || (info instanceof WorkspaceItemInfo wii && wii.hasPromiseIconUi()) + || (oldIcon instanceof PreloadIconDrawable p && p.hasNotCompleted()); + } + + private void setNonPendingIcon(ItemInfoWithIcon info) { ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext()); int flags = (shouldUseTheme() && themeManager.isMonoThemeEnabled()) ? FLAG_THEMED : 0; @@ -463,7 +498,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mDotParams.appColor = iconDrawable.getIconColor(); mDotParams.dotColor = Themes.getAttrColor(getContext(), R.attr.notificationDotColor); setIcon(iconDrawable); - applyLabel(info); } protected boolean shouldUseTheme() { @@ -1070,38 +1104,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mLongPressHelper.cancelLongPress(); } - /** - * Applies the loading progress value to the progress bar. - * - * If this app is installing, the progress bar will be updated with the installation progress. - * If this app is installed and downloading incrementally, the progress bar will be updated - * with the total download progress. - */ - public void applyLoadingState(PreloadIconDrawable icon) { - if (getTag() instanceof ItemInfoWithIcon) { - WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); - if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0 - || info.hasPromiseIconUi() - || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0 - || (icon != null)) { - updateProgressBarUi(info.getProgressLevel() == 100 ? icon : null); - } - } - } - - private void updateProgressBarUi(PreloadIconDrawable oldIcon) { - FastBitmapDrawable originalIcon = mIcon; - PreloadIconDrawable preloadDrawable = applyProgressLevel(); - if (preloadDrawable != null && oldIcon != null) { - preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon)); - } - } - /** Applies the given progress level to the this icon's progress bar. */ @Nullable - public PreloadIconDrawable applyProgressLevel() { - if (!(getTag() instanceof ItemInfoWithIcon info) - || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) { + private PreloadIconDrawable applyProgressLevel(ItemInfoWithIcon info) { + if (info.isInactiveArchive()) { return null; } @@ -1115,23 +1121,16 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, setContentDescription(getContext() .getString(R.string.app_waiting_download_title, info.title)); } - if (mIcon != null) { - PreloadIconDrawable preloadIconDrawable; - if (mIcon instanceof PreloadIconDrawable) { - preloadIconDrawable = (PreloadIconDrawable) mIcon; - preloadIconDrawable.setLevel(progressLevel); - preloadIconDrawable.setIsDisabled(isIconDisabled(info)); - } else { - preloadIconDrawable = makePreloadIcon(); - setIcon(preloadIconDrawable); - if (info.isArchived() && Flags.useNewIconForArchivedApps()) { - // reapply text without cloud icon as soon as unarchiving is triggered - applyLabel(info); - } - } - return preloadIconDrawable; + PreloadIconDrawable pid; + if (mIcon instanceof PreloadIconDrawable p) { + pid = p; + pid.setLevel(progressLevel); + pid.setIsDisabled(isIconDisabled(info)); + } else { + pid = makePreloadIcon(info); + setIcon(pid); } - return null; + return pid; } /** @@ -1140,11 +1139,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, */ @Nullable public PreloadIconDrawable makePreloadIcon() { - if (!(getTag() instanceof ItemInfoWithIcon)) { - return null; - } + return getTag() instanceof ItemInfoWithIcon info ? makePreloadIcon(info) : null; + } - ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); + @NonNull + private PreloadIconDrawable makePreloadIcon(ItemInfoWithIcon info) { int progressLevel = info.getProgressLevel(); final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info); @@ -1163,7 +1162,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, public void applyDotState(ItemInfo itemInfo, boolean animate) { - if (mIcon instanceof FastBitmapDrawable) { + if (mIcon != null) { boolean wasDotted = mDotInfo != null; mDotInfo = mActivity.getDotInfoForItem(itemInfo); boolean isDotted = mDotInfo != null; @@ -1212,7 +1211,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, setContentDescription(getContext().getString( R.string.app_archived_title, info.title)); } - } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) + } else if ((info.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { String percentageString = NumberFormat.getPercentInstance() .format(progressLevel * 0.01); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 7df4014fc0..647d2ad97b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -277,11 +277,11 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -2598,25 +2598,12 @@ public class Launcher extends StatefulActivity mModelCallbacks.bindIncrementalDownloadProgressUpdated(app); } - @Override - public void bindWidgetsRestored(ArrayList widgets) { - mModelCallbacks.bindWidgetsRestored(widgets); - } - /** * See {@code LauncherBindingDelegate} */ @Override - public void bindWorkspaceItemsChanged(List updated) { - mModelCallbacks.bindWorkspaceItemsChanged(updated); - } - - /** - * See {@code LauncherBindingDelegate} - */ - @Override - public void bindRestoreItemsChange(HashSet updates) { - mModelCallbacks.bindRestoreItemsChange(updates); + public void bindItemsUpdated(Set updates) { + mModelCallbacks.bindItemsUpdated(updates); } /** diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt index 5d3252514e..5338fb435f 100644 --- a/src/com/android/launcher3/ModelCallbacks.kt +++ b/src/com/android/launcher3/ModelCallbacks.kt @@ -17,8 +17,6 @@ import com.android.launcher3.model.BgDataModel import com.android.launcher3.model.StringCache import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.LauncherAppWidgetInfo -import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.popup.PopupContainerWithArrow import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.IntArray as LIntArray @@ -215,29 +213,13 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { launcher.appsView.appsStore.updateProgressBar(app) } - override fun bindWidgetsRestored(widgets: ArrayList?) { - launcher.workspace.widgetsRestored(widgets) - } - - /** - * Some shortcuts were updated in the background. Implementation of the method from - * LauncherModel.Callbacks. - * - * @param updated list of shortcuts which have changed. - */ - override fun bindWorkspaceItemsChanged(updated: List) { - if (updated.isNotEmpty()) { - launcher.workspace.updateWorkspaceItems(updated, launcher) - PopupContainerWithArrow.dismissInvalidPopup(launcher) - } - } - /** * Update the state of a package, typically related to install state. Implementation of the * method from LauncherModel.Callbacks. */ - override fun bindRestoreItemsChange(updates: HashSet?) { - launcher.workspace.updateRestoreItems(updates, launcher) + override fun bindItemsUpdated(updates: Set) { + launcher.workspace.updateContainerItems(updates, launcher) + PopupContainerWithArrow.dismissInvalidPopup(launcher) } /** diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 86c49d0e47..7d82179d43 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -53,8 +53,6 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Message; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; @@ -125,13 +123,9 @@ import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.WallpaperOffsetInterpolator; import com.android.launcher3.widget.LauncherAppWidgetHostView; -import com.android.launcher3.widget.LauncherWidgetHolder; -import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener; import com.android.launcher3.widget.NavigableAppWidgetHostView; import com.android.launcher3.widget.PendingAddShortcutInfo; import com.android.launcher3.widget.PendingAddWidgetInfo; -import com.android.launcher3.widget.PendingAppWidgetHostView; -import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.util.WidgetSizes; import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks; import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy; @@ -664,9 +658,6 @@ public class Workspace extends PagedView bindAndInitFirstWorkspaceScreen(); } - // Remove any deferred refresh callbacks - mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class); - // Re-enable the layout transitions enableLayoutTransitions(); } @@ -3465,43 +3456,6 @@ public class Workspace extends PagedView removeItemsByMatcher(matcher); } - public void widgetsRestored(final ArrayList changedInfo) { - if (!changedInfo.isEmpty()) { - DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo, - mLauncher.getAppWidgetHolder()); - - LauncherAppWidgetInfo item = changedInfo.get(0); - final AppWidgetProviderInfo widgetInfo; - WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext()); - if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { - widgetInfo = widgetHelper.findProvider(item.providerName, item.user); - } else { - widgetInfo = widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId, - item.getTargetComponent()); - } - - if (widgetInfo != null) { - // Re-inflate the widgets which have changed status - widgetRefresh.run(); - } else { - // widgetRefresh will automatically run when the packages are updated. - // For now just update the progress bars - mapOverItems(new ItemOperator() { - @Override - public boolean evaluate(ItemInfo info, View view) { - if (view instanceof PendingAppWidgetHostView - && changedInfo.contains(info)) { - ((LauncherAppWidgetInfo) info).installProgress = 100; - ((PendingAppWidgetHostView) view).applyState(); - } - // process all the shortcuts - return false; - } - }); - } - } - } - public boolean isOverlayShown() { return mOverlayShown; } @@ -3608,62 +3562,6 @@ public class Workspace extends PagedView return mLauncher.getCellPosMapper(); } - /** - * Used as a workaround to ensure that the AppWidgetService receives the - * PACKAGE_ADDED broadcast before updating widgets. - */ - private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener { - private final ArrayList mInfos; - private final LauncherWidgetHolder mWidgetHolder; - private final Handler mHandler; - - private boolean mRefreshPending; - - DeferredWidgetRefresh(ArrayList infos, - LauncherWidgetHolder holder) { - mInfos = infos; - mWidgetHolder = holder; - mHandler = mLauncher.mHandler; - mRefreshPending = true; - - mWidgetHolder.addProviderChangeListener(this); - // Force refresh after 10 seconds, if we don't get the provider changed event. - // This could happen when the provider is no longer available in the app. - Message msg = Message.obtain(mHandler, this); - msg.obj = DeferredWidgetRefresh.class; - mHandler.sendMessageDelayed(msg, 10000); - } - - @Override - public void run() { - mWidgetHolder.removeProviderChangeListener(this); - mHandler.removeCallbacks(this); - - if (!mRefreshPending) { - return; - } - - mRefreshPending = false; - - ArrayList views = new ArrayList<>(mInfos.size()); - mapOverItems((info, view) -> { - if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) { - views.add((PendingAppWidgetHostView) view); - } - // process all children - return false; - }); - for (PendingAppWidgetHostView view : views) { - view.reInflate(); - } - } - - @Override - public void notifyWidgetProvidersChanged() { - run(); - } - } - private class StateTransitionListener extends AnimatorListenerAdapter implements AnimatorUpdateListener { diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java index 9afe06c6f7..d5a4022792 100644 --- a/src/com/android/launcher3/allapps/AllAppsStore.java +++ b/src/com/android/launcher3/allapps/AllAppsStore.java @@ -17,7 +17,6 @@ package com.android.launcher3.allapps; import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK; import android.content.Context; import android.os.UserHandle; @@ -229,11 +228,7 @@ public class AllAppsStore { public void updateProgressBar(AppInfo app) { updateAllIcons((child) -> { if (child.getTag() == app) { - if ((app.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) == 0) { - child.applyFromApplicationInfo(app); - } else { - child.applyProgressLevel(); - } + child.applyFromApplicationInfo(app); } }); } diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java index 3464e9bdb6..f1891821b7 100644 --- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -24,7 +24,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; @@ -33,12 +32,14 @@ import android.graphics.PathMeasure; import android.graphics.Rect; import android.util.Property; +import androidx.annotation.VisibleForTesting; import androidx.core.graphics.ColorUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.util.Themes; @@ -63,8 +64,6 @@ public class PreloadIconDrawable extends FastBitmapDrawable { private static final int DEFAULT_PATH_SIZE = 100; private static final int MAX_PAINT_ALPHA = 255; - private static final int TRACK_ALPHA = (int) (0.27f * MAX_PAINT_ALPHA); - private static final int DISABLED_ICON_ALPHA = (int) (0.6f * MAX_PAINT_ALPHA); private static final long DURATION_SCALE = 500; private static final long SCALE_AND_ALPHA_ANIM_DURATION = 500; @@ -284,20 +283,25 @@ public class PreloadIconDrawable extends FastBitmapDrawable { (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); mCurrentAnim.setInterpolator(LINEAR); if (isFinish) { - if (onFinishCallback != null) { - mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback)); - } mCurrentAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mRanFinishAnimation = true; } }); + if (onFinishCallback != null) { + mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback)); + } } mCurrentAnim.start(); } } + @VisibleForTesting + public ObjectAnimator getActiveAnimation() { + return mCurrentAnim; + } + /** * Sets the internal progress and updates the UI accordingly * for progress <= 0: @@ -358,8 +362,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable { @Override public FastBitmapConstantState newConstantState() { return new PreloadIconConstantState( - mBitmap, - mIconColor, + mBitmapInfo, mItem, mIndicatorColor, new int[] {mSystemAccentColor, mSystemBackgroundColor}, @@ -377,14 +380,13 @@ public class PreloadIconDrawable extends FastBitmapDrawable { private final Path mShapePath; public PreloadIconConstantState( - Bitmap bitmap, - int iconColor, + BitmapInfo bitmapInfo, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode, Path shapePath) { - super(bitmap, iconColor); + super(bitmapInfo); mInfo = info; mIndicatorColor = indicatorColor; mPreloadColors = preloadColors; diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index a04cbfb27b..ddc775dd9c 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -49,7 +49,6 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; @@ -70,7 +69,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -419,9 +417,9 @@ public class BgDataModel { * Binds updated incremental download progress */ default void bindIncrementalDownloadProgressUpdated(AppInfo app) { } - default void bindWorkspaceItemsChanged(List updated) { } - default void bindWidgetsRestored(ArrayList widgets) { } - default void bindRestoreItemsChange(HashSet updates) { } + + /** Called when a runtime property of the ItemInfo is updated due to some system event */ + default void bindItemsUpdated(Set updates) { } default void bindWorkspaceComponentsRemoved(Predicate matcher) { } /** diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java index b544b91382..48934e2c11 100644 --- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java +++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java @@ -15,6 +15,9 @@ */ package com.android.launcher3.model; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; +import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER; + import android.content.ComponentName; import android.os.UserHandle; @@ -23,6 +26,8 @@ import androidx.annotation.NonNull; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.LauncherSettings; import com.android.launcher3.icons.IconCache; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import java.util.ArrayList; @@ -55,7 +60,7 @@ public class CacheDataUpdatedTask implements ModelUpdateTask { public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) { IconCache iconCache = taskController.getApp().getIconCache(); - ArrayList updatedShortcuts = new ArrayList<>(); + ArrayList updatedItems = new ArrayList<>(); synchronized (dataModel) { dataModel.forAllWorkspaceItemInfos(mUser, si -> { @@ -64,12 +69,25 @@ public class CacheDataUpdatedTask implements ModelUpdateTask { && isValidShortcut(si) && cn != null && mPackages.contains(cn.getPackageName())) { iconCache.getTitleAndIcon(si, si.getMatchingLookupFlag()); - updatedShortcuts.add(si); + updatedItems.add(si); } }); + + dataModel.itemsIdMap.stream() + .filter(WIDGET_FILTER) + .filter(item -> mUser.equals(item.user)) + .map(item -> (LauncherAppWidgetInfo) item) + .filter(widget -> mPackages.contains(widget.providerName.getPackageName()) + && widget.pendingItemInfo != null) + .forEach(widget -> { + iconCache.getTitleAndIconForApp( + widget.pendingItemInfo, DEFAULT_LOOKUP_FLAG); + updatedItems.add(widget); + }); + apps.updateIconsAndLabels(mPackages, mUser); } - taskController.bindUpdatedWorkspaceItems(updatedShortcuts); + taskController.bindUpdatedWorkspaceItems(updatedItems); taskController.bindApplicationsIfNeeded(); } diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt index fc5334341b..40ea17d437 100644 --- a/src/com/android/launcher3/model/ModelTaskController.kt +++ b/src/com/android/launcher3/model/ModelTaskController.kt @@ -22,7 +22,6 @@ import com.android.launcher3.LauncherModel.CallbackTask import com.android.launcher3.celllayout.CellPosMapper import com.android.launcher3.model.BgDataModel.FixedContainerItems import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.util.PackageUserKey import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder import java.util.Objects @@ -51,18 +50,17 @@ class ModelTaskController( */ fun getModelWriter() = model.getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null) - fun bindUpdatedWorkspaceItems(allUpdates: List) { + fun bindUpdatedWorkspaceItems(allUpdates: Collection) { // Bind workspace items - val workspaceUpdates = - allUpdates.stream().filter { info -> info.id != ItemInfo.NO_ID }.toList() + val workspaceUpdates = allUpdates.filter { it.id != ItemInfo.NO_ID }.toSet() if (workspaceUpdates.isNotEmpty()) { - scheduleCallbackTask { it.bindWorkspaceItemsChanged(workspaceUpdates) } + scheduleCallbackTask { it.bindItemsUpdated(workspaceUpdates) } } // Bind extra items if any allUpdates .stream() - .mapToInt { info: WorkspaceItemInfo -> info.container } + .mapToInt { it.container } .distinct() .mapToObj { dataModel.extraItems.get(it) } .filter { Objects.nonNull(it) } diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java index 4103937445..a2160422b2 100644 --- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java +++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java @@ -99,8 +99,7 @@ public class PackageInstallStateChangedTask implements ModelUpdateTask { }); if (!updates.isEmpty()) { - taskController.scheduleCallbackTask( - callbacks -> callbacks.bindRestoreItemsChange(updates)); + taskController.bindUpdatedWorkspaceItems(updates); } } } diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 1153f487a0..6bef292841 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -214,8 +214,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { // Update shortcut infos if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { - final ArrayList updatedWorkspaceItems = new ArrayList<>(); - final ArrayList widgets = new ArrayList<>(); + final ArrayList updatedWorkspaceItems = new ArrayList<>(); // For system apps, package manager send OP_UPDATE when an app is enabled. final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; @@ -364,8 +363,8 @@ public class PackageUpdatedTask implements ModelUpdateTask { // if the widget has a config activity. In case there is no config // activity, it will be marked as 'restored' during bind. widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - - widgets.add(widgetInfo); + widgetInfo.installProgress = 100; + updatedWorkspaceItems.add(widgetInfo); taskController.getModelWriter().updateItemInDatabase(widgetInfo); }); } @@ -377,10 +376,6 @@ public class PackageUpdatedTask implements ModelUpdateTask { "removing shortcuts with invalid target components." + " ids=" + removedShortcuts); } - - if (!widgets.isEmpty()) { - taskController.scheduleCallbackTask(c -> c.bindWidgetsRestored(widgets)); - } } final HashSet removedPackages = new HashSet<>(); diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index 78709b84c8..381d17accb 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -228,10 +228,9 @@ public class ItemClickHandler { private static void onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted) { ItemInfo item = (ItemInfo) v.getTag(); - CompletableFuture siFuture; - siFuture = CompletableFuture.supplyAsync(() -> - InstallSessionHelper.INSTANCE.get(launcher) - .getActiveSessionInfo(item.user, packageName), + CompletableFuture siFuture = CompletableFuture.supplyAsync(() -> + InstallSessionHelper.INSTANCE.get(launcher) + .getActiveSessionInfo(item.user, packageName), UI_HELPER_EXECUTOR); Consumer marketLaunchAction = sessionInfo -> { if (sessionInfo != null) { @@ -245,8 +244,8 @@ public class ItemClickHandler { } } // Fallback to using custom market intent. - Intent intent = ApiWrapper.INSTANCE.get(launcher).getAppMarketActivityIntent( - packageName, Process.myUserHandle()); + Intent intent = ApiWrapper.INSTANCE.get(launcher).getMarketSearchIntent( + packageName, item.user); launcher.startActivitySafely(v, intent, item); }; @@ -358,9 +357,7 @@ public class ItemClickHandler { // Check for abandoned promise if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi() && (!Flags.enableSupportForArchiving() || !shortcut.isArchived())) { - String packageName = shortcut.getIntent().getComponent() != null - ? shortcut.getIntent().getComponent().getPackageName() - : shortcut.getIntent().getPackage(); + String packageName = shortcut.getTargetPackage(); if (!TextUtils.isEmpty(packageName)) { onClickPendingAppItem( v, diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java index 467a7ec54f..d24d084a29 100644 --- a/src/com/android/launcher3/util/ApiWrapper.java +++ b/src/com/android/launcher3/util/ApiWrapper.java @@ -28,6 +28,7 @@ import android.content.pm.LauncherActivityInfo; import android.content.pm.ShortcutInfo; import android.graphics.drawable.ColorDrawable; import android.net.Uri; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; @@ -120,6 +121,21 @@ public class ApiWrapper { * Activity). */ public Intent getAppMarketActivityIntent(String packageName, UserHandle user) { + return createMarketIntent(packageName); + } + + /** + * Returns an intent which can be used to start a search for a package on app market + */ + public Intent getMarketSearchIntent(String packageName, UserHandle user) { + // If we are search for the current user, just launch the market directly as the + // system won't have the installer details either + return (Process.myUserHandle().equals(user)) + ? createMarketIntent(packageName) + : getAppMarketActivityIntent(packageName, user); + } + + private static Intent createMarketIntent(String packageName) { return new Intent(Intent.ACTION_VIEW) .setData(new Uri.Builder() .scheme("market") diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java index 02779ce596..20e3eafcfb 100644 --- a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java +++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java @@ -15,24 +15,20 @@ */ package com.android.launcher3.util; -import android.graphics.drawable.Drawable; import android.view.View; import com.android.launcher3.BubbleTextView; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.PendingAppWidgetHostView; -import java.util.HashSet; -import java.util.List; +import java.util.Set; /** * Interface representing a container which can bind Launcher items with some utility methods @@ -41,27 +37,22 @@ public interface LauncherBindableItemsContainer { /** * Called to update workspace items as a result of - * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindWorkspaceItemsChanged(List)} + * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindItemsUpdated(Set)} */ - default void updateWorkspaceItems(List shortcuts, ActivityContext context) { - final HashSet updates = new HashSet<>(shortcuts); + default void updateContainerItems(Set updates, ActivityContext context) { ItemOperator op = (info, v) -> { - if (v instanceof BubbleTextView && updates.contains(info)) { - WorkspaceItemInfo si = (WorkspaceItemInfo) info; - BubbleTextView shortcut = (BubbleTextView) v; - Drawable oldIcon = shortcut.getIcon(); - boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable) - && ((PreloadIconDrawable) oldIcon).hasNotCompleted(); - shortcut.applyFromWorkspaceItem( - si, - si.isPromise() != oldPromiseState - && oldIcon instanceof PreloadIconDrawable - ? (PreloadIconDrawable) oldIcon - : null); - } else if (info instanceof FolderInfo && v instanceof FolderIcon) { - ((FolderIcon) v).updatePreviewItems(updates::contains); + if (v instanceof BubbleTextView shortcut + && info instanceof WorkspaceItemInfo wii + && updates.contains(info)) { + shortcut.applyFromWorkspaceItem(wii); + } else if (info instanceof FolderInfo && v instanceof FolderIcon folderIcon) { + folderIcon.updatePreviewItems(updates::contains); } else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) { appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); + } else if (v instanceof PendingAppWidgetHostView pendingView + && updates.contains(info)) { + pendingView.applyState(); + pendingView.postProviderAvailabilityCheck(); } // Iterate all items @@ -75,35 +66,6 @@ public interface LauncherBindableItemsContainer { } } - /** - * Called to update restored items as a result of - * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindRestoreItemsChange(HashSet)}} - */ - default void updateRestoreItems(final HashSet updates, ActivityContext context) { - ItemOperator op = (info, v) -> { - if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView - && updates.contains(info)) { - ((BubbleTextView) v).applyLoadingState(null); - } else if (v instanceof PendingAppWidgetHostView - && info instanceof LauncherAppWidgetInfo - && updates.contains(info)) { - ((PendingAppWidgetHostView) v).applyState(); - } else if (v instanceof FolderIcon && info instanceof FolderInfo) { - ((FolderIcon) v).updatePreviewItems(updates::contains); - } else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) { - appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); - } - // process all the shortcuts - return false; - }; - - mapOverItems(op); - Folder folder = Folder.getOpen(context); - if (folder != null) { - folder.iterateOverItems(op); - } - } - /** * Map the operator over the shortcuts and widgets. * diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java index 9c9b80d80f..cd8e4571e5 100644 --- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java @@ -21,7 +21,7 @@ import static android.graphics.Paint.DITHER_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; -import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; +import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.appwidget.AppWidgetProviderInfo; @@ -37,6 +37,9 @@ import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -60,8 +63,10 @@ import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.Themes; +import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener; import java.util.List; @@ -81,6 +86,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView private final Matrix mMatrix = new Matrix(); private final RectF mPreviewBitmapRect = new RectF(); private final RectF mCanvasRect = new RectF(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final RunnableList mOnDetachCleanup = new RunnableList(); private final LauncherWidgetHolder mWidgetHolder; private final LauncherAppWidgetProviderInfo mAppwidget; @@ -90,7 +97,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView private final CharSequence mLabel; private OnClickListener mClickListener; - private SafeCloseable mOnDetachCleanup; private int mDragFlags; @@ -210,16 +216,15 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView protected void onAttachedToWindow() { super.onAttachedToWindow(); + mOnDetachCleanup.executeAllAndClear(); if ((mAppwidget != null) && !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) && mInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) { // If the widget is not completely restored, but has a valid ID, then listen of // updates from provider app for potential restore complete. - if (mOnDetachCleanup != null) { - mOnDetachCleanup.close(); - } - mOnDetachCleanup = mWidgetHolder.addOnUpdateListener( + SafeCloseable updateCleanup = mWidgetHolder.addOnUpdateListener( mInfo.appWidgetId, mAppwidget, this::checkIfRestored); + mOnDetachCleanup.add(updateCleanup::close); checkIfRestored(); } } @@ -227,10 +232,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (mOnDetachCleanup != null) { - mOnDetachCleanup.close(); - mOnDetachCleanup = null; - } + mOnDetachCleanup.executeAllAndClear(); } /** @@ -295,43 +297,30 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView mCenterDrawable.setCallback(null); mCenterDrawable = null; } - mDragFlags = 0; - if (info.bitmap.icon != null) { - mDragFlags = FLAG_DRAW_ICON; + mDragFlags = FLAG_DRAW_ICON; - Drawable widgetCategoryIcon = getWidgetCategoryIcon(); - // The view displays three modes, - // 1) App icon in the center - // 2) Preload icon in the center - // 3) App icon in the center with a setup icon on the top left corner. - if (mDisabledForSafeMode) { - if (widgetCategoryIcon == null) { - FastBitmapDrawable disabledIcon = info.newIcon(getContext()); - disabledIcon.setIsDisabled(true); - mCenterDrawable = disabledIcon; - } else { - widgetCategoryIcon.setColorFilter(getDisabledColorFilter()); - mCenterDrawable = widgetCategoryIcon; - } - mSettingIconDrawable = null; - } else if (isReadyForClickSetup()) { - mCenterDrawable = widgetCategoryIcon == null - ? info.newIcon(getContext()) - : widgetCategoryIcon; - mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate(); - updateSettingColor(info.bitmap.color); + // The view displays three modes, + // 1) App icon in the center + // 2) Preload icon in the center + // 3) App icon in the center with a setup icon on the top left corner. + if (mDisabledForSafeMode) { + FastBitmapDrawable disabledIcon = info.newIcon(getContext()); + disabledIcon.setIsDisabled(true); + mCenterDrawable = disabledIcon; + mSettingIconDrawable = null; + } else if (isReadyForClickSetup()) { + mCenterDrawable = info.newIcon(getContext()); + mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate(); + updateSettingColor(info.bitmap.color); - mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL; - } else { - mCenterDrawable = widgetCategoryIcon == null - ? newPendingIcon(getContext(), info) - : widgetCategoryIcon; - mSettingIconDrawable = null; - applyState(); - } - mCenterDrawable.setCallback(this); - mDrawableSizeChanged = true; + mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL; + } else { + mCenterDrawable = newPendingIcon(getContext(), info); + mSettingIconDrawable = null; + applyState(); } + mCenterDrawable.setCallback(this); + mDrawableSizeChanged = true; invalidate(); } @@ -350,6 +339,11 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView } public void applyState() { + if (mCenterDrawable instanceof FastBitmapDrawable fb + && mInfo.pendingItemInfo != null + && !fb.isSameInfo(mInfo.pendingItemInfo.bitmap)) { + reapplyItemInfo(mInfo.pendingItemInfo); + } if (mCenterDrawable != null) { mCenterDrawable.setLevel(Math.max(mInfo.installProgress, 0)); } @@ -486,16 +480,72 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView } /** - * Returns the widget category icon for {@link #mInfo}. - * - *

If {@link #mInfo}'s category is {@code PackageItemInfo#NO_CATEGORY} or unknown, returns - * {@code null}. + * Creates a runnable runnable which tries to refresh the widget if it is restored */ - @Nullable - private Drawable getWidgetCategoryIcon() { - if (mInfo.pendingItemInfo.widgetCategory == WidgetSections.NO_CATEGORY) { - return null; + public void postProviderAvailabilityCheck() { + if (!mInfo.hasRestoreFlag(FLAG_PROVIDER_NOT_READY) && getAppWidgetInfo() == null) { + // If the info state suggests that the provider is ready, but there is no + // provider info attached on this pending view, recreate when the provider is available + DeferredWidgetRefresh restoreRunnable = new DeferredWidgetRefresh(); + mOnDetachCleanup.add(restoreRunnable::cleanup); + mHandler.post(restoreRunnable::notifyWidgetProvidersChanged); + } + } + + /** + * Used as a workaround to ensure that the AppWidgetService receives the + * PACKAGE_ADDED broadcast before updating widgets. + * + * This class will periodically check for the availability of the WidgetProvider as a result + * of providerChanged callback from the host. When the provider is available or a timeout of + * 10-sec is reached, it reinflates the pending-widget which in-turn goes through the process + * of re-evaluating the pending state of the widget, + */ + private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener { + private boolean mRefreshPending = true; + + DeferredWidgetRefresh() { + mWidgetHolder.addProviderChangeListener(this); + // Force refresh after 10 seconds, if we don't get the provider changed event. + // This could happen when the provider is no longer available in the app. + Message msg = Message.obtain(getHandler(), this); + msg.obj = DeferredWidgetRefresh.class; + mHandler.sendMessageDelayed(msg, 10000); + } + + /** + * Reinflate the widget if it is still attached. + */ + @Override + public void run() { + cleanup(); + if (mRefreshPending) { + reInflate(); + mRefreshPending = false; + } + } + + @Override + public void notifyWidgetProvidersChanged() { + final AppWidgetProviderInfo widgetInfo; + WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext()); + if (mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { + widgetInfo = widgetHelper.findProvider(mInfo.providerName, mInfo.user); + } else { + widgetInfo = widgetHelper.getLauncherAppWidgetInfo(mInfo.appWidgetId, + mInfo.getTargetComponent()); + } + if (widgetInfo != null) { + run(); + } + } + + /** + * Removes any scheduled callbacks and change listeners, no-op if nothing is scheduled + */ + public void cleanup() { + mWidgetHolder.removeProviderChangeListener(this); + mHandler.removeCallbacks(this); } - return mInfo.pendingItemInfo.newIcon(getContext()); } } diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java index f51871b647..faf6b91b1e 100644 --- a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java @@ -27,7 +27,9 @@ import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT_SMALL; import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING; import static com.android.launcher3.Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS; import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.google.common.truth.Truth.assertThat; @@ -39,6 +41,8 @@ import static org.mockito.Mockito.verify; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; import android.graphics.Typeface; import android.os.Build; import android.os.UserHandle; @@ -57,13 +61,17 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.Flags; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.Utilities; +import com.android.launcher3.graphics.PreloadIconDrawable; +import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.search.StringMatcherUtility; import com.android.launcher3.util.ActivityContextWrapper; import com.android.launcher3.util.FlagOp; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.views.BaseDragLayer; import org.junit.After; @@ -73,6 +81,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CountDownLatch; + /** * Unit tests for testing modifyTitleToSupportMultiLine() in BubbleTextView.java * This class tests a couple of strings and uses the getMaxLines() to determine if the test passes. @@ -485,4 +495,40 @@ public class BubbleTextViewTest { assertThat(mBubbleTextView.getIcon().hasBadge()).isEqualTo(true); } + + @Test + public void applyingPendingIcon_preserves_last_icon() throws Exception { + mItemInfoWithIcon.bitmap = + BitmapInfo.fromBitmap(Bitmap.createBitmap(100, 100, Config.ARGB_8888)); + mItemInfoWithIcon.setProgressLevel(30, PackageInstallInfo.STATUS_INSTALLING); + + TestUtil.runOnExecutorSync(MAIN_EXECUTOR, + () -> mBubbleTextView.applyIconAndLabel(mItemInfoWithIcon)); + assertThat(mBubbleTextView.getIcon()).isInstanceOf(PreloadIconDrawable.class); + assertThat(mBubbleTextView.getIcon().getLevel()).isEqualTo(30); + PreloadIconDrawable oldIcon = (PreloadIconDrawable) mBubbleTextView.getIcon(); + + // Same icon is used when progress changes + mItemInfoWithIcon.setProgressLevel(50, PackageInstallInfo.STATUS_INSTALLING); + TestUtil.runOnExecutorSync(MAIN_EXECUTOR, + () -> mBubbleTextView.applyIconAndLabel(mItemInfoWithIcon)); + assertThat(mBubbleTextView.getIcon()).isSameInstanceAs(oldIcon); + assertThat(mBubbleTextView.getIcon().getLevel()).isEqualTo(50); + + // Icon is replaced with a non pending icon when download finishes + mItemInfoWithIcon.setProgressLevel(100, PackageInstallInfo.STATUS_INSTALLED); + + CountDownLatch animWait = new CountDownLatch(1); + TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> { + mBubbleTextView.applyIconAndLabel(mItemInfoWithIcon); + assertThat(mBubbleTextView.getIcon()).isSameInstanceAs(oldIcon); + assertThat(oldIcon.getActiveAnimation()).isNotNull(); + oldIcon.getActiveAnimation().addListener(forEndCallback(animWait::countDown)); + }); + animWait.await(); + + // Assert that the icon is replaced with a non-pending icon + assertThat(mBubbleTextView.getIcon()).isNotInstanceOf(PreloadIconDrawable.class); + } + } diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherBindableItemsContainerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/LauncherBindableItemsContainerTest.kt new file mode 100644 index 0000000000..93be5f5125 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherBindableItemsContainerTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 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.util + +import android.content.ComponentName +import android.content.pm.LauncherApps +import android.graphics.Bitmap +import android.graphics.Bitmap.Config.ARGB_8888 +import android.os.Process.myUserHandle +import android.platform.uiautomatorhelpers.DeviceHelpers.context +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.BubbleTextView +import com.android.launcher3.graphics.PreloadIconDrawable +import com.android.launcher3.icons.BitmapInfo +import com.android.launcher3.icons.FastBitmapDrawable +import com.android.launcher3.icons.PlaceHolderIconDrawable +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.AppInfo.makeLaunchIntent +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.pm.PackageInstallInfo +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2 +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3 +import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LauncherBindableItemsContainerTest { + + private val icon1 by lazy { getLAI(TEST_ACTIVITY) } + private val icon2 by lazy { getLAI(TEST_ACTIVITY2) } + private val icon3 by lazy { getLAI(TEST_ACTIVITY3) } + + private val container = TestContainer() + + @Test + fun `icon bitmap is updated`() { + container.addIcon(icon1) + container.addIcon(icon2) + container.addIcon(icon3) + + assertThat(container.getAppIcon(icon1).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon2).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon3).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + + icon2.bitmap = BitmapInfo.fromBitmap(Bitmap.createBitmap(200, 200, ARGB_8888)) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) { + container.updateContainerItems(setOf(icon2), container) + } + + assertThat(container.getAppIcon(icon1).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon3).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon2).icon) + .isNotInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon2).icon).isInstanceOf(FastBitmapDrawable::class.java) + } + + @Test + fun `icon download progress updated`() { + container.addIcon(icon1) + container.addIcon(icon2) + assertThat(container.getAppIcon(icon1).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon2).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + + icon1.status = WorkspaceItemInfo.FLAG_RESTORED_ICON + icon1.bitmap = BitmapInfo.fromBitmap(Bitmap.createBitmap(200, 200, ARGB_8888)) + icon1.setProgressLevel(30, PackageInstallInfo.STATUS_INSTALLING) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) { + container.updateContainerItems(setOf(icon1), container) + } + + assertThat(container.getAppIcon(icon2).icon) + .isInstanceOf(PlaceHolderIconDrawable::class.java) + assertThat(container.getAppIcon(icon1).icon).isInstanceOf(PreloadIconDrawable::class.java) + val oldIcon = container.getAppIcon(icon1).icon as PreloadIconDrawable + assertThat(oldIcon.level).isEqualTo(30) + } + + private fun getLAI(className: String): WorkspaceItemInfo = + AppInfo( + context, + context + .getSystemService(LauncherApps::class.java)!! + .resolveActivity( + makeLaunchIntent(ComponentName(TEST_PACKAGE, className)), + myUserHandle(), + )!!, + myUserHandle(), + ) + .makeWorkspaceItem(context) + + class TestContainer : ActivityContextWrapper(context), LauncherBindableItemsContainer { + + val items = mutableMapOf() + + override fun mapOverItems(op: ItemOperator) { + items.forEach { (item, view) -> if (op.evaluate(item, view)) return@forEach } + } + + fun addIcon(info: WorkspaceItemInfo) { + val btv = BubbleTextView(this) + btv.applyFromWorkspaceItem(info) + items[info] = btv + } + + fun getAppIcon(info: WorkspaceItemInfo) = items[info] as BubbleTextView + } +}