Migrate AllAppsSearch [part 1/3]

[Video attached to bug report]

Setup DeviceSearchAlgorithm to handle on device search

Bug: 161801950
Test: Manual
Change-Id: Ib55f415f9992ceab687bbbfe904d153157541648
This commit is contained in:
Samuel Fufa 2020-07-26 19:11:14 -07:00
parent 0e649985e7
commit df10ff46bd
11 changed files with 268 additions and 81 deletions

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/section_title"
android:textSize="14sp"
android:fontFamily="@style/TextHeadline"
android:layout_width="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:paddingTop="8dp"
android:layout_height="wrap_content"/>

View File

@ -67,6 +67,10 @@
<!-- Label for an icon representing any generic app. [CHAR_LIMIT=50] --> <!-- Label for an icon representing any generic app. [CHAR_LIMIT=50] -->
<string name="label_application">App</string> <string name="label_application">App</string>
<!--All apps Search-->
<!-- Section title for apps [CHAR_LIMIT=50] -->
<string name="search_corpus_apps">Apps</string>
<!-- Popup items --> <!-- Popup items -->
<!-- Text to display as the header above notifications. [CHAR_LIMIT=30] --> <!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
<string name="notifications_header">Notifications</string> <string name="notifications_header">Notifications</string>

View File

@ -66,6 +66,8 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
// A divider that separates the apps list and the search market button // A divider that separates the apps list and the search market button
public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4; public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
public static final int VIEW_TYPE_SEARCH_CORPUS_TITLE = 1 << 5;
// Common view type masks // Common view type masks
public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
@ -274,6 +276,9 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
case VIEW_TYPE_ALL_APPS_DIVIDER: case VIEW_TYPE_ALL_APPS_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate( return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_divider, parent, false)); R.layout.all_apps_divider, parent, false));
case VIEW_TYPE_SEARCH_CORPUS_TITLE:
return new ViewHolder(
mLayoutInflater.inflate(R.layout.search_section_title, parent, false));
default: default:
throw new RuntimeException("Unexpected view type"); throw new RuntimeException("Unexpected view type");
} }
@ -302,6 +307,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
searchView.setVisibility(View.GONE); searchView.setVisibility(View.GONE);
} }
break; break;
case VIEW_TYPE_SEARCH_CORPUS_TITLE:
TextView titleView = (TextView) holder.itemView;
titleView.setText(mApps.getAdapterItems().get(position).searchSectionInfo.getTitle(
titleView.getContext()));
case VIEW_TYPE_ALL_APPS_DIVIDER: case VIEW_TYPE_ALL_APPS_DIVIDER:
// nothing to do // nothing to do
break; break;

View File

@ -107,6 +107,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE, 1);
mViewHeights.clear(); mViewHeights.clear();
mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx); mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);

View File

