Avoid flicker to drop a widget that needs a config activity.

When dropping a widget that requires an config activity, drop a
PendingAppWidgetHostView to workspace

Adb command to try the feature:

adb shell device_config put launcher com.android.launcher3.enable_add_app_widget_via_config_activity_v2 true

Fix: 284236964
Test: manual
Flag: aconfig launcher.enable_add_app_widget_via_config_activity_v2 DISABLED
Change-Id: Ifd0be5c607a388cf8a8f6d77b46c03112e3e599f
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 496cb4e..a74cffb 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
+import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
 import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -116,6 +117,8 @@
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -829,7 +832,7 @@
                 announceForAccessibility(R.string.item_added_to_workspace);
                 break;
             case REQUEST_CREATE_APPWIDGET:
-                completeAddAppWidget(appWidgetId, info, null, null);
+                completeAddAppWidget(appWidgetId, info, null, null, false, null);
                 break;
             case REQUEST_RECONFIGURE_APPWIDGET:
                 getStatsLogManager().logger().withItemInfo(info).log(LAUNCHER_WIDGET_RECONFIGURED);
@@ -1016,11 +1019,18 @@
         AppWidgetHostView boundWidget = null;
         if (resultCode == RESULT_OK) {
             animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
-            final AppWidgetHostView layout = mAppWidgetHolder.createView(appWidgetId,
-                    requestArgs.getWidgetHandler().getProviderInfo(this));
+
+            // Now that we are exiting the config activity with RESULT_OK.
+            // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we can retrieve the
+            // PendingAppWidgetHostView from LauncherWidgetHolder (it was added to
+            // LauncherWidgetHolder when starting the config activity).
+            final AppWidgetHostView layout = enableAddAppWidgetViaConfigActivityV2()
+                    ? getWorkspace().getWidgetForAppWidgetId(appWidgetId)
+                    : mAppWidgetHolder.createView(appWidgetId,
+                            requestArgs.getWidgetHandler().getProviderInfo(this));
             boundWidget = layout;
             onCompleteRunnable = () -> {
-                completeAddAppWidget(appWidgetId, requestArgs, layout, null);
+                completeAddAppWidget(appWidgetId, requestArgs, layout, null, false, null);
                 if (!isInState(EDIT_MODE)) {
                     mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                 }
@@ -1450,14 +1460,15 @@
      */
     @Thunk
     void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
-            AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
+            @Nullable AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo,
+            boolean showPendingWidget, @Nullable Bitmap widgetPreviewBitmap) {
 
         if (appWidgetInfo == null) {
             appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId,
                     itemInfo.getTargetComponent());
         }
 
-        if (hostView == null) {
+        if (hostView == null && !showPendingWidget) {
             // Perform actual inflation because we're live
             hostView = mAppWidgetHolder.createView(appWidgetId, appWidgetInfo);
         }
@@ -1471,39 +1482,71 @@
         launcherInfo.minSpanX = itemInfo.minSpanX;
         launcherInfo.minSpanY = itemInfo.minSpanY;
         launcherInfo.user = appWidgetInfo.getProfile();
+        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
+        if (showPendingWidget) {
+            launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+            PendingAppWidgetHostView pendingAppWidgetHostView =
+                    new PendingAppWidgetHostView(this, launcherInfo, appWidgetInfo);
+            pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
+            hostView = pendingAppWidgetHostView;
+        } else if (hostView instanceof PendingAppWidgetHostView) {
+            ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null);
+            // User has selected a widget config and exited the config activity, we can trigger
+            // re-inflation of PendingAppWidgetHostView to replace it with
+            // LauncherAppWidgetHostView in workspace.
+            completeRestoreAppWidget(appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED);
+
+            // Show resize frame on the newly inflated LauncherAppWidgetHostView.
+            LauncherAppWidgetHostView reInflatedHostView =
+                    getWorkspace().getWidgetForAppWidgetId(appWidgetId);
+            showWidgetResizeFrame(
+                    reInflatedHostView,
+                    (LauncherAppWidgetInfo) reInflatedHostView.getTag(),
+                    presenterPos);
+            return;
+        }
         if (itemInfo instanceof PendingAddWidgetInfo) {
             launcherInfo.sourceContainer = ((PendingAddWidgetInfo) itemInfo).sourceContainer;
         } else if (itemInfo instanceof PendingRequestArgs) {
             launcherInfo.sourceContainer =
                     ((PendingRequestArgs) itemInfo).getWidgetSourceContainer();
         }
-        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
         getModelWriter().addItemToDatabase(launcherInfo,
                 itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY);
 
         hostView.setVisibility(View.VISIBLE);
         mItemInflater.prepareAppWidget(hostView, launcherInfo);
-        mWorkspace.addInScreen(hostView, launcherInfo);
+        if (!enableAddAppWidgetViaConfigActivityV2() || hostView.getParent() == null) {
+            mWorkspace.addInScreen(hostView, launcherInfo);
+        }
         announceForAccessibility(R.string.item_added_to_workspace);
 
         // Show the widget resize frame.
         if (hostView instanceof LauncherAppWidgetHostView) {
             final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView;
-            CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
-            if (mStateManager.getState() == NORMAL) {
-                AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
-            } else {
-                mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
-                    @Override
-                    public void onStateTransitionComplete(LauncherState finalState) {
-                        if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
-                                && finalState == NORMAL) {
-                            AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
-                            mStateManager.removeStateListener(this);
-                        }
+            showWidgetResizeFrame(launcherHostView, launcherInfo, presenterPos);
+        }
+    }
+
+    /** Show widget resize frame. */
+    private void showWidgetResizeFrame(
+            LauncherAppWidgetHostView launcherHostView,
+            LauncherAppWidgetInfo launcherInfo,
+            CellPos presenterPos) {
+        CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
+        if (mStateManager.getState() == NORMAL) {
+            AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+        } else {
+            mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
+                            && finalState == NORMAL) {
+                        AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+                        mStateManager.removeStateListener(this);
                     }
-                });
-            }
+                }
+            });
         }
     }
 
