Move most of the functions in LauncherAppWidgetHost to LauncherWidgetHolder

Test: N/A
Bug: 235358918
Change-Id: I343419376491203a195154f2766b12e5def38879
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 75e89b2..8604f1a 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -2,7 +2,6 @@
 
 import static android.os.Process.myUserHandle;
 
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.BroadcastReceiver;
@@ -12,6 +11,7 @@
 import android.database.Cursor;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -21,7 +21,6 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
@@ -32,7 +31,7 @@
         if (AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED.equals(intent.getAction())) {
             int hostId = intent.getIntExtra(AppWidgetManager.EXTRA_HOST_ID, 0);
             Log.d(TAG, "Widget ID map received for host:" + hostId);
-            if (hostId != LauncherAppWidgetHost.APPWIDGET_HOST_ID) {
+            if (hostId != LauncherWidgetHolder.APPWIDGET_HOST_ID) {
                 return;
             }
 
@@ -50,11 +49,11 @@
      * Updates the app widgets whose id has changed during the restore process.
      */
     @WorkerThread
-    public static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
-        AppWidgetHost appWidgetHost = new LauncherAppWidgetHost(context);
+    public static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds,
+            @NonNull LauncherWidgetHolder holder) {
         if (WidgetsModel.GO_DISABLE_WIDGETS) {
             Log.e(TAG, "Skipping widget ID remap as widgets not supported");
-            appWidgetHost.deleteHost();
+            holder.deleteHost();
             return;
         }
         if (!RestoreDbTask.isPending(context)) {
@@ -63,7 +62,7 @@
             Log.e(TAG, "Skipping widget ID remap as DB already in use");
             for (int widgetId : newWidgetIds) {
                 Log.d(TAG, "Deleting widgetId: " + widgetId);
-                appWidgetHost.deleteAppWidgetId(widgetId);
+                holder.deleteAppWidgetId(widgetId);
             }
             return;
         }
@@ -100,7 +99,7 @@
                 try {
                     if (!cursor.moveToFirst()) {
                         // The widget no long exists.
-                        appWidgetHost.deleteAppWidgetId(newWidgetIds[i]);
+                        holder.deleteAppWidgetId(newWidgetIds[i]);
                     }
                 } finally {
                     cursor.close();
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 64666b0..0003510 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import android.appwidget.AppWidgetHost;
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
@@ -74,7 +73,7 @@
     private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
     private static final String LAYOUT_RES = "default_layout";
 
-    static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
+    static AutoInstallsLayout get(Context context, LauncherWidgetHolder appWidgetHolder,
             LayoutParserCallback callback) {
         Pair<String, Resources> customizationApkInfo = PackageManagerHelper.findSystemApk(
                 ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
@@ -109,7 +108,7 @@
             Log.e(TAG, "Layout definition not found in package: " + pkg);
             return null;
         }
-        return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
+        return new AutoInstallsLayout(context, appWidgetHolder, callback, targetRes, layoutId,
                 TAG_WORKSPACE);
     }
 
@@ -156,7 +155,7 @@
     @Thunk
     final Context mContext;
     @Thunk
-    final AppWidgetHost mAppWidgetHost;
+    final LauncherWidgetHolder mAppWidgetHolder;
     protected final LayoutParserCallback mCallback;
 
     protected final PackageManager mPackageManager;
@@ -174,17 +173,17 @@
 
     protected SQLiteDatabase mDb;
 
-    public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
+    public AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder,
             LayoutParserCallback callback, Resources res,
             int layoutId, String rootTag) {
-        this(context, appWidgetHost, callback, res, () -> res.getXml(layoutId), rootTag);
+        this(context, appWidgetHolder, callback, res, () -> res.getXml(layoutId), rootTag);
     }
 
-    public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
+    public AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder,
             LayoutParserCallback callback, Resources res,
             Supplier<XmlPullParser> initialLayoutSupplier, String rootTag) {
         mContext = context;
-        mAppWidgetHost = appWidgetHost;
+        mAppWidgetHolder = appWidgetHolder;
         mCallback = callback;
 
         mPackageManager = context.getPackageManager();
diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java
index 4daca8b..d8558f0 100644
--- a/src/com/android/launcher3/DefaultLayoutParser.java
+++ b/src/com/android/launcher3/DefaultLayoutParser.java
@@ -1,6 +1,5 @@
 package com.android.launcher3;
 
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -55,9 +54,9 @@
     private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
             "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
 
-    public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost,
+    public DefaultLayoutParser(Context context, LauncherWidgetHolder appWidgetHolder,
             LayoutParserCallback callback, Resources sourceRes, int layoutId) {
-        super(context, appWidgetHost, callback, sourceRes, layoutId, TAG_FAVORITES);
+        super(context, appWidgetHolder, callback, sourceRes, layoutId, TAG_FAVORITES);
     }
 
     @Override
@@ -336,11 +335,11 @@
             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
             int insertedId = -1;
             try {
-                int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
+                int appWidgetId = mAppWidgetHolder.allocateAppWidgetId();
 
                 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn)) {
                     Log.e(TAG, "Unable to bind app widget id " + cn);
-                    mAppWidgetHost.deleteAppWidgetId(appWidgetId);
+                    mAppWidgetHolder.deleteAppWidgetId(appWidgetId);
                     return -1;
                 }
 
@@ -349,7 +348,7 @@
                 mValues.put(Favorites._ID, mCallback.generateNewItemId());
                 insertedId = mCallback.insertAndCheck(mDb, mValues);
                 if (insertedId < 0) {
-                    mAppWidgetHost.deleteAppWidgetId(appWidgetId);
+                    mAppWidgetHolder.deleteAppWidgetId(appWidgetId);
                     return insertedId;
                 }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ab7f622..de2ef55 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1720,6 +1720,7 @@
         } catch (NullPointerException ex) {
             Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex);
         }
+        mAppWidgetHolder.destroy();
 
         TextKeyListener.getInstance().release();
         clearPendingBinds();
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 58e85fe..a6731a5 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -22,7 +22,6 @@
 
 import android.annotation.TargetApi;
 import android.app.backup.BackupManager;
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -55,6 +54,8 @@
 import android.util.Log;
 import android.util.Xml;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
