Use generated RemoteViews preview for widget picker if available

WidgetCell will use the generated RemoteViews preview to display
previews in the widget picker. It will fallback to current preview
methods if not available.

Introduces WidgetManagerHelper.loadGeneratedPreviews as a utility
function. WidgetManagerHelper is passed into the constructor of
WidgetItem to avoid calling
context.getSystemService(AppWidgetManager.class) for each widget.

Bug: 308041327
Test: atest Launcher3Tests:GeneratedPreviewTest
Flag: ACONFIG com.android.launcher3.enable_generated_previews DEVELOPMENT
Change-Id: I37429057cda83a5321884ace2537038e050b9a58
diff --git a/Android.bp b/Android.bp
index eca0778..7856f07 100644
--- a/Android.bp
+++ b/Android.bp
@@ -202,6 +202,7 @@
         "animationlib",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "android.appwidget.flags-aconfig-java",
     ],
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index c13c68c..5d6ce2d 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -142,6 +142,13 @@
 }
 
 flag {
+    name: "enable_generated_previews"
+    namespace: "launcher"
+    description: "Enables support for RemoteViews previews in the widget picker."
+    bug: "306546610"
+}
+
+flag {
   name: "enable_categorized_widget_suggestions"
   namespace: "launcher"
   description: "Enables widget suggestions in widget picker to be displayed in categories"
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index c99b889..ddf4023 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -1,5 +1,9 @@
 package com.android.launcher3.model;
 
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX;
+
 import static com.android.launcher3.Utilities.ATLEAST_S;
 
 import android.annotation.SuppressLint;
@@ -7,13 +11,19 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.util.SparseArray;
+import android.widget.RemoteViews;
 
+import androidx.core.os.BuildCompat;
+
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 /**
  * An wrapper over various items displayed in a widget picker,
@@ -28,9 +38,11 @@
     public final String label;
     public final CharSequence description;
     public final int spanX, spanY;
+    public final SparseArray<RemoteViews> generatedPreviews;
 
     public WidgetItem(LauncherAppWidgetProviderInfo info,
-            InvariantDeviceProfile idp, IconCache iconCache, Context context) {
+            InvariantDeviceProfile idp, IconCache iconCache, Context context,
+            WidgetManagerHelper helper) {
         super(info.provider, info.getProfile());
 
         label = iconCache.getTitleNoCache(info);
@@ -40,6 +52,27 @@
 
         spanX = Math.min(info.spanX, idp.numColumns);
         spanY = Math.min(info.spanY, idp.numRows);
+
+        if (BuildCompat.isAtLeastV() && Flags.enableGeneratedPreviews()) {
+            generatedPreviews = new SparseArray<>(3);
+            for (int widgetCategory : new int[] {
+                    WIDGET_CATEGORY_HOME_SCREEN,
+                    WIDGET_CATEGORY_KEYGUARD,
+                    WIDGET_CATEGORY_SEARCHBOX,
+            }) {
+                if ((widgetCategory & widgetInfo.generatedPreviewCategories) != 0) {
+                    generatedPreviews.put(widgetCategory,
+                            helper.loadGeneratedPreview(widgetInfo, widgetCategory));
+                }
+            }
+        } else {
+            generatedPreviews = null;
+        }
+    }
+
+    public WidgetItem(LauncherAppWidgetProviderInfo info,
+            InvariantDeviceProfile idp, IconCache iconCache, Context context) {
+        this(info, idp, iconCache, context, new WidgetManagerHelper(context));
     }
 
     public WidgetItem(ShortcutConfigActivityInfo info, IconCache iconCache, PackageManager pm) {
@@ -50,6 +83,7 @@
         widgetInfo = null;
         activityInfo = info;
         spanX = spanY = 1;
+        generatedPreviews = null;
     }
 
     /**
@@ -78,4 +112,15 @@
     public boolean isShortcut() {
         return activityInfo != null;
     }
+
+    /**
+     * Returns whether this {@link WidgetItem} has a generated preview for the given widget
+     * category.
+     */
+    public boolean hasGeneratedPreview(int widgetCategory) {
+        if (!Flags.enableGeneratedPreviews() || generatedPreviews == null) {
+            return false;
+        }
+        return generatedPreviews.contains(widgetCategory);
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 8f5e2b6..c75f9d1 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.widget;
 
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
 import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
 import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
@@ -44,6 +46,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -241,6 +244,11 @@
             mAppWidgetHostViewPreview = createAppWidgetHostView(context);
             setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo,
                     mRemoteViewsPreview);
+        } else if (Flags.enableGeneratedPreviews()
+                && item.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)) {
+            mAppWidgetHostViewPreview = createAppWidgetHostView(context);
+            setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo,
+                    item.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN));
         } else if (item.hasPreviewLayout()) {
             // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview
             // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView,
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 058523b..52767a4 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -24,8 +24,11 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.widget.RemoteViews;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -130,6 +133,23 @@
                 appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED);
     }
 
