Revert^2 "Unifying various model update callbacks into one"
72f9943f64
Change-Id: I38901714947a2b7926723ea25df4a2b8216303e4
This commit is contained in:
@@ -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 :
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+137
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user