@@ -70,7 +71,6 @@
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
-import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 import org.xmlpull.v1.XmlPullParser;
 
@@ -255,17 +255,20 @@
                     values.getAsString(Favorites.APPWIDGET_PROVIDER));
 
             if (cn != null) {
+                LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
                 try {
-                    AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
-                    int appWidgetId = widgetHost.allocateAppWidgetId();
+                    int appWidgetId = widgetHolder.allocateAppWidgetId();
                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
                     if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
-                        widgetHost.deleteAppWidgetId(appWidgetId);
+                        widgetHolder.deleteAppWidgetId(appWidgetId);
                         return false;
                     }
                 } catch (RuntimeException e) {
                     Log.e(TAG, "Failed to initialize external widget", e);
                     return false;
+                } finally {
+                    // Necessary to destroy the holder to free up possible activity context
+                    widgetHolder.destroy();
                 }
             } else {
                 return false;
@@ -533,10 +536,10 @@
         if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
             Log.d(TAG, "loading default workspace");
 
-            AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
-            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
+            LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
+            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
             if (loader == null) {
-                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
+                loader = AutoInstallsLayout.get(getContext(), widgetHolder, mOpenHelper);
             }
             if (loader == null) {
                 final Partner partner = Partner.get(getContext().getPackageManager());
@@ -545,7 +548,7 @@
                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                             "xml", partner.getPackageName());
                     if (workspaceResId != 0) {
-                        loader = new DefaultLayoutParser(getContext(), widgetHost,
+                        loader = new DefaultLayoutParser(getContext(), widgetHolder,
                                 mOpenHelper, partnerRes, workspaceResId);
                     }
                 }
@@ -553,7 +556,7 @@
 
             final boolean usingExternallyProvidedLayout = loader != null;
             if (loader == null) {
-                loader = getDefaultLayoutParser(widgetHost);
+                loader = getDefaultLayoutParser(widgetHolder);
             }
 
             // There might be some partially restored DB items, due to buggy restore logic in
@@ -565,9 +568,10 @@
                 // Unable to load external layout. Cleanup and load the internal layout.
                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
-                        getDefaultLayoutParser(widgetHost));
+                        getDefaultLayoutParser(widgetHolder));
             }
             clearFlagEmptyDbCreated();
+            widgetHolder.destroy();
         }
     }
 
@@ -576,7 +580,8 @@
      *
      * @return the loader if the restrictions are set and the resource exists; null otherwise.
      */