@ -19,8 +19,9 @@ package com.android.launcher3.allapps;
import android.content.Context; import android.content.Context;
import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.allapps.search.SearchSectionInfo;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LabelComparator; import com.android.launcher3.util.LabelComparator;
@ -30,6 +31,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors;
/** /**
* The alphabetically sorted list of applications. * The alphabetically sorted list of applications.
@ -82,6 +84,8 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
public AppInfo appInfo = null; public AppInfo appInfo = null;
// The index of this app not including sections // The index of this app not including sections
public int appIndex = -1; public int appIndex = -1;
// Search section associated to result
public SearchSectionInfo searchSectionInfo = null;
public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
int appIndex) { int appIndex) {
@ -114,6 +118,17 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
item.position = pos; item.position = pos;
return item; return item;
} }
/**
* Factory method for search section title AdapterItem
*/
public static AdapterItem asSearchTitle(SearchSectionInfo sectionInfo, int pos) {
AdapterItem item = new AdapterItem();
item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE;
item.position = pos;
item.searchSectionInfo = sectionInfo;
return item;
}
} }
private final BaseDraggingActivity mLauncher; private final BaseDraggingActivity mLauncher;
@ -132,7 +147,7 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
private final boolean mIsWork; private final boolean mIsWork;
// The of ordered component names as a result of a search query // The of ordered component names as a result of a search query
private ArrayList<ComponentKey> mSearchResults; private ArrayList<AdapterItem> mSearchResults;
private AllAppsGridAdapter mAdapter; private AllAppsGridAdapter mAdapter;
private AppInfoComparator mAppNameComparator; private AppInfoComparator mAppNameComparator;
private final int mNumAppsPerRow; private final int mNumAppsPerRow;
@ -210,10 +225,10 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
} }
/** /**
* Sets the sorted list of filtered components. * Sets results list for search
*/ */
public boolean setOrderedFilter(ArrayList<ComponentKey> f) { public boolean setSearchResults(ArrayList<AdapterItem> f) {
if (mSearchResults != f) { if (f == null || mSearchResults != f) {
boolean same = mSearchResults != null && mSearchResults.equals(f); boolean same = mSearchResults != null && mSearchResults.equals(f);
mSearchResults = f; mSearchResults = f;
onAppsUpdated(); onAppsUpdated();
@ -298,35 +313,42 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
// Recreate the filtered and sectioned apps (for convenience for the grid layout) from the // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
// ordered set of sections // ordered set of sections
for (AppInfo info : getFiltersAppInfos()) { if (!hasFilter()) {
String sectionName = info.sectionName; for (AppInfo info : mApps) {
String sectionName = info.sectionName;
// Create a new section if the section names do not match // Create a new section if the section names do not match
if (!sectionName.equals(lastSectionName)) { if (!sectionName.equals(lastSectionName)) {
lastSectionName = sectionName; lastSectionName = sectionName;
lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
mFastScrollerSections.add(lastFastScrollerSectionInfo); mFastScrollerSections.add(lastFastScrollerSectionInfo);
} }
// Create an app item
AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
lastFastScrollerSectionInfo.fastScrollToItem = appItem;
}
mAdapterItems.add(appItem);
mFilteredApps.add(info);
}
} else {
mAdapterItems.addAll(mSearchResults);
List<AppInfo> appInfos = mSearchResults.stream().filter(
i -> AllAppsGridAdapter.isIconViewType(i.viewType)).map(i -> i.appInfo).collect(
Collectors.toList());
mFilteredApps.addAll(appInfos);
if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
// Append the search market item
if (hasNoFilteredResults()) {
mAdapterItems.add(AdapterItem.asEmptySearch(position++));
} else {
mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
}
mAdapterItems.add(AdapterItem.asMarketSearch(position++));
// Create an app item
AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
lastFastScrollerSectionInfo.fastScrollToItem = appItem;
} }
mAdapterItems.add(appItem);
mFilteredApps.add(info);
} }
if (hasFilter()) {
// Append the search market item
if (hasNoFilteredResults()) {
mAdapterItems.add(AdapterItem.asEmptySearch(position++));
} else {
mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
}
mAdapterItems.add(AdapterItem.asMarketSearch(position++));
}
if (mNumAppsPerRow != 0) { if (mNumAppsPerRow != 0) {
// Update the number of rows in the adapter after we do all the merging (otherwise, we // Update the number of rows in the adapter after we do all the merging (otherwise, we
// would have to shift the values again) // would have to shift the values again)
@ -381,18 +403,4 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
} }
} }
} }
private List<AppInfo> getFiltersAppInfos() {
if (mSearchResults == null) {
return mApps;
}
ArrayList<AppInfo> result = new ArrayList<>();
for (ComponentKey key : mSearchResults) {
AppInfo match = mAllAppsStore.getApp(key);
if (match != null) {
result.add(match);
}
}
return result;
}
} }

View File