+
+    /**
+     * Load RemoteViews preview for this provider if available.
+     *
+     * @param info The provider info for the widget you want to preview.
+     * @param widgetCategory The widget category for which you want to display previews.
+     *
+     * @return Returns the widget preview that matches selected category, if available.
+     */
+    @Nullable
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public RemoteViews loadGeneratedPreview(@NonNull AppWidgetProviderInfo info,
+            int widgetCategory) {
+        if (!android.appwidget.flags.Flags.generatedPreviews()) return null;
+        return mAppWidgetManager.getWidgetPreview(info.provider, info.getProfile(), widgetCategory);
+    }
+
     private static Stream<AppWidgetProviderInfo> allWidgetsSteam(Context context) {
         AppWidgetManager awm = context.getSystemService(AppWidgetManager.class);
         return Stream.concat(
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 4c5bfd8..8b983fc 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -147,7 +147,8 @@
                         LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
 
                 widgetsAndShortcuts.add(new WidgetItem(
-                        launcherWidgetInfo, idp, app.getIconCache(), app.getContext()));
+                        launcherWidgetInfo, idp, app.getIconCache(), app.getContext(),
+                        widgetManager));
                 updatedItems.add(launcherWidgetInfo);
             }
 
@@ -206,6 +207,7 @@
 
     public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
             LauncherAppState app) {
+        WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext());
         for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
             if (packageNames.contains(entry.getKey().packageName)) {
                 List<WidgetItem> items = entry.getValue();
@@ -219,7 +221,7 @@
                         } else {
                             items.set(i, new WidgetItem(item.widgetInfo,
                                     app.getInvariantDeviceProfile(), app.getIconCache(),
-                                    app.getContext()));
+                                    app.getContext(), widgetManager));
                         }
                     }
                 }
@@ -337,4 +339,4 @@
             return mMap.values();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 310e418..b053bbe 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -127,6 +127,7 @@
         "testables",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "android.appwidget.flags-aconfig-java",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
diff --git a/tests/res/layout/test_layout_appwidget_red.xml b/tests/res/layout/test_layout_appwidget_red.xml
index 48d3e81..0f2bda3 100644
--- a/tests/res/layout/test_layout_appwidget_red.xml
+++ b/tests/res/layout/test_layout_appwidget_red.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/content"
     android:orientation="vertical"
     android:background="#FFFF0000"
     android:layout_width="match_parent"