@@ -1760,19 +1803,39 @@
         addAppWidgetImpl(appWidgetId, info, boundWidget, addFlowHandler, 0);
     }
 
+    /**
+     * If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we always add widget
+     * host view to workspace, otherwise we only add widget to host view if config activity is
+     * not started.
+     */
     void addAppWidgetImpl(int appWidgetId, ItemInfo info,
             AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) {
-        if (!addFlowHandler.startConfigActivity(this, appWidgetId, info,
-                REQUEST_CREATE_APPWIDGET)) {
-            // If the configuration flow was not started, add the widget
+        final boolean isActivityStarted = addFlowHandler.startConfigActivity(
+                this, appWidgetId, info, REQUEST_CREATE_APPWIDGET);
 
-            // Exit spring loaded mode if necessary after adding the widget
-            Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
-                    : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
-            completeAddAppWidget(appWidgetId, info, boundWidget,
-                    addFlowHandler.getProviderInfo(this));
-            mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        if (!enableAddAppWidgetViaConfigActivityV2() && isActivityStarted) {
+            return;
         }
+
+        // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled and config activity is
+        // started, we should remove the dropped AppWidgetHostView from drag layer and extract the
+        // Bitmap that shows the preview. Then pass the Bitmap to completeAddAppWidget() to create
+        // a PendingWidgetHostView.
+        Bitmap widgetPreviewBitmap = null;
+        if (isActivityStarted) {
+            DragView dropView = getDragLayer().clearAnimatedView();
+            if (dropView != null && dropView.containsAppWidgetHostView()) {
+                widgetPreviewBitmap = getBitmapFromView(dropView.getContentView());
+            }
+        }
+
+        // Exit spring loaded mode if necessary after adding the widget
+        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
+                : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+        completeAddAppWidget(appWidgetId, info, boundWidget,
+                addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(),
+                widgetPreviewBitmap);
+        mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
     }
 
     public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
@@ -2373,6 +2436,18 @@
         return null;
     }
 