@ -28,7 +28,7 @@ import android.widget.TextView.OnEditorActionListener;
import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.ExtendedEditText; import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.Utilities; import com.android.launcher3.Utilities;
import com.android.launcher3.util.ComponentKey; import com.android.launcher3.allapps.AlphabeticalAppsList;
import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageManagerHelper;
import java.util.ArrayList; import java.util.ArrayList;
@ -50,6 +50,7 @@ public class AllAppsSearchBarController
public void setVisibility(int visibility) { public void setVisibility(int visibility) {
mInput.setVisibility(visibility); mInput.setVisibility(visibility);
} }
/** /**
* Sets the references to the apps model and the search result callback. * Sets the references to the apps model and the search result callback.
*/ */
@ -164,9 +165,9 @@ public class AllAppsSearchBarController
/** /**
* Called when the search is complete. * Called when the search is complete.
* *
* @param apps sorted list of matching components or null if in case of failure. * @param items sorted list of search result adapter items.
*/ */
void onSearchResult(String query, ArrayList<ComponentKey> apps); void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items);
/** /**
* Called when the search results should be cleared. * Called when the search results should be cleared.

View File

@ -38,13 +38,13 @@ import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
import com.android.launcher3.ExtendedEditText; import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.Insettable; import com.android.launcher3.Insettable;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AlphabeticalAppsList; import com.android.launcher3.allapps.AlphabeticalAppsList;
import com.android.launcher3.allapps.SearchUiManager; import com.android.launcher3.allapps.SearchUiManager;
import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.util.ComponentKey;
import java.util.ArrayList; import java.util.ArrayList;
@ -135,7 +135,8 @@ public class AppsSearchContainerLayout extends ExtendedEditText
mApps = appsView.getApps(); mApps = appsView.getApps();
mAppsView = appsView; mAppsView = appsView;
mSearchBarController.initialize( mSearchBarController.initialize(
new DefaultAppSearchAlgorithm(mApps.getApps()), this, mLauncher, this); new DefaultAppSearchAlgorithm(LauncherAppState.getInstance(mLauncher)), this,
mLauncher, this);
} }
@Override @Override
@ -168,9 +169,9 @@ public class AppsSearchContainerLayout extends ExtendedEditText
} }
@Override @Override
public void onSearchResult(String query, ArrayList<ComponentKey> apps) { public void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items) {
if (apps != null) { if (items != null) {
mApps.setOrderedFilter(apps); mApps.setSearchResults(items);
notifyResultChanged(); notifyResultChanged();
mAppsView.setLastSearchQuery(query); mAppsView.setLastSearchQuery(query);
} }
@ -178,7 +179,7 @@ public class AppsSearchContainerLayout extends ExtendedEditText
@Override @Override
public void clearSearchResult() { public void clearSearchResult() {
if (mApps.setOrderedFilter(null)) { if (mApps.setSearchResults(null)) {
notifyResultChanged(); notifyResultChanged();
} }

View File

@ -0,0 +1,92 @@
/*
* 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 androidx.annotation.WorkerThread;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BaseModelUpdateTask;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.data.AppInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* A device search section for handling app searches
*/
public class AppsSearchPipeline implements SearchPipeline {
private static final int MAX_RESULTS_COUNT = 5;
private final SearchSectionInfo mSearchSectionInfo;
private final LauncherAppState mLauncherAppState;
public AppsSearchPipeline(LauncherAppState launcherAppState) {
mLauncherAppState = launcherAppState;
mSearchSectionInfo = new SearchSectionInfo(R.string.search_corpus_apps);
}
@Override
@WorkerThread
public void performSearch(String query, Consumer<ArrayList<AdapterItem>> callback) {
mLauncherAppState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
callback.accept(getAdapterItems(getTitleMatchResult(apps.data, query)));
}
});
}
/**
* Filters {@link AppInfo}s matching specified query
*/
public static ArrayList<AppInfo> getTitleMatchResult(List<AppInfo> apps, String query) {
// Do an intersection of the words in the query and each title, and filter out all the
// apps that don't match all of the words in the query.
final String queryTextLower = query.toLowerCase();
final ArrayList<AppInfo> result = new ArrayList<>();
DefaultAppSearchAlgorithm.StringMatcher matcher =
DefaultAppSearchAlgorithm.StringMatcher.getInstance();
for (AppInfo info : apps) {
if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
result.add(info);
}
}
return result;
}
private ArrayList<AdapterItem> getAdapterItems(List<AppInfo> matchingApps) {
ArrayList<AdapterItem> items = new ArrayList<>();
if (matchingApps.isEmpty()) {
return items;
}
items.add(AdapterItem.asSearchTitle(mSearchSectionInfo, 0));
int existingItems = items.size();
int searchResultsCount = Math.min(matchingApps.size(), MAX_RESULTS_COUNT);
for (int i = 0; i < searchResultsCount; i++) {
AdapterItem appItem = AdapterItem.asApp(i + existingItems, "", matchingApps.get(i), i);
appItem.searchSectionInfo = mSearchSectionInfo;
items.add(appItem);
}
return items;
}
}

