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);
+ }
+}