-    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
+    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
+            LauncherWidgetHolder widgetHolder) {
         Context ctx = getContext();
         final String authority;
         if (!TextUtils.isEmpty(mProviderAuthority)) {
@@ -602,7 +607,7 @@
             parser.setInput(new StringReader(layout));
 
             Log.d(TAG, "Loading layout from " + authority);
-            return new AutoInstallsLayout(ctx, widgetHost, mOpenHelper,
+            return new AutoInstallsLayout(ctx, widgetHolder, mOpenHelper,
                     ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
                     () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
         } catch (Exception e) {
@@ -621,7 +626,7 @@
                 .build();
     }
 
-    private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
+    private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
         InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
         int defaultLayout = mUseTestWorkspaceLayout
                 ? TEST_WORKSPACE_LAYOUT_RES_XML : idp.defaultLayoutId;
@@ -631,7 +636,7 @@
             defaultLayout = idp.demoModeLayoutId;
         }
 
-        return new DefaultLayoutParser(getContext(), widgetHost,
+        return new DefaultLayoutParser(getContext(), widgetHolder,
                 mOpenHelper, getContext().getResources(), defaultLayout);
     }
 
@@ -932,40 +937,46 @@
          */
         public void removeGhostWidgets(SQLiteDatabase db) {
             // Get all existing widget ids.
-            final AppWidgetHost host = newLauncherWidgetHost();
-            final int[] allWidgets;
+            final LauncherWidgetHolder holder = newLauncherWidgetHolder();
             try {
-                // Although the method was defined in O, it has existed since the beginning of time,
-                // so it might work on older platforms as well.
-                allWidgets = host.getAppWidgetIds();
-            } catch (IncompatibleClassChangeError e) {
-                Log.e(TAG, "getAppWidgetIds not supported", e);
-                return;
-            }
-            final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db,
-                    Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
-                    "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
-            boolean isAnyWidgetRemoved = false;
-            for (int widgetId : allWidgets) {
-                if (!validWidgets.contains(widgetId)) {
-                    try {
-                        FileLog.d(TAG, "Deleting invalid widget " + widgetId);
-                        host.deleteAppWidgetId(widgetId);
-                        isAnyWidgetRemoved = true;
-                    } catch (RuntimeException e) {
-                        // Ignore
+                final int[] allWidgets;
+                try {
+                    // Although the method was defined in O, it has existed since the beginning of
+                    // time, so it might work on older platforms as well.
+                    allWidgets = holder.getAppWidgetIds();
+                } catch (IncompatibleClassChangeError e) {
+                    Log.e(TAG, "getAppWidgetIds not supported", e);
+                    // Necessary to destroy the holder to free up possible activity context
+                    holder.destroy();
+                    return;
+                }
+                final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db,
+                        Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
+                        "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
+                boolean isAnyWidgetRemoved = false;
+                for (int widgetId : allWidgets) {
+                    if (!validWidgets.contains(widgetId)) {
+                        try {
+                            FileLog.d(TAG, "Deleting invalid widget " + widgetId);
+                            holder.deleteAppWidgetId(widgetId);
+                            isAnyWidgetRemoved = true;
+                        } catch (RuntimeException e) {
+                            // Ignore
+                        }
                     }
                 }
-            }
-            if (isAnyWidgetRemoved) {
-                final String allWidgetsIds = Arrays.stream(allWidgets).mapToObj(String::valueOf)
-                        .collect(Collectors.joining(",", "[", "]"));
-                final String validWidgetsIds = Arrays.stream(
-                        validWidgets.getArray().toArray()).mapToObj(String::valueOf)
-                        .collect(Collectors.joining(",", "[", "]"));
-                FileLog.d(TAG, "One or more widgets was removed. db_path=" + db.getPath()
-                        + " allWidgetsIds=" + allWidgetsIds
-                        + ", validWidgetsIds=" + validWidgetsIds);
+                if (isAnyWidgetRemoved) {
+                    final String allWidgetsIds = Arrays.stream(allWidgets).mapToObj(String::valueOf)
+                            .collect(Collectors.joining(",", "[", "]"));
+                    final String validWidgetsIds = Arrays.stream(
+                                    validWidgets.getArray().toArray()).mapToObj(String::valueOf)
+                            .collect(Collectors.joining(",", "[", "]"));
+                    FileLog.d(TAG, "One or more widgets was removed. db_path=" + db.getPath()
+                            + " allWidgetsIds=" + allWidgetsIds
+                            + ", validWidgetsIds=" + validWidgetsIds);
+                }
+            } finally {
+                holder.destroy();
             }
         }
 
@@ -1066,8 +1077,12 @@
             return mMaxItemId;
         }
 
-        public AppWidgetHost newLauncherWidgetHost() {
-            return new LauncherAppWidgetHost(mContext);
+        /**
+         * @return A new {@link LauncherWidgetHolder} based on the current context
+         */
+        @NonNull
+        public LauncherWidgetHolder newLauncherWidgetHolder() {
+            return new LauncherWidgetHolder(mContext);
         }
 
         @Override
diff --git a/src/com/android/launcher3/LauncherWidgetHolder.java b/src/com/android/launcher3/LauncherWidgetHolder.java
index 5fcd46f..2612b9c 100644
--- a/src/com/android/launcher3/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/LauncherWidgetHolder.java
@@ -15,17 +15,39 @@
  */
 package com.android.launcher3;
 
+import static android.app.Activity.RESULT_CANCELED;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
+import android.content.ActivityNotFoundException;
 import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.ApiWrapper;
+import com.android.launcher3.widget.DeferredAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.function.IntConsumer;
 
@@ -34,8 +56,36 @@
  * background.
  */
 public class LauncherWidgetHolder {
+    public static final int APPWIDGET_HOST_ID = 1024;
+
+    private static final int FLAG_LISTENING = 1;
+    private static final int FLAG_STATE_IS_NORMAL = 1 << 1;
+    private static final int FLAG_ACTIVITY_STARTED = 1 << 2;
+    private static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
+    private static final int FLAGS_SHOULD_LISTEN =
+            FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
+
     @NonNull
-    private final LauncherAppWidgetHost mWidgetHost;
+    private final Context mContext;
+
+    @NonNull
+    private final AppWidgetHost mWidgetHost;
+
+    @NonNull
+    private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
+    @NonNull
+    private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
+    @NonNull
+    private final SparseArray<LauncherAppWidgetHostView> mDeferredViews = new SparseArray<>();
+    @NonNull
+    private final SparseArray<RemoteViews> mCachedRemoteViews = new SparseArray<>();
+
+    private int mFlags = FLAG_STATE_IS_NORMAL;
+
+    // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
+    private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
+    // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden
+    private static final int SPLASH_SCREEN_STYLE_EMPTY = 0;
 
     public LauncherWidgetHolder(@NonNull Context context) {
         this(context, null);
@@ -43,30 +93,70 @@
 
     public LauncherWidgetHolder(@NonNull Context context,
             @Nullable IntConsumer appWidgetRemovedCallback) {
-        mWidgetHost = new LauncherAppWidgetHost(context, appWidgetRemovedCallback);
+        mContext = context;
+        mWidgetHost = createHost(context, appWidgetRemovedCallback);
+    }
+
+    protected AppWidgetHost createHost(
+            Context context, @Nullable IntConsumer appWidgetRemovedCallback) {
+        return new LauncherAppWidgetHost(context, appWidgetRemovedCallback, this);
     }
 
     /**
      * Starts listening to the widget updates from the server side
      */
     public void startListening() {
-        mWidgetHost.startListening();
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return;
+        }
+        setListeningFlag(true);
+        try {
+            mWidgetHost.startListening();
+        } catch (Exception e) {
+            if (!Utilities.isBinderSizeError(e)) {
+                throw new RuntimeException(e);
+            }
+            // We're willing to let this slide. The exception is being caused by the list of
+            // RemoteViews which is being passed back. The startListening relationship will
+            // have been established by this point, and we will end up populating the
+            // widgets upon bind anyway. See issue 14255011 for more context.
+        }
+
+        // We go in reverse order and inflate any deferred or cached widget
+        for (int i = mViews.size() - 1; i >= 0; i--) {
+            LauncherAppWidgetHostView view = mViews.valueAt(i);
+            if (view instanceof DeferredAppWidgetHostView) {
+                view.reInflate();
+            }
+            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
+                final int appWidgetId = mViews.keyAt(i);
+                if (view == mDeferredViews.get(appWidgetId)) {
+                    // If the widget view was deferred, we'll need to call super.createView here
+                    // to make the binder call to system process to fetch cumulative updates to this
+                    // widget, as well as setting up this view for future updates.
+                    mWidgetHost.createView(view.getLauncher(), appWidgetId,
+                            view.getAppWidgetInfo());
+                    // At this point #onCreateView should have been called, which in turn returned
+                    // the deferred view. There's no reason to keep the reference anymore, so we
+                    // removed it here.
+                    mDeferredViews.remove(appWidgetId);
+                }
+            }
+        }
     }
 
     /**
-     * Set the STARTED state of the widget host
-     * @param isStarted True if setting the host as started, false otherwise
+     * Registers an "activity started/stopped" event.
      */
     public void setActivityStarted(boolean isStarted) {
-        mWidgetHost.setActivityStarted(isStarted);
+        setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted);
     }
 
     /**
-     * Set the RESUMED state of the widget host
-     * @param isResumed True if setting the host as resumed, false otherwise
+     * Registers an "activity paused/resumed" event.
      */
     public void setActivityResumed(boolean isResumed) {
-        mWidgetHost.setActivityResumed(isResumed);
+        setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed);
     }
 
     /**
@@ -74,7 +164,7 @@
      * @param isNormal True if setting the host to be in normal state, false otherwise
      */
     public void setStateIsNormal(boolean isNormal) {
-        mWidgetHost.setStateIsNormal(isNormal);
+        setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal);
     }
 
     /**
@@ -83,6 +173,7 @@
      */
     public void deleteAppWidgetId(int appWidgetId) {
         mWidgetHost.deleteAppWidgetId(appWidgetId);
+        mViews.remove(appWidgetId);
     }
 
     /**
@@ -91,20 +182,37 @@
      * @param view The {@link PendingAppWidgetHostView} of the app widget
      */
     public void addPendingView(int appWidgetId, @NonNull PendingAppWidgetHostView view) {
-        mWidgetHost.addPendingView(appWidgetId, view);
+        mPendingViews.put(appWidgetId, view);
     }
 
     /**
-     * @return True if the host is listening to the widget updates, false otherwise
+     * @param appWidgetId The app widget id of the specified widget
+     * @return The {@link PendingAppWidgetHostView} of the widget if it exists, null otherwise
      */
-    public boolean isListening() {
-        return mWidgetHost.isListening();
+    @Nullable
+    protected PendingAppWidgetHostView getPendingView(int appWidgetId) {
+        return mPendingViews.get(appWidgetId);
+    }
+
+    protected void removePendingView(int appWidgetId) {
+        mPendingViews.remove(appWidgetId);
+    }
+
+    /**
+     * Called when the launcher is destroyed
+     */
+    public void destroy() {
+        // No-op
     }
 
     /**
      * @return The allocated app widget id if allocation is successful, returns -1 otherwise
      */
     public int allocateAppWidgetId() {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return AppWidgetManager.INVALID_APPWIDGET_ID;
+        }
+
         return mWidgetHost.allocateAppWidgetId();
     }
 
@@ -112,18 +220,18 @@
      * Add a listener that is triggered when the providers of the widgets are changed
      * @param listener The listener that notifies when the providers changed
      */
-    public void addProviderChangeListener(
-            @NonNull LauncherAppWidgetHost.ProviderChangedListener listener) {
-        mWidgetHost.addProviderChangeListener(listener);
+    public void addProviderChangeListener(@NonNull ProviderChangedListener listener) {
+        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
+        tempHost.addProviderChangeListener(listener);
     }
 
     /**
      * Remove the specified listener from the host
      * @param listener The listener that is to be removed from the host
      */
-    public void removeProviderChangeListener(
-            LauncherAppWidgetHost.ProviderChangedListener listener) {
-        mWidgetHost.removeProviderChangeListener(listener);
+    public void removeProviderChangeListener(ProviderChangedListener listener) {
+        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
+        tempHost.removeProviderChangeListener(listener);
     }
 
     /**
@@ -134,7 +242,40 @@
      */
     public void startConfigActivity(@NonNull BaseDraggingActivity activity, int widgetId,
             int requestCode) {
-        mWidgetHost.startConfigActivity(activity, widgetId, requestCode);
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            sendActionCancelled(activity, requestCode);
+            return;
+        }
+
+        try {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
+            mWidgetHost.startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode,
+                    getConfigurationActivityOptions(activity, widgetId));
+        } catch (ActivityNotFoundException | SecurityException e) {
+            Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+            sendActionCancelled(activity, requestCode);
+        }
+    }
+
+    private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
+        MAIN_EXECUTOR.execute(
+                () -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
+    }
+
+    /**
+     * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching
+     * the configuration of the {@code widgetId} app widget, or null of options cannot be produced.
+     */
+    @Nullable
+    protected Bundle getConfigurationActivityOptions(@NonNull BaseDraggingActivity activity,
+            int widgetId) {
+        LauncherAppWidgetHostView view = mViews.get(widgetId);
+        if (view == null) return null;
+        Object tag = view.getTag();
+        if (!(tag instanceof ItemInfo)) return null;
+        Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
+        bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
+        return bundle;
     }
 
     /**
@@ -146,27 +287,38 @@
      */
     public void startBindFlow(@NonNull BaseActivity activity,
             int appWidgetId, @NonNull AppWidgetProviderInfo info, int requestCode) {
-        mWidgetHost.startBindFlow(activity, appWidgetId, info, requestCode);
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            sendActionCancelled(activity, requestCode);
+            return;
+        }
+
+        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
+                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
+                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
+        // TODO: we need to make sure that this accounts for the options bundle.
+        // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
+        activity.startActivityForResult(intent, requestCode);
     }
 
     /**
      * Stop the host from listening to the widget updates
      */
     public void stopListening() {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return;
+        }
+
         mWidgetHost.stopListening();
