Initial setup for widgets in Search

LiveSearchManager creates AppWidgetHost when user starts a new search session and destroys it when user returns to home. In addition, it also manages the creation and caching of PlaceholderSearchWidget which can be used to create AppWidgetHostViews.
Bug: 168321831
Test: Manual

Change-Id: I06a893028e55aa6e0702a4f1cd7a2edbb1f61671
diff --git a/res/layout/search_result_widget_live.xml b/res/layout/search_result_widget_live.xml
new file mode 100644
index 0000000..0dd8a06
--- /dev/null
+++ b/res/layout/search_result_widget_live.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.launcher3.views.SearchResultWidget android:layout_height="wrap_content"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:gravity="center"
+    android:layout_width="match_parent" />
\ No newline at end of file
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 168d9c4..bc3e341 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -36,7 +36,7 @@
     private static final Rect sTmpRect = new Rect();
 
     // Represents the cell size on the grid in the two orientations.
-    private static final MainThreadInitializedObject<Point[]> CELL_SIZE =
+    public static final MainThreadInitializedObject<Point[]> CELL_SIZE =
             new MainThreadInitializedObject<>(c -> {
                 InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
                 return new Point[] {inv.landscapeProfile.getCellSize(),
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 699734c..38ebe14 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -111,6 +111,7 @@
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.allapps.search.LiveSearchManager;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -276,6 +277,8 @@
 
     private LifecycleRegistry mLifecycleRegistry;
 
+    private LiveSearchManager mLiveSearchManager;
+
     @Thunk
     Workspace mWorkspace;
     @Thunk
@@ -389,6 +392,8 @@
         mIconCache = app.getIconCache();
         mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
 
+        mLiveSearchManager = new LiveSearchManager(this);
+
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new StateManager<>(this, NORMAL);
@@ -492,6 +497,10 @@
         return mLifecycleRegistry;
     }
 
+    public LiveSearchManager getLiveSearchManager() {
+        return mLiveSearchManager;
+    }
+
     protected LauncherOverlayManager getDefaultOverlay() {
         return new LauncherOverlayManager() { };
     }
@@ -1583,6 +1592,7 @@
         mAppTransitionManager.unregisterRemoteAnimations();
         mUserChangedCallbackCloseable.close();
         mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+        mLiveSearchManager.stop();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 434bc14..5b42681 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.views.SearchResultWidget;
 import com.android.systemui.plugins.shared.SearchTarget;
 
 import java.util.List;
@@ -89,6 +90,8 @@
 
     public static final int VIEW_TYPE_SEARCH_ICON = 1 << 14;
 
+    public static final int VIEW_TYPE_SEARCH_WIDGET_LIVE = 1 << 15;
+
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON | VIEW_TYPE_SEARCH_ICON;
@@ -181,7 +184,8 @@
                     || viewType == VIEW_TYPE_SEARCH_THUMBNAIL
                     || viewType == VIEW_TYPE_SEARCH_ICON_ROW
                     || viewType == VIEW_TYPE_SEARCH_ICON
-                    || viewType == VIEW_TYPE_SEARCH_SUGGEST;
+                    || viewType == VIEW_TYPE_SEARCH_SUGGEST
+                    || viewType == VIEW_TYPE_SEARCH_WIDGET_LIVE;
         }
     }
 
@@ -427,6 +431,9 @@
             case VIEW_TYPE_SEARCH_SUGGEST:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.search_result_suggest, parent, false));
+            case VIEW_TYPE_SEARCH_WIDGET_LIVE:
+                return new ViewHolder(mLayoutInflater.inflate(
+                        R.layout.search_result_widget_live, parent, false));
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -469,6 +476,7 @@
             case VIEW_TYPE_SEARCH_PEOPLE:
             case VIEW_TYPE_SEARCH_THUMBNAIL:
             case VIEW_TYPE_SEARCH_SUGGEST:
+            case VIEW_TYPE_SEARCH_WIDGET_LIVE:
                 SearchAdapterItem item =
                         (SearchAdapterItem) mApps.getAdapterItems().get(position);
                 SearchTargetHandler payloadResultView = (SearchTargetHandler) holder.itemView;
@@ -487,6 +495,9 @@
         if (holder.itemView instanceof AllAppsSectionDecorator.SelfDecoratingView) {
             ((AllAppsSectionDecorator.SelfDecoratingView) holder.itemView).removeDecoration();
         }
