[2/n] Avoid flicker to drop a widget that needs a config activity

1. Don't show background in PendingAppWidgetHostView if we are showing preview bitmap
2. Extract bitmap from DropView instead of LauncherAppHostView
3. Letterbox bitmap into PendingAppWidgetHostView's canvas if canvas has different aspect ratio

Fix: 284236964
Flag: aconfig launcher.enable_add_app_widget_via_config_activity_v2 DISABLED
Test: manual
Change-Id: I76de215186b96ffe65c909b28155fb19ac90d4f1
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index c1ebbe5..7267e63 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1483,11 +1483,10 @@
         if (showPendingWidget) {
             launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
             PendingAppWidgetHostView pendingAppWidgetHostView = new PendingAppWidgetHostView(
-                    this, mAppWidgetHolder, launcherInfo, appWidgetInfo);
-            pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
+                    this, mAppWidgetHolder, launcherInfo, appWidgetInfo, widgetPreviewBitmap);
             hostView = pendingAppWidgetHostView;
         } else if (hostView instanceof PendingAppWidgetHostView) {
-            ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null);
+            ((PendingAppWidgetHostView) hostView).setPreviewBitmapAndUpdateBackground(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.
@@ -1822,7 +1821,9 @@
         if (isActivityStarted) {
             DragView dropView = getDragLayer().clearAnimatedView();
             if (dropView != null && dropView.containsAppWidgetHostView()) {
-                widgetPreviewBitmap = getBitmapFromView(dropView.getContentView());
+                // Extracting Bitmap from dropView instead of its content view produces the correct
+                // bitmap.
+                widgetPreviewBitmap = getBitmapFromView(dropView);
             }
         }
 
diff --git a/src/com/android/launcher3/RectUtils.kt b/src/com/android/launcher3/RectUtils.kt
new file mode 100644
index 0000000..68d2eaf
--- /dev/null
+++ b/src/com/android/launcher3/RectUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.graphics.Rect
+
+/**
+ * Fit [this] into [targetRect] with letter boxing. After calling this method, [this] will be
+ * modified to be letter boxed.
+ *
+ * @param targetRect target [Rect] that [this] should be fitted into
+ */
+fun Rect.letterBox(targetRect: Rect) {
+    letterBox(targetRect, this)
+}
+
+/**
+ * Fit [this] into [targetRect] with letter boxing. After calling this method, [resultRect] will be
+ * modified to be letter boxed.
+ *
+ * @param targetRect target [Rect] that [this] should be fitted into
+ * @param resultRect the letter boxed [Rect]
+ */
+fun Rect.letterBox(targetRect: Rect, resultRect: Rect) {
+    val widthRatio: Float = 1f * targetRect.width() / width()
+    val heightRatio: Float = 1f * targetRect.height() / height()
+    if (widthRatio < heightRatio) {
+        val scaledHeight: Int = (widthRatio * height()).toInt()
+        val verticalPadding: Int = (targetRect.height() - scaledHeight) / 2
+        resultRect.set(
+            targetRect.left,
+            targetRect.top + verticalPadding,
+            targetRect.right,
+            targetRect.bottom - verticalPadding
+        )
+    } else {
+        val scaledWidth: Int = (heightRatio * width()).toInt()
+        val horizontalPadding: Int = (targetRect.width() - scaledWidth) / 2
+        resultRect.set(
+            targetRect.left + horizontalPadding,
+            targetRect.top,
+            targetRect.right - horizontalPadding,
+            targetRect.bottom
+        )
+    }
+}
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 86400ba..7f9a1fc 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -53,6 +53,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.RectUtilsKt;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -76,6 +77,10 @@
 
     private final Rect mRect = new Rect();
 
+    private final Rect mPreviewBitmapRect = new Rect();
+    private final Rect mCanvasRect = new Rect();
+    private final Rect mLetterBoxedPreviewBitmapRect = new Rect();
+
     private final LauncherWidgetHolder mWidgetHolder;
     private final LauncherAppWidgetProviderInfo mAppwidget;
     private final LauncherAppWidgetInfo mInfo;
@@ -103,9 +108,14 @@
 
     public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
             LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) {
-        this(context, widgetHolder, info, appWidget,
-                context.getResources().getText(R.string.gadget_complete_setup_text));
+        this(context, widgetHolder, info, appWidget, null);
+    }
 
+    public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
+            LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget,
+            @Nullable Bitmap previewBitmap) {
+        this(context, widgetHolder, info, appWidget,
+                context.getResources().getText(R.string.gadget_complete_setup_text), previewBitmap);
         super.updateAppWidget(null);
         setOnClickListener(mActivityContext.getItemOnClickListener());
 
@@ -123,7 +133,7 @@
             Context context, LauncherWidgetHolder widgetHolder,
             int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
         this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
-                appWidget, appWidget.label);
+                appWidget, appWidget.label, null);
         getBackground().mutate().setAlpha(DEFERRED_ALPHA);
 
         mCenterDrawable = new ColorDrawable(Color.TRANSPARENT);
@@ -132,8 +142,12 @@
         mIsDeferredWidget = true;
     }
 
