Revert^2 "Unifying various model update callbacks into one"

72f9943f64

Change-Id: I38901714947a2b7926723ea25df4a2b8216303e4
This commit is contained in:
Sunny Goyal
2025-01-30 13:17:41 -08:00
parent 72f9943f64
commit dfbbebc9e3
19 changed files with 437 additions and 370 deletions
@@ -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<WorkspaceItemInfo> updated) {
updateWorkspaceItems(updated, mContext);
}
@Override
public void bindRestoreItemsChange(HashSet<ItemInfo> updates) {
updateRestoreItems(updates, mContext);
public void bindItemsUpdated(Set<ItemInfo> updates) {
updateContainerItems(updates, mContext);
}
@Override
@@ -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 :
+65 -66
View File
@@ -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);
+3 -16
View File
@@ -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<LauncherState>
mModelCallbacks.bindIncrementalDownloadProgressUpdated(app);
}
@Override
public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) {
mModelCallbacks.bindWidgetsRestored(widgets);
}
/**
* See {@code LauncherBindingDelegate}
*/
@Override
public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
mModelCallbacks.bindWorkspaceItemsChanged(updated);
}
/**
* See {@code LauncherBindingDelegate}
*/
@Override
public void bindRestoreItemsChange(HashSet<ItemInfo> updates) {
mModelCallbacks.bindRestoreItemsChange(updates);
public void bindItemsUpdated(Set<ItemInfo> updates) {
mModelCallbacks.bindItemsUpdated(updates);
}
/**
+3 -21
View File
@@ -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<LauncherAppWidgetInfo?>?) {
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<WorkspaceItemInfo?>) {
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<ItemInfo?>?) {
launcher.workspace.updateRestoreItems(updates, launcher)
override fun bindItemsUpdated(updates: Set<ItemInfo>) {
launcher.workspace.updateContainerItems(updates, launcher)
PopupContainerWithArrow.dismissInvalidPopup(launcher)
}
/**
-102
View File
@@ -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<T extends View & PageIndicator> extends PagedView<T>
bindAndInitFirstWorkspaceScreen();
}
// Remove any deferred refresh callbacks
mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class);
// Re-enable the layout transitions
enableLayoutTransitions();
}
@@ -3465,43 +3456,6 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
removeItemsByMatcher(matcher);
}
public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> 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<T extends View & PageIndicator> extends PagedView<T>
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<LauncherAppWidgetInfo> mInfos;
private final LauncherWidgetHolder mWidgetHolder;
private final Handler mHandler;
private boolean mRefreshPending;
DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> 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<PendingAppWidgetHostView> 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 {
@@ -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<T extends Context & ActivityContext> {
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);
}
});
}
@@ -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;
@@ -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<WorkspaceItemInfo> updated) { }
default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
/** Called when a runtime property of the ItemInfo is updated due to some system event */
default void bindItemsUpdated(Set<ItemInfo> updates) { }
default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
/**
@@ -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<WorkspaceItemInfo> updatedShortcuts = new ArrayList<>();
ArrayList<ItemInfo> 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();
}
@@ -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<WorkspaceItemInfo>) {
fun bindUpdatedWorkspaceItems(allUpdates: Collection<ItemInfo>) {
// 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) }
@@ -99,8 +99,7 @@ public class PackageInstallStateChangedTask implements ModelUpdateTask {
});
if (!updates.isEmpty()) {
taskController.scheduleCallbackTask(
callbacks -> callbacks.bindRestoreItemsChange(updates));
taskController.bindUpdatedWorkspaceItems(updates);
}
}
}
@@ -214,8 +214,7 @@ public class PackageUpdatedTask implements ModelUpdateTask {
// Update shortcut infos
if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>();
final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
final ArrayList<ItemInfo> 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<String> removedPackages = new HashSet<>();
@@ -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<SessionInfo> siFuture;
siFuture = CompletableFuture.supplyAsync(() ->
InstallSessionHelper.INSTANCE.get(launcher)
.getActiveSessionInfo(item.user, packageName),
CompletableFuture<SessionInfo> siFuture = CompletableFuture.supplyAsync(() ->
InstallSessionHelper.INSTANCE.get(launcher)
.getActiveSessionInfo(item.user, packageName),
UI_HELPER_EXECUTOR);
Consumer<SessionInfo> 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,
@@ -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")
@@ -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<WorkspaceItemInfo> shortcuts, ActivityContext context) {
final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
default void updateContainerItems(Set<ItemInfo> 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<ItemInfo> 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.
*
@@ -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}.
*
* <p>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());
}
}
@@ -28,6 +28,7 @@ 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.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,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 +60,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;
@@ -485,4 +492,38 @@ 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);
TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> {
mBubbleTextView.applyIconAndLabel(mItemInfoWithIcon);
assertThat(mBubbleTextView.getIcon()).isSameInstanceAs(oldIcon);
assertThat(oldIcon.getActiveAnimation()).isNotNull();
oldIcon.getActiveAnimation().end();
});
// Assert that the icon is replaced with a non-pending icon
assertThat(mBubbleTextView.getIcon()).isNotInstanceOf(PreloadIconDrawable.class);
}
}
@@ -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<ItemInfo, View>()
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
}
}