+        if (holder.itemView instanceof SearchResultWidget) {
+            ((SearchResultWidget) holder.itemView).removeListener();
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 0c488a6..f9ab196 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -264,11 +264,15 @@
         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
             mInsetController.onAnimationEnd(mProgress);
             if (Float.compare(mProgress, 0f) == 0) {
+                mLauncher.getLiveSearchManager().start();
                 EditText editText = mAppsView.getSearchUiManager().getEditText();
                 if (editText != null) {
                     editText.requestFocus();
                 }
             }
+            else {
+                mLauncher.getLiveSearchManager().stop();
+            }
             // TODO: should make the controller hide synchronously
         }
     }
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
new file mode 100644
index 0000000..e8a00d9
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2020 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.allapps.search;
+
+import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
+
+import android.appwidget.AppWidgetHost;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+
+import java.util.HashMap;
+
+/**
+ * Manages Lifecycle for Live search results
+ */
+public class LiveSearchManager {
+
+    public static final int SEARCH_APPWIDGET_HOST_ID = 2048;
+
+    private final Launcher mLauncher;
+    private final AppWidgetManager mAppWidgetManger;
+    private HashMap<ComponentKey, SearchWidgetInfoContainer> mWidgetPlaceholders = new HashMap<>();
+    private SearchWidgetHost mSearchWidgetHost;
+
+    public LiveSearchManager(Launcher launcher) {
+        mLauncher = launcher;
+        mAppWidgetManger = AppWidgetManager.getInstance(launcher);
+    }
+
+    /**
+     * Creates new {@link AppWidgetHostView} from {@link AppWidgetProviderInfo}. Caches views for
+     * quicker result within the same search session
+     */
+    public SearchWidgetInfoContainer getPlaceHolderWidget(AppWidgetProviderInfo providerInfo) {
+        if (mSearchWidgetHost == null) {
+            throw new RuntimeException("AppWidgetHost has not been created yet");
+        }
+
+        ComponentName provider = providerInfo.provider;
+        UserHandle userHandle = providerInfo.getProfile();
+
+        ComponentKey key = new ComponentKey(provider, userHandle);
+        SearchWidgetInfoContainer view = mWidgetPlaceholders.getOrDefault(key, null);
+        if (mWidgetPlaceholders.containsKey(key)) {
+            return mWidgetPlaceholders.get(key);
+        }
+        LauncherAppWidgetProviderInfo pinfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
+                mLauncher, providerInfo);
+        PendingAddWidgetInfo pendingAddWidgetInfo = new PendingAddWidgetInfo(pinfo);
+
+        Bundle options = getDefaultOptionsForWidget(mLauncher, pendingAddWidgetInfo);
+        int appWidgetId = mSearchWidgetHost.allocateAppWidgetId();
+        boolean success = mAppWidgetManger.bindAppWidgetIdIfAllowed(appWidgetId, userHandle,
+                provider, options);
+        if (!success) {
+            mWidgetPlaceholders.put(key, null);
+            return null;
+        }
+
+        view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(mLauncher, appWidgetId,
+                providerInfo);
+        view.setTag(pendingAddWidgetInfo);
+        mWidgetPlaceholders.put(key, view);
+        return view;
+    }
+
+    /**
+     * Start search session
+     */
+    public void start() {
+        stop();
+        mSearchWidgetHost = new SearchWidgetHost(mLauncher);
+        mSearchWidgetHost.startListening();
+    }
+
+    /**
+     * Stop search session
+     */
+    public void stop() {
+        if (mSearchWidgetHost != null) {
+            mSearchWidgetHost.stopListening();
+            mSearchWidgetHost.deleteHost();
+            for (SearchWidgetInfoContainer placeholder : mWidgetPlaceholders.values()) {
+                placeholder.clearListeners();
+            }
+            mWidgetPlaceholders.clear();
+            mSearchWidgetHost = null;
+        }
+    }
+
+    static class SearchWidgetHost extends AppWidgetHost {
+        SearchWidgetHost(Context context) {
+            super(context, SEARCH_APPWIDGET_HOST_ID);
+        }
+
+        @Override
+        protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
+                AppWidgetProviderInfo appWidget) {
+            return new SearchWidgetInfoContainer(context);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
new file mode 100644
index 0000000..b5c2268
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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.allapps.search;
+
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A placeholder {@link AppWidgetHostView} used for managing widget search results
+ */
+public class SearchWidgetInfoContainer extends AppWidgetHostView {
+    private int mAppWidgetId;
+    private AppWidgetProviderInfo mProviderInfo;
+    private RemoteViews mViews;
+    private List<AppWidgetHostView> mListeners = new ArrayList<>();
+
+    public SearchWidgetInfoContainer(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
+        mAppWidgetId = appWidgetId;
+        mProviderInfo = info;
+        for (AppWidgetHostView listener : mListeners) {
+            listener.setAppWidget(mAppWidgetId, mProviderInfo);
+        }
+    }
+
+    @Override
+    public void updateAppWidget(RemoteViews remoteViews) {
+        mViews = remoteViews;
+        for (AppWidgetHostView listener : mListeners) {
+            listener.updateAppWidget(remoteViews);
+        }
+    }
+
+    /**
+     * Create a live {@link AppWidgetHostView} from placeholder
+     */
+    public void attachWidget(AppWidgetHostView hv) {
+        hv.setTag(getTag());
+        hv.setAppWidget(mAppWidgetId, mProviderInfo);
+        hv.updateAppWidget(mViews);
+        mListeners.add(hv);
+    }
+
+    /**
+     * stops AppWidgetHostView from getting updates
+     */
+    public void detachWidget(AppWidgetHostView hostView) {
+        mListeners.remove(hostView);
+    }
+
+    /**
+     * Removes all AppWidgetHost update listeners
+     */
+    public void clearListeners() {
+        mListeners.clear();
+    }
+}
diff --git a/src/com/android/launcher3/views/SearchResultWidget.java b/src/com/android/launcher3/views/SearchResultWidget.java
new file mode 100644
index 0000000..65131be
--- /dev/null
+++ b/src/com/android/launcher3/views/SearchResultWidget.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 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.views;
+
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AppWidgetResizeFrame;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.allapps.search.AllAppsSearchBarController;
+import com.android.launcher3.allapps.search.SearchEventTracker;
+import com.android.launcher3.allapps.search.SearchWidgetInfoContainer;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.systemui.plugins.shared.SearchTarget;
+import com.android.systemui.plugins.shared.SearchTargetEvent;
+
+/**
+ * displays live version of a widget upon receiving {@link AppWidgetProviderInfo} from Search
+ * provider
+ */
+public class SearchResultWidget extends RelativeLayout implements
+        AllAppsSearchBarController.SearchTargetHandler {
+
+    private static final String TAG = "SearchResultWidget";
+
+    public static final String TARGET_TYPE_WIDGET_LIVE = "widget";
+
+    private final Launcher mLauncher;
+    private final AppWidgetHostView mHostView;
+
+    private SearchTarget mSearchTarget;
+    private AppWidgetProviderInfo mProviderInfo;
+
+    private SearchWidgetInfoContainer mInfoContainer;
+
+    public SearchResultWidget(@NonNull Context context) {
+        this(context, null, 0);
+    }
+
+    public SearchResultWidget(@NonNull Context context,
+            @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SearchResultWidget(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mHostView = new AppWidgetHostView(context);
+        mLauncher = Launcher.getLauncher(context);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        addView(mHostView);
+    }
+
+    @Override
+    public void applySearchTarget(SearchTarget searchTarget) {
+        if (searchTarget.getExtras() == null
+                || searchTarget.getExtras().getParcelable("provider") == null) {
+            setVisibility(GONE);
+            return;
+        }
+        AppWidgetProviderInfo providerInfo = searchTarget.getExtras().getParcelable("provider");
+        if (mProviderInfo != null && providerInfo.provider.equals(mProviderInfo.provider)
+                && providerInfo.getProfile().equals(mProviderInfo.getProfile())) {
+            return;
+        }
+        removeListener();
+
+        mSearchTarget = searchTarget;
+        mProviderInfo = providerInfo;
+
+        mInfoContainer = mLauncher.getLiveSearchManager().getPlaceHolderWidget(providerInfo);
+        if (mInfoContainer == null) {
+            setVisibility(GONE);
+            return;
+        }
+        setVisibility(VISIBLE);
+        mInfoContainer.attachWidget(mHostView);
+        PendingAddWidgetInfo info = (PendingAddWidgetInfo) mHostView.getTag();
+        int[] size = mLauncher.getWorkspace().estimateItemSize(info);
+        mHostView.getLayoutParams().width = size[0];
+        mHostView.getLayoutParams().height = size[1];
+        AppWidgetResizeFrame.updateWidgetSizeRanges(mHostView, mLauncher, info.spanX,
+                info.spanY);
+        mHostView.requestLayout();
+
+
+    }
+
+    /**
+     * Stops hostView from getting updates on a widget provider
+     */
+    public void removeListener() {
+        if (mInfoContainer != null) {
+            mInfoContainer.detachWidget(mHostView);
+        }
+    }
+
+    @Override
+    public void handleSelection(int eventType) {
+        SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent(
+                new SearchTargetEvent.Builder(mSearchTarget, eventType).build());
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            handleSelection(SearchTargetEvent.CHILD_SELECT);
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+}