+    /** Convert a {@link View} to {@link Bitmap}. */
+    private static Bitmap getBitmapFromView(@Nullable View view) {
+        if (view == null) {
+            return null;
+        }
+        Bitmap returnedBitmap =
+                Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(returnedBitmap);
+        view.draw(canvas);
+        return returnedBitmap;
+    }
+
     /**
      * Returns the first view matching the operator in the given ViewGroups, or null if none.
      * Forward iteration matters.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index ac0d7ce..b4745c9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2988,7 +2988,7 @@
     }
 
     public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
-            final Runnable onCompleteRunnable, int animationType, final View finalView,
+            final Runnable onCompleteRunnable, int animationType, @Nullable final View finalView,
             boolean external) {
         int[] finalPos = new int[2];
         float scaleXY[] = new float[2];
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index f18f900..db693f0 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -42,6 +42,8 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.Nullable;
+
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DropTargetBar;
@@ -388,7 +390,13 @@
         mDropAnim.start();
     }
 
-    public void clearAnimatedView() {
+    /**
+     * Remove the drop view and end the drag animation.
+     *
+     * @return {@link DragView} that is removed.
+     */
+    @Nullable
+    public DragView clearAnimatedView() {
         if (mDropAnim != null) {
             mDropAnim.cancel();
         }
@@ -396,8 +404,10 @@
         if (mDropView != null) {
             mDragController.onDeferredEndDrag(mDropView);
         }
+        DragView ret = mDropView;
         mDropView = null;
         invalidate();
+        return ret;
     }
 
     public View getAnimatedView() {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 1f07352..bcee442 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -30,6 +30,7 @@
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.TargetApi;
+import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -563,6 +564,11 @@
         return mContentViewParent;
     }
 
+    /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+    public boolean containsAppWidgetHostView() {
+        return mContent instanceof AppWidgetHostView;
+    }
+
     private static class SpringFloatValue {
 
         private static final FloatPropertyCompat<SpringFloatValue> VALUE =
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 25979c2..adf85c7 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,13 +16,20 @@
 
 package com.android.launcher3.widget;
 
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+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.util.Executors.MAIN_EXECUTOR;
 
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
@@ -85,8 +92,12 @@
     private boolean mDrawableSizeChanged;
 
     private final TextPaint mPaint;
+
+    private final Paint mPreviewPaint;
     private Layout mSetupTextLayout;
 
+    @Nullable private Bitmap mPreviewBitmap;
+
     public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
             @Nullable LauncherAppWidgetProviderInfo appWidget) {
         this(context, info, appWidget,
@@ -116,6 +127,15 @@
         mDrawableSizeChanged = true;
     }
 
+    /** Set {@link Bitmap} of widget preview. */
+    public void setPreviewBitmap(@Nullable Bitmap previewBitmap) {
+        if (this.mPreviewBitmap == previewBitmap) {
+            return;
+        }
+        this.mPreviewBitmap = previewBitmap;
+        invalidate();
+    }
+
     private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
             LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
         super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
@@ -130,12 +150,17 @@
         mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
         mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
                 mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
-
+        mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
         setWillNotDraw(false);
         setBackgroundResource(R.drawable.pending_widget_bg);
     }
 
     @Override
+    public AppWidgetProviderInfo getAppWidgetInfo() {
+        return mAppwidget;
+    }
+
+    @Override
     public void updateAppWidget(RemoteViews remoteViews) {
         checkIfRestored();
     }
@@ -393,6 +418,11 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
+        if (mPreviewBitmap != null
+                && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) {
+            canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint);
+            return;
+        }
         if (mCenterDrawable == null) {
             // Nothing to draw
             return;