+        setListeningFlag(false);
     }
 
-    /**
-     * Create a view for the specified app widget
-     * @param context The activity context for which the view is created
-     * @param appWidgetId The ID of the widget
-     * @param info The {@link LauncherAppWidgetProviderInfo} of the widget
-     * @return A view for the widget
-     */
-    @NonNull
-    public AppWidgetHostView createView(@NonNull Context context, int appWidgetId,
-            @NonNull LauncherAppWidgetProviderInfo info) {
-        return mWidgetHost.createView(context, appWidgetId, info);
+    protected void setListeningFlag(final boolean isListening) {
+        if (isListening) {
+            mFlags |= FLAG_LISTENING;
+            return;
+        }
+        mFlags &= ~FLAG_LISTENING;
     }
 
     /**
@@ -174,14 +326,184 @@
      * @param handler The interaction handler
      */
     public void setInteractionHandler(
-            @Nullable LauncherAppWidgetHost.LauncherWidgetInteractionHandler handler) {
+            @Nullable LauncherWidgetInteractionHandler handler) {
         ApiWrapper.setHostInteractionHandler(mWidgetHost, handler);
     }
 
     /**
+     * Delete the host
+     */
+    public void deleteHost() {
+        mWidgetHost.deleteHost();
+    }
+
+    /**
+     * @return The app widget ids
+     */
+    @NonNull
+    public int[] getAppWidgetIds() {
+        return mWidgetHost.getAppWidgetIds();
+    }
+
+    /**
+     * Create a view for the specified app widget
+     * @param context The activity context for which the view is created
+     * @param appWidgetId The ID of the widget
+     * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget
+     * @return A view for the widget
+     */
+    @NonNull
+    public AppWidgetHostView createView(@NonNull Context context, int appWidgetId,
+            @NonNull LauncherAppWidgetProviderInfo appWidget) {
+        if (appWidget.isCustomWidget()) {
+            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
+            lahv.setAppWidget(0, appWidget);
+            CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
+            return lahv;
+        } else if ((mFlags & FLAG_LISTENING) == 0) {
+            // Since the launcher hasn't started listening to widget updates, we can't simply call
+            // super.createView here because the later will make a binder call to retrieve
+            // RemoteViews from system process.
+            // TODO: have launcher always listens to widget updates in background so that this
+            //  check can be removed altogether.
+            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()
+                    && mCachedRemoteViews.get(appWidgetId) != null) {
+                // We've found RemoteViews from cache for this widget, so we will instantiate a
+                // widget host view and populate it with the cached RemoteViews.
+                final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context);
+                view.setAppWidget(appWidgetId, appWidget);
+                view.updateAppWidget(mCachedRemoteViews.get(appWidgetId));
+                mDeferredViews.put(appWidgetId, view);
+                mViews.put(appWidgetId, view);
+                return view;
+            } else {
+                // When cache misses, a placeholder for the widget will be returned instead.
+                DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
+                view.setAppWidget(appWidgetId, appWidget);
+                mViews.put(appWidgetId, view);
+                return view;
+            }
+        } else {
+            try {
+                return mWidgetHost.createView(context, appWidgetId, appWidget);
+            } catch (Exception e) {
+                if (!Utilities.isBinderSizeError(e)) {
+                    throw new RuntimeException(e);
+                }
+
+                // If the exception was thrown while fetching the remote views, let the view stay.
+                // This will ensure that if the widget posts a valid update later, the view
+                // will update.
+                LauncherAppWidgetHostView view = mViews.get(appWidgetId);
+                if (view == null) {
+                    view = onCreateView(mContext, appWidgetId, appWidget);
+                }
+                view.setAppWidget(appWidgetId, appWidget);
+                view.switchToErrorView();
+                return view;
+            }
+        }
+    }
+
+    /**
+     * Listener for getting notifications on provider changes.
+     */
+    public interface ProviderChangedListener {
+        /**
+         * Notify the listener that the providers have changed
+         */
+        void notifyWidgetProvidersChanged();
+    }
+
+    /**
+     * Called to return a proper view when creating a view
+     * @param context The context for which the widget view is created
+     * @param appWidgetId The ID of the added widget
+     * @param appWidget The provider info of the added widget
+     * @return A view for the specified app widget
+     */
+    @NonNull
+    public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
+            AppWidgetProviderInfo appWidget) {
+        final LauncherAppWidgetHostView view;
+        if (getPendingView(appWidgetId) != null) {
+            view = getPendingView(appWidgetId);
+            removePendingView(appWidgetId);
+        } else if (mDeferredViews.get(appWidgetId) != null) {
+            // In case the widget view is deferred, we will simply return the deferred view as
+            // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher
+            // already added the former to the workspace.
+            view = mDeferredViews.get(appWidgetId);
+        } else {
+            view = new LauncherAppWidgetHostView(context);
+        }
+        mViews.put(appWidgetId, view);
+        return view;
+    }
+
+    /**
      * Clears all the views from the host
      */
     public void clearViews() {
-        mWidgetHost.clearViews();
+        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
+        tempHost.clearViews();
+        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
+            // First, we clear any previously cached content from existing widgets
+            mCachedRemoteViews.clear();
+            mDeferredViews.clear();
+            // Then we proceed to cache the content from the widgets
+            for (int i = 0; i < mViews.size(); i++) {
+                final int appWidgetId = mViews.keyAt(i);
+                final LauncherAppWidgetHostView view = mViews.get(appWidgetId);
+                mCachedRemoteViews.put(appWidgetId, view.getLastRemoteViews());
+            }
+        }
+        mViews.clear();
+    }
+
+    /**
+     * @return True if the host is listening to the updates, false otherwise
+     */
+    public boolean isListening() {
+        return (mFlags & FLAG_LISTENING) != 0;
+    }
+
+    /**
+     * Sets or unsets a flag the can change whether the widget host should be in the listening
+     * state.
+     */
+    private void setShouldListenFlag(int flag, boolean on) {
+        if (on) {
+            mFlags |= flag;
+        } else {
+            mFlags &= ~flag;
+        }
+
+        final boolean listening = isListening();
+        if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) {
+            // Postpone starting listening until all flags are on.
+            startListening();
+        } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
+            // Postpone stopping listening until the activity is stopped.
+            stopListening();
+        }
+    }
+
+    /**
+     * Set as a substitution for the hidden interaction handler in RemoteViews
+     */
+    public interface LauncherWidgetInteractionHandler {
+        /**
+         * Invoked when the user performs an interaction on the View.
+         *
+         * @param view the View with which the user interacted
+         * @param pendingIntent the base PendingIntent associated with the view
+         * @param response the response to the interaction, which knows how to fill in the
+         *                 attached PendingIntent
+         */
+        boolean onInteraction(
+                View view,
+                PendingIntent pendingIntent,
+                RemoteViews.RemoteResponse response);
     }
 }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 27e1ba1..39679a9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -63,6 +63,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.LauncherWidgetHolder.ProviderChangedListener;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.Interpolators;
