diff --git a/res/layout/search_result_widget_live.xml b/res/layout/search_result_widget_live.xml new file mode 100644 index 0000000000..0dd8a06c9a --- /dev/null +++ b/res/layout/search_result_widget_live.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index 168d9c479c..bc3e341806 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -36,7 +36,7 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O private static final Rect sTmpRect = new Rect(); // Represents the cell size on the grid in the two orientations. - private static final MainThreadInitializedObject CELL_SIZE = + public static final MainThreadInitializedObject 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 699734c0c5..38ebe14d01 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -111,6 +111,7 @@ import com.android.launcher3.allapps.AllAppsContainerView; 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 @@ public class Launcher extends StatefulActivity implements Launche private LifecycleRegistry mLifecycleRegistry; + private LiveSearchManager mLiveSearchManager; + @Thunk Workspace mWorkspace; @Thunk @@ -389,6 +392,8 @@ public class Launcher extends StatefulActivity implements Launche 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 @@ public class Launcher extends StatefulActivity implements Launche return mLifecycleRegistry; } + public LiveSearchManager getLiveSearchManager() { + return mLiveSearchManager; + } + protected LauncherOverlayManager getDefaultOverlay() { return new LauncherOverlayManager() { }; } @@ -1583,6 +1592,7 @@ public class Launcher extends StatefulActivity implements Launche 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 434bc1453d..5b4268111b 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.allapps.search.SearchSectionInfo; 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 class AllAppsGridAdapter extends 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 @@ public class AllAppsGridAdapter extends || 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 @@ public class AllAppsGridAdapter extends 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 @@ public class AllAppsGridAdapter extends 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 @@ public class AllAppsGridAdapter extends 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 0c488a609b..f9ab196792 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -264,11 +264,15 @@ public class AllAppsTransitionController implements StateHandler, 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 0000000000..e8a00d958a --- /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 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 0000000000..b5c22688e5 --- /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 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 0000000000..65131be1a7 --- /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); + } +}