diff --git a/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
new file mode 100644
index 0000000..11855e6
--- /dev/null
+++ b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -0,0 +1,142 @@
+package com.android.launcher3.widget
+
+import android.appwidget.AppWidgetProviderInfo
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.LayoutInflater
+import android.widget.RemoteViews
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags.FLAG_ENABLE_GENERATED_PREVIEWS
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.IconProvider
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.tests.R
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.Executors
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GeneratedPreviewTest {
+    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+    private val providerName =
+        ComponentName(
+            "com.android.launcher3.tests",
+            "com.android.launcher3.testcomponent.AppWidgetNoConfig"
+        )
+    private val generatedPreviewLayout = R.layout.test_layout_appwidget_blue
+    private lateinit var context: Context
+    private lateinit var generatedPreview: RemoteViews
+    private lateinit var widgetCell: WidgetCell
+    private lateinit var helper: WidgetManagerHelper
+    private lateinit var appWidgetProviderInfo: LauncherAppWidgetProviderInfo
+    private lateinit var widgetItem: WidgetItem
+
+    @Before
+    fun setup() {
+        context = getApplicationContext()
+        generatedPreview = RemoteViews(context.packageName, generatedPreviewLayout)
+        widgetCell =
+            LayoutInflater.from(ActivityContextWrapper(context))
+                .inflate(com.android.launcher3.R.layout.widget_cell, null) as WidgetCell
+        appWidgetProviderInfo =
+            AppWidgetProviderInfo()
+                .apply {
+                    generatedPreviewCategories = WIDGET_CATEGORY_HOME_SCREEN
+                    provider = providerName
+                    providerInfo = ActivityInfo().apply { applicationInfo = ApplicationInfo() }
+                }
+                .let { LauncherAppWidgetProviderInfo.fromProviderInfo(context, it) }
+        helper =
+            object : WidgetManagerHelper(context) {
+                override fun loadGeneratedPreview(
+                    info: AppWidgetProviderInfo,
+                    widgetCategory: Int
+                ) =
+                    generatedPreview.takeIf {
+                        info === appWidgetProviderInfo &&
+                            widgetCategory == WIDGET_CATEGORY_HOME_SCREEN
+                    }
+            }
+        createWidgetItem()
+    }
+
+    private fun createWidgetItem() {
+        Executors.MODEL_EXECUTOR.submit {
+                val idp = InvariantDeviceProfile()
+                widgetItem =
+                    WidgetItem(
+                        appWidgetProviderInfo,
+                        idp,
+                        IconCache(context, idp, null, IconProvider(context)),
+                        context,
+                        helper,
+                    )
+            }
+            .get()
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetItem_hasGeneratedPreview() {
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isTrue()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse()
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetItem_hasGeneratedPreview_noPreview() {
+        appWidgetProviderInfo.generatedPreviewCategories = 0
+        createWidgetItem()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isFalse()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse()
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetItem_hasGeneratedPreview_flagDisabled() {
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isFalse()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse()
+        assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse()
+    }
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetItem_getGeneratedPreview() {
+        val preview = widgetItem.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN)
+        assertThat(preview).isEqualTo(generatedPreview)
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetCell_showGeneratedPreview() {
+        widgetCell.applyFromCellItem(widgetItem)
+        assertThat(widgetCell.appWidgetHostViewPreview).isNotNull()
+        assertThat(widgetCell.appWidgetHostViewPreview?.appWidgetInfo)
+            .isEqualTo(appWidgetProviderInfo)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS)
+    fun widgetCell_showGeneratedPreview_flagDisabled() {
+        widgetCell.applyFromCellItem(widgetItem)
+        assertThat(widgetCell.appWidgetHostViewPreview).isNull()
+    }
+}
diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 60590e7..0286279 100644
--- a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import org.junit.Before;
@@ -137,6 +138,7 @@
     }
 
     private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        WidgetManagerHelper widgetManager = new WidgetManagerHelper(mContext);
         ArrayList<WidgetItem> widgetItems = new ArrayList<>();
         for (int i = 0; i < numOfWidgets; i++) {
             ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
@@ -144,7 +146,7 @@
 
             widgetItems.add(new WidgetItem(
                     LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
-                    mTestProfile, mIconCache, mContext));
+                    mTestProfile, mIconCache, mContext, widgetManager));
         }
         return widgetItems;
     }