@@ -107,7 +108,6 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.WallpaperOffsetInterpolator;
-import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.NavigableAppWidgetHostView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 05b1984..4fb5ff6 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -53,10 +53,13 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherWidgetHolder;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -69,7 +72,6 @@
 import com.android.launcher3.views.AbstractSlideInView;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.widget.AddItemWidgetsBottomSheet;
-import com.android.launcher3.widget.LauncherAppWidgetHost;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.NavigableAppWidgetHostView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -105,7 +107,8 @@
     private WidgetCell mWidgetCell;
 
     // Widget request specific options.
-    private LauncherAppWidgetHost mAppWidgetHost;
+    @Nullable
+    private LauncherWidgetHolder mAppWidgetHolder = null;
     private WidgetManagerHelper mAppWidgetManager;
     private int mPendingBindWidgetId;
     private Bundle mWidgetOptions;
@@ -284,7 +287,7 @@
         mWidgetCell.setRemoteViewsPreview(PinItemDragListener.getPreview(mRequest));
 
         mAppWidgetManager = new WidgetManagerHelper(this);
-        mAppWidgetHost = new LauncherAppWidgetHost(this);
+        mAppWidgetHolder = new LauncherWidgetHolder(this);
 
         PendingAddWidgetInfo pendingInfo =
                 new PendingAddWidgetInfo(widgetInfo, CONTAINER_PIN_WIDGETS);