View File

@ -17,24 +17,22 @@ package com.android.launcher3.allapps.search;
import android.os.Handler; import android.os.Handler;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.ComponentKey;
import java.text.Collator; import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
/** /**
* The default search implementation. * The default search implementation.
*/ */
public class DefaultAppSearchAlgorithm implements SearchAlgorithm { public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
private final List<AppInfo> mApps;
protected final Handler mResultHandler; protected final Handler mResultHandler;
private final AppsSearchPipeline mAppsSearchPipeline;
public DefaultAppSearchAlgorithm(List<AppInfo> apps) { public DefaultAppSearchAlgorithm(LauncherAppState launcherAppState) {
mApps = apps;
mResultHandler = new Handler(); mResultHandler = new Handler();
mAppsSearchPipeline = new AppsSearchPipeline(launcherAppState);
} }
@Override @Override
@ -47,28 +45,8 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
@Override @Override
public void doSearch(final String query, public void doSearch(final String query,
final AllAppsSearchBarController.Callbacks callback) { final AllAppsSearchBarController.Callbacks callback) {
final ArrayList<ComponentKey> result = getTitleMatchResult(query); mAppsSearchPipeline.performSearch(query,
mResultHandler.post(new Runnable() { results -> mResultHandler.post(() -> callback.onSearchResult(query, results)));
@Override
public void run() {
callback.onSearchResult(query, result);
}
});
}
private ArrayList<ComponentKey> getTitleMatchResult(String query) {
// Do an intersection of the words in the query and each title, and filter out all the
// apps that don't match all of the words in the query.
final String queryTextLower = query.toLowerCase();
final ArrayList<ComponentKey> result = new ArrayList<>();
StringMatcher matcher = StringMatcher.getInstance();
for (AppInfo info : mApps) {
if (matches(info, queryTextLower, matcher)) {
result.add(info.toComponentKey());
}
}
return result;
} }
public static boolean matches(AppInfo info, String query, StringMatcher matcher) { public static boolean matches(AppInfo info, String query, StringMatcher matcher) {

View File

@ -0,0 +1,32 @@
/*
* 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 com.android.launcher3.allapps.AlphabeticalAppsList;
import java.util.ArrayList;
import java.util.function.Consumer;
/**
* An interface for handling search within pipeline
*/
public interface SearchPipeline {
/**
* Perform query
*/
void performSearch(String query, Consumer<ArrayList<AlphabeticalAppsList.AdapterItem>> cb);
}

View File

@ -0,0 +1,36 @@
/*
* 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.content.Context;
/**
* Info class for a search section
*/
public class SearchSectionInfo {
private final int mTitleResId;
public SearchSectionInfo(int titleResId) {
mTitleResId = titleResId;
}
/**
* Returns the section's title
*/
public String getTitle(Context context) {
return context.getString(mTitleResId);
}
}