-    /** Set {@link Bitmap} of widget preview. */
-    public void setPreviewBitmap(@Nullable Bitmap previewBitmap) {
+    /**
+     * Set {@link Bitmap} of widget preview and update background drawable. When showing preview
+     * bitmap, we shouldn't draw background.
+     */
+    public void setPreviewBitmapAndUpdateBackground(@Nullable Bitmap previewBitmap) {
+        setBackgroundResource(previewBitmap != null ? 0 : R.drawable.pending_widget_bg);
         if (this.mPreviewBitmap == previewBitmap) {
             return;
         }
@@ -143,7 +157,8 @@
 
     private PendingAppWidgetHostView(Context context,
             LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info,
-            LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
+            LauncherAppWidgetProviderInfo appwidget, CharSequence label,
+            @Nullable Bitmap previewBitmap) {
         super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
         mWidgetHolder = widgetHolder;
         mAppwidget = appwidget;
@@ -161,7 +176,7 @@
         mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
 
         setWillNotDraw(false);
-        setBackgroundResource(R.drawable.pending_widget_bg);
+        setPreviewBitmapAndUpdateBackground(previewBitmap);
     }
 
     @Override
@@ -440,7 +455,12 @@
     protected void onDraw(Canvas canvas) {
         if (mPreviewBitmap != null
                 && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) {
-            canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint);
+            mPreviewBitmapRect.set(0, 0, mPreviewBitmap.getWidth(), mPreviewBitmap.getHeight());
+            mCanvasRect.set(0, 0, getWidth(), getHeight());
+
+            RectUtilsKt.letterBox(mPreviewBitmapRect, mCanvasRect, mLetterBoxedPreviewBitmapRect);
+            canvas.drawBitmap(mPreviewBitmap, mPreviewBitmapRect, mLetterBoxedPreviewBitmapRect,
+                    mPreviewPaint);
             return;
         }
         if (mCenterDrawable == null) {
@@ -463,7 +483,6 @@
             mSetupTextLayout.draw(canvas);
             canvas.restore();
         }
-
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/RectUtilsTest.kt b/tests/src/com/android/launcher3/RectUtilsTest.kt
new file mode 100644
index 0000000..f0d22eb
--- /dev/null
+++ b/tests/src/com/android/launcher3/RectUtilsTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RectUtilsTest {
+
+    private val srcRect = Rect()
+    private val destRect = Rect()
+    private val letterBoxedRect = Rect()
+
+    @Test
+    fun letterBoxSelf_toSameRect_noScale() {
+        srcRect.set(0, 0, 100, 100)
+        destRect.set(0, 0, 100, 100)
+
+        srcRect.letterBox(destRect)
+
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 100, 100))
+    }
+
+    @Test
+    fun letterBox_toSameRect_noScale() {
+        srcRect.set(0, 0, 100, 100)
+        destRect.set(0, 0, 100, 100)
+
+        srcRect.letterBox(destRect, letterBoxedRect)
+
+        assertThat(letterBoxedRect).isEqualTo(Rect(0, 0, 100, 100))
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 100, 100))
+    }
+
+    @Test
+    fun letterBoxSelf_toSmallHeight_scaleDownHorizontally() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(0, 0, 939, 520)
+
+        srcRect.letterBox(destRect)
+
+        assertThat(srcRect).isEqualTo(Rect(114, 0, 825, 520))
+    }
+
+    @Test
+    fun letterBoxRect_toSmallHeight_scaleDownHorizontally() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(0, 0, 939, 520)
+
+        srcRect.letterBox(destRect, letterBoxedRect)
+
+        assertThat(letterBoxedRect).isEqualTo(Rect(114, 0, 825, 520))
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114))
+    }
+
+    @Test
+    fun letterBoxSelf_toSmallHeightWithOffset_scaleDownHorizontally() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(10, 20, 949, 540)
+
+        srcRect.letterBox(destRect)
+
+        assertThat(srcRect).isEqualTo(Rect(124, 20, 835, 540))
+    }
+
+    @Test
+    fun letterBoxRect_toSmallHeightWithOffset_scaleDownHorizontally() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(10, 20, 949, 540)
+
+        srcRect.letterBox(destRect, letterBoxedRect)
+
+        assertThat(letterBoxedRect).isEqualTo(Rect(124, 20, 835, 540))
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114))
+    }
+
+    @Test
+    fun letterBoxSelf_toSmallWidth_scaleDownVertically() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(0, 0, 520, 939)
+
+        srcRect.letterBox(destRect)
+
+        assertThat(srcRect).isEqualTo(Rect(0, 280, 520, 659))
+    }
+
+    @Test
+    fun letterBoxRect_toSmallWidth_scaleDownVertically() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(0, 0, 520, 939)
+
+        srcRect.letterBox(destRect, letterBoxedRect)
+
+        assertThat(letterBoxedRect).isEqualTo(Rect(0, 280, 520, 659))
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114))
+    }
+
+    @Test
+    fun letterBoxSelf_toSmallWidthWithOffset_scaleDownVertically() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(40, 60, 560, 999)
+
+        srcRect.letterBox(destRect)
+
+        assertThat(srcRect).isEqualTo(Rect(40, 340, 560, 719))
+    }
+
+    @Test
+    fun letterBoxRect_toSmallWidthWithOffset_scaleDownVertically() {
+        srcRect.set(0, 0, 2893, 2114)
+        destRect.set(40, 60, 560, 999)
+
+        srcRect.letterBox(destRect, letterBoxedRect)
+
+        assertThat(letterBoxedRect).isEqualTo(Rect(40, 340, 560, 719))
+        assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114))
+    }
+}