@@ -338,7 +341,7 @@
             return;
         }
 
-        mPendingBindWidgetId = mAppWidgetHost.allocateAppWidgetId();
+        mPendingBindWidgetId = mAppWidgetHolder.allocateAppWidgetId();
         AppWidgetProviderInfo widgetProviderInfo = mRequest.getAppWidgetProviderInfo(this);
         boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
                 mPendingBindWidgetId, widgetProviderInfo, mWidgetOptions);
@@ -349,7 +352,7 @@
         }
 
         // request bind widget
-        mAppWidgetHost.startBindFlow(this, mPendingBindWidgetId,
+        mAppWidgetHolder.startBindFlow(this, mPendingBindWidgetId,
                 mRequest.getAppWidgetProviderInfo(this), REQUEST_BIND_APPWIDGET);
     }
 
@@ -363,6 +366,15 @@
     }
 
     @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mAppWidgetHolder != null) {
+            // Necessary to destroy the holder to free up possible activity context
+            mAppWidgetHolder.destroy();
+        }
+    }
+
+    @Override
     public void onBackPressed() {
         logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_BACK);
         mSlideInView.close(/* animate= */ true);
@@ -378,7 +390,7 @@
                 acceptWidget(widgetId);
             } else {
                 // Simply wait it out.
-                mAppWidgetHost.deleteAppWidgetId(widgetId);
+                mAppWidgetHolder.deleteAppWidgetId(widgetId);
                 mPendingBindWidgetId = -1;
             }
             return;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 482e923..0646e7f 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -68,6 +68,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherWidgetHolder;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
@@ -99,7 +100,6 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.widget.BaseLauncherAppWidgetHostView;
-import com.android.launcher3.widget.LauncherAppWidgetHost;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.LocalColorExtractor;
 import com.android.launcher3.widget.NavigableAppWidgetHostView;
@@ -554,7 +554,7 @@
     private class LauncherPreviewAppWidgetHost extends AppWidgetHost {
 
         private LauncherPreviewAppWidgetHost(Context context) {
-            super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID);
+            super(context, LauncherWidgetHolder.APPWIDGET_HOST_ID);
         }
 
         @Override
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 48b3acf..70dd98e 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherWidgetHolder;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
@@ -353,9 +354,12 @@
     private void restoreAppWidgetIdsIfExists(Context context) {
         SharedPreferences prefs = Utilities.getPrefs(context);
         if (prefs.contains(APPWIDGET_OLD_IDS) && prefs.contains(APPWIDGET_IDS)) {
+            LauncherWidgetHolder holder = new LauncherWidgetHolder(context);
             AppWidgetsRestoredReceiver.restoreAppWidgetIds(context,
                     IntArray.fromConcatString(prefs.getString(APPWIDGET_OLD_IDS, "")).toArray(),
-                    IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray());
+                    IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray(),
+                    holder);
+            holder.destroy();
         } else {
             FileLog.d(TAG, "No app widget ids to restore.");
         }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index fff8fbb..c57139d 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -16,301 +16,88 @@
 
 package com.android.launcher3.widget;
 
-import static android.app.Activity.RESULT_CANCELED;
+import static com.android.launcher3.LauncherWidgetHolder.APPWIDGET_HOST_ID;
 
-import android.app.PendingIntent;
 import android.appwidget.AppWidgetHost;
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.SparseArray;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.WidgetsModel;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.LauncherWidgetHolder;
 
 import java.util.ArrayList;
 import java.util.function.IntConsumer;
 
-
 /**
  * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
  * which correctly captures all long-press events. This ensures that users can
  * always pick up and move widgets.
  */
 public class LauncherAppWidgetHost extends AppWidgetHost {
+    @NonNull
+    private final ArrayList<LauncherWidgetHolder.ProviderChangedListener>
+            mProviderChangeListeners = new ArrayList<>();
 
-    private static final int FLAG_LISTENING = 1;
-    private static final int FLAG_STATE_IS_NORMAL = 1 << 1;
-    private static final int FLAG_ACTIVITY_STARTED = 1 << 2;
-    private static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
-    private static final int FLAGS_SHOULD_LISTEN =
-            FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
-    // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
-    private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
-    // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden
-    private static final int SPLASH_SCREEN_STYLE_EMPTY = 0;
-
-    public static final int APPWIDGET_HOST_ID = 1024;
-
-    private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
-    private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
-    private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
-    private final SparseArray<LauncherAppWidgetHostView> mDeferredViews = new SparseArray<>();
-    private final SparseArray<RemoteViews> mCachedRemoteViews = new SparseArray<>();
-
+    @NonNull
     private final Context mContext;
-    private int mFlags = FLAG_STATE_IS_NORMAL;
 
-    private IntConsumer mAppWidgetRemovedCallback = null;
+    @Nullable
+    private final IntConsumer mAppWidgetRemovedCallback;
 
-    /**
-     * This serves for the purpose of getting rid of the hidden API calling of InteractionHandler
-     */
-    public interface LauncherWidgetInteractionHandler {
-        /**
-         * Invoked when the user performs an interaction on the View.
-         *
-         * @param view the View with which the user interacted
-         * @param pendingIntent the base PendingIntent associated with the view
-         * @param response the response to the interaction, which knows how to fill in the
-         *                 attached PendingIntent
-         */
-        boolean onInteraction(
-                View view,
-                PendingIntent pendingIntent,
-                RemoteViews.RemoteResponse response);
-    }
+    @NonNull
+    private final LauncherWidgetHolder mHolder;
 
-    public LauncherAppWidgetHost(Context context) {
-        this(context, null);
-    }
-
-    public LauncherAppWidgetHost(Context context,
-            IntConsumer appWidgetRemovedCallback) {
+    public LauncherAppWidgetHost(@NonNull Context context,
+            @Nullable IntConsumer appWidgetRemovedCallback, @NonNull LauncherWidgetHolder holder) {
         super(context, APPWIDGET_HOST_ID);
         mContext = context;
         mAppWidgetRemovedCallback = appWidgetRemovedCallback;
-    }
-
-    @Override
-    protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
-            AppWidgetProviderInfo appWidget) {
-        final LauncherAppWidgetHostView view;
-        if (mPendingViews.get(appWidgetId) != null) {
-            view = mPendingViews.get(appWidgetId);
-            mPendingViews.remove(appWidgetId);
-        } else if (mDeferredViews.get(appWidgetId) != null) {
-            // In case the widget view is deferred, we will simply return the deferred view as
-            // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher
-            // already added the former to the workspace.
-            view = mDeferredViews.get(appWidgetId);
-        } else {
-            view = new LauncherAppWidgetHostView(context);
-        }
-        mViews.put(appWidgetId, view);
-        return view;
-    }
-
-    @Override
-    public void startListening() {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            return;
-        }
-        mFlags |= FLAG_LISTENING;
-        try {
-            super.startListening();
-        } catch (Exception e) {
-            if (!Utilities.isBinderSizeError(e)) {
-                throw new RuntimeException(e);
-            }
-            // We're willing to let this slide. The exception is being caused by the list of
-            // RemoteViews which is being passed back. The startListening relationship will
-            // have been established by this point, and we will end up populating the
-            // widgets upon bind anyway. See issue 14255011 for more context.
-        }
-
-        // We go in reverse order and inflate any deferred or cached widget
-        for (int i = mViews.size() - 1; i >= 0; i--) {
-            LauncherAppWidgetHostView view = mViews.valueAt(i);
-            if (view instanceof DeferredAppWidgetHostView) {
-                view.reInflate();
-            }
-            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-                final int appWidgetId = mViews.keyAt(i);
-                if (view == mDeferredViews.get(appWidgetId)) {
-                    // If the widget view was deferred, we'll need to call super.createView here
-                    // to make the binder call to system process to fetch cumulative updates to this
-                    // widget, as well as setting up this view for future updates.
-                    super.createView(view.mLauncher, appWidgetId, view.getAppWidgetInfo());
-                    // At this point #onCreateView should have been called, which in turn returned
-                    // the deferred view. There's no reason to keep the reference anymore, so we
-                    // removed it here.
-                    mDeferredViews.remove(appWidgetId);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void stopListening() {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            return;
-        }
-        mFlags &= ~FLAG_LISTENING;
-        super.stopListening();
-    }
-
-    public boolean isListening() {
-        return (mFlags & FLAG_LISTENING) != 0;
+        mHolder = holder;
     }
 
     /**
-     * Sets or unsets a flag the can change whether the widget host should be in the listening
-     * state.
+     * Add a listener that is triggered when the providers of the widgets are changed
+     * @param listener The listener that notifies when the providers changed
      */
-    private void setShouldListenFlag(int flag, boolean on) {
-        if (on) {
-            mFlags |= flag;
-        } else {
-            mFlags &= ~flag;
-        }
-
-        final boolean listening = isListening();
-        if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) {
-            // Postpone starting listening until all flags are on.
-            startListening();
-        } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
-            // Postpone stopping listening until the activity is stopped.
-            stopListening();
-        }
+    public void addProviderChangeListener(
+            @NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
+        mProviderChangeListeners.add(listener);
     }
 
     /**
-     * Registers an "entering/leaving Normal state" event.
+     * Remove the specified listener from the host
+     * @param listener The listener that is to be removed from the host
      */
-    public void setStateIsNormal(boolean isNormal) {
-        setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal);
-    }
-
-    /**
-     * Registers an "activity started/stopped" event.
-     */
-    public void setActivityStarted(boolean isStarted) {
-        setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted);
-    }
-
-    /**
-     * Registers an "activity paused/resumed" event.
-     */
-    public void setActivityResumed(boolean isResumed) {
-        setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed);
+    public void removeProviderChangeListener(
+            LauncherWidgetHolder.ProviderChangedListener listener) {
+        mProviderChangeListeners.remove(listener);
     }
 
     @Override
-    public int allocateAppWidgetId() {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            return AppWidgetManager.INVALID_APPWIDGET_ID;
-        }
-
-        return super.allocateAppWidgetId();
-    }
-
-    public void addProviderChangeListener(ProviderChangedListener callback) {
-        mProviderChangeListeners.add(callback);
-    }
-
-    public void removeProviderChangeListener(ProviderChangedListener callback) {
-        mProviderChangeListeners.remove(callback);
-    }
-
     protected void onProvidersChanged() {
         if (!mProviderChangeListeners.isEmpty()) {
-            for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) {
+            for (LauncherWidgetHolder.ProviderChangedListener callback :
+                    new ArrayList<>(mProviderChangeListeners)) {
                 callback.notifyWidgetProvidersChanged();
             }
         }
     }
 
-    public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
-        mPendingViews.put(appWidgetId, view);
-    }
-
-    public AppWidgetHostView createView(Context context, int appWidgetId,
-            LauncherAppWidgetProviderInfo appWidget) {
-        if (appWidget.isCustomWidget()) {
-            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
-            lahv.setAppWidget(0, appWidget);
-            CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
-            return lahv;
-        } else if ((mFlags & FLAG_LISTENING) == 0) {
-            // Since the launcher hasn't started listening to widget updates, we can't simply call
-            // super.createView here because the later will make a binder call to retrieve
-            // RemoteViews from system process.
-            // TODO: have launcher always listens to widget updates in background so that this
-            //  check can be removed altogether.
-            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()
-                    && mCachedRemoteViews.get(appWidgetId) != null) {
-                // We've found RemoteViews from cache for this widget, so we will instantiate a
-                // widget host view and populate it with the cached RemoteViews.
-                final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context);
-                view.setAppWidget(appWidgetId, appWidget);
-                view.updateAppWidget(mCachedRemoteViews.get(appWidgetId));
-                mDeferredViews.put(appWidgetId, view);
-                mViews.put(appWidgetId, view);
-                return view;
-            } else {
-                // When cache misses, a placeholder for the widget will be returned instead.
-                DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
-                view.setAppWidget(appWidgetId, appWidget);
-                mViews.put(appWidgetId, view);
-                return view;
-            }
-        } else {
-            try {
-                return super.createView(context, appWidgetId, appWidget);
-            } catch (Exception e) {
-                if (!Utilities.isBinderSizeError(e)) {
-                    throw new RuntimeException(e);
-                }
-
-                // If the exception was thrown while fetching the remote views, let the view stay.
-                // This will ensure that if the widget posts a valid update later, the view
-                // will update.
-                LauncherAppWidgetHostView view = mViews.get(appWidgetId);
-                if (view == null) {
-                    view = onCreateView(mContext, appWidgetId, appWidget);
-                }
-                view.setAppWidget(appWidgetId, appWidget);
-                view.switchToErrorView();
-                return view;
-            }
-        }
+    @Override
+    @NonNull
+    public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
+            AppWidgetProviderInfo appWidget) {
+        return mHolder.onCreateView(context, appWidgetId, appWidget);
     }
 
     /**
      * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
      */
     @Override
-    protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
+    protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
         LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
                 mContext, appWidget);
         super.onProviderChanged(appWidgetId, info);
@@ -324,6 +111,7 @@
      *
      * @param appWidgetId TODO: make this override when SDK is updated
      */
+    @Override
     public void onAppWidgetRemoved(int appWidgetId) {
         if (mAppWidgetRemovedCallback == null) {
             return;
@@ -331,92 +119,12 @@
         mAppWidgetRemovedCallback.accept(appWidgetId);
     }
 
-    @Override
-    public void deleteAppWidgetId(int appWidgetId) {
-        super.deleteAppWidgetId(appWidgetId);
-        mViews.remove(appWidgetId);
-    }
-
+    /**
+     * The same as super.clearViews(), except with the scope exposed
+     */
     @Override
     public void clearViews() {
         super.clearViews();
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-            // First, we clear any previously cached content from existing widgets
-            mCachedRemoteViews.clear();
-            mDeferredViews.clear();
-            // Then we proceed to cache the content from the widgets
-            for (int i = 0; i < mViews.size(); i++) {
-                final int appWidgetId = mViews.keyAt(i);
-                final LauncherAppWidgetHostView view = mViews.get(appWidgetId);
-                mCachedRemoteViews.put(appWidgetId, view.mLastRemoteViews);
-            }
-        }
-        mViews.clear();
     }
 
-    public void startBindFlow(BaseActivity activity,
-            int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
-
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            sendActionCancelled(activity, requestCode);
-            return;
-        }
-
-        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
-                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
-                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
-                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
-        // TODO: we need to make sure that this accounts for the options bundle.
-        // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
-        activity.startActivityForResult(intent, requestCode);
-    }
-
-    /**
-     * Launches an app widget's configuration activity.
-     * @param activity The activity from which to launch the configuration activity
-     * @param widgetId The id of the bound app widget to be configured
-     * @param requestCode An optional request code to be returned with the result
-     */
-    public void startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode) {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            sendActionCancelled(activity, requestCode);
-            return;
-        }
-
-        try {
-            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
-            startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode,
-                    getConfigurationActivityOptions(activity, widgetId));
-        } catch (ActivityNotFoundException | SecurityException e) {
-            Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
-            sendActionCancelled(activity, requestCode);
-        }
-    }
-
-    /**
-     * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching
-     * the configuration of the {@code widgetId} app widget, or null of options cannot be produced.
-     */
-    @Nullable
-    private Bundle getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId) {
-        LauncherAppWidgetHostView view = mViews.get(widgetId);
-        if (view == null) return null;
-        Object tag = view.getTag();
-        if (!(tag instanceof ItemInfo)) return null;
-        Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
-        bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
-        return bundle;
-    }
-
-    private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
-        new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
-    }
-
-    /**
-     * Listener for getting notifications on provider changes.
-     */
-    public interface ProviderChangedListener {
-
-        void notifyWidgetProvidersChanged();
-    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index bc3889f..990282b 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -37,6 +37,7 @@
 import android.widget.Advanceable;
 import android.widget.RemoteViews;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.CheckLongPressHelper;
@@ -172,6 +173,16 @@
         mReinflateOnConfigChange = !isSameOrientation();
     }
 
+    @NonNull
+    public Launcher getLauncher() {
+        return mLauncher;
+    }
+
+    @Nullable
+    public RemoteViews getLastRemoteViews() {
+        return mLastRemoteViews;
+    }
+
     private boolean isSameOrientation() {
         return mLauncher.getResources().getConfiguration().orientation ==
                 mLauncher.getOrientation();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 21b2647..ee8d8e8 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -52,6 +52,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherWidgetHolder.ProviderChangedListener;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
@@ -65,7 +66,6 @@
 import com.android.launcher3.views.StickyHeaderLayout;
 import com.android.launcher3.views.WidgetsEduView;
 import com.android.launcher3.widget.BaseWidgetSheet;
-import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;