Adding an asynchronous search interface for apps search

Change-Id: Ib09df0a3d587dc60ed888ddbd0edf058e4a1cc3e
This commit is contained in:
Sunny Goyal
2015-05-29 14:54:40 -07:00
parent dd5814c471
commit 5683f87172
7 changed files with 357 additions and 90 deletions
+11
View File
@@ -100,6 +100,7 @@ import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.PagedView.PageSwitchListener;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AppSearchManager;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.LauncherActivityInfoCompat;
import com.android.launcher3.compat.LauncherAppsCompat;
@@ -583,6 +584,11 @@ public class Launcher extends Activity
}
}
}
@Override
public void setSearchManager(AppSearchManager manager) {
mAppsView.setSearchManager(manager);
}
});
mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() {
private boolean mImportanceStored = false;
@@ -1158,6 +1164,11 @@ public class Launcher extends Activity
* Called to dismiss all apps if it is showing.
*/
public void dismissAllApps();
/**
* Sets the search manager to be used for app search.
*/
public void setSearchManager(AppSearchManager manager);
}
public interface LauncherSearchCallbacks {
@@ -41,6 +41,7 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.launcher3.AppInfo;
import com.android.launcher3.BaseContainerView;
import com.android.launcher3.BubbleTextView;
@@ -54,15 +55,15 @@ import com.android.launcher3.Folder;
import com.android.launcher3.Insettable;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherTransitionable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.allapps.AppSearchManager.AppSearchResultCallback;
import com.android.launcher3.util.Thunk;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
@@ -171,7 +172,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
AlphabeticalAppsList.AdapterChangedCallback, AllAppsGridAdapter.PredictionBarSpacerCallbacks,
View.OnTouchListener, View.OnClickListener, View.OnLongClickListener,
ViewTreeObserver.OnPreDrawListener {
ViewTreeObserver.OnPreDrawListener, AppSearchResultCallback {
public static final boolean GRID_MERGE_SECTIONS = true;
@@ -183,8 +184,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
private static final int FADE_OUT_DURATION = 100;
private static final int SEARCH_TRANSLATION_X_DP = 18;
private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
@Thunk Launcher mLauncher;
@Thunk AlphabeticalAppsList mApps;
private LayoutInflater mLayoutInflater;
@@ -221,6 +220,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
private CheckLongPressHelper mPredictionIconCheckForLongPress;
private View mPredictionIconUnderTouch;
private AppSearchManager mSearchManager;
public AllAppsContainerView(Context context) {
this(context, null);
}
@@ -231,7 +232,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LauncherAppState app = LauncherAppState.getInstance();
Resources res = context.getResources();
mLauncher = (Launcher) context;
@@ -258,6 +258,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mContentMarginStart = mAdapter.getContentMarginStart();
mApps.setAdapter(mAdapter);
mSearchManager = mApps.newSimpleAppSearchManager();
}
/**
@@ -281,6 +282,11 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mApps.addApps(apps);
}
public void setSearchManager(AppSearchManager searchManager) {
mSearchManager.cancel(true);
mSearchManager = searchManager;
}
/**
* Updates existing apps in the list
*/
@@ -664,44 +670,25 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
public void afterTextChanged(final Editable s) {
String queryText = s.toString();
if (queryText.isEmpty()) {
mApps.setFilter(null);
mSearchManager.cancel(true);
mApps.setOrderedFilter(null);
} else {
String formatStr = getResources().getString(R.string.all_apps_no_search_results);
mAdapter.setEmptySearchText(String.format(formatStr, queryText));
// 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 = queryText.toLowerCase();
final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
mApps.setFilter(new AlphabeticalAppsList.Filter() {
@Override
public boolean retainApp(AppInfo info, String sectionName) {
if (sectionName.toLowerCase().contains(queryTextLower)) {
return true;
}
String title = info.title.toString();
String[] words = SPLIT_PATTERN.split(title.toLowerCase());
for (int qi = 0; qi < queryWords.length; qi++) {
boolean foundMatch = false;
for (int i = 0; i < words.length; i++) {
if (words[i].startsWith(queryWords[qi])) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
// If there is a word in the query that does not match any words in this
// title, so skip it.
return false;
}
}
return true;
}
});
mSearchManager.cancel(false);
mSearchManager.doSearch(queryText, this);
}
scrollToTop();
}
@Override
public void onSearchResult(ArrayList<ComponentName> apps) {
if (apps != null) {
mApps.setOrderedFilter(apps);
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
@@ -796,7 +783,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
* recycler view.
*/
private boolean handleTouchEvent(MotionEvent ev) {
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = mLauncher.getDeviceProfile();
int x = (int) ev.getX();
int y = (int) ev.getY();
@@ -919,6 +905,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
* Shows the search field.
*/
private void showSearchField() {
mSearchManager.connect();
// Show the search bar and focus the search
final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
getContext().getResources().getDisplayMetrics());
@@ -949,6 +937,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
* Hides the search field.
*/
private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
mSearchManager.cancel(true);
final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
getContext().getResources().getDisplayMetrics());
@@ -966,7 +956,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
if (resetTextField) {
mSearchBarEditView.setText("");
}
mApps.setFilter(null);
mApps.setOrderedFilter(null);
if (returnFocusToRecyclerView) {
mAppsRecyclerView.requestFocus();
}
@@ -983,7 +973,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
if (resetTextField) {
mSearchBarEditView.setText("");
}
mApps.setFilter(null);
mApps.setOrderedFilter(null);
mSearchButtonView.setAlpha(1f);
mSearchButtonView.setTranslationX(0f);
if (returnFocusToRecyclerView) {
@@ -1,13 +1,31 @@
/*
* Copyright (C) 2015 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;
import android.content.ComponentName;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import com.android.launcher3.AppInfo;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.model.AbstractUserComparator;
import com.android.launcher3.model.AppNameComparator;
import java.nio.charset.CharsetEncoder;
@@ -114,13 +132,6 @@ public class AlphabeticalAppsList {
}
}
/**
* A filter interface to limit the set of applications in the apps list.
*/
public interface Filter {
boolean retainApp(AppInfo info, String sectionName);
}
/**
* Callback to notify when the set of adapter items have changed.
*/
@@ -198,7 +209,7 @@ public class AlphabeticalAppsList {
private Launcher mLauncher;
// The set of apps from the system not including predictions
private List<AppInfo> mApps = new ArrayList<>();
private final List<AppInfo> mApps = new ArrayList<>();
// The set of filtered apps with the current filter
private List<AppInfo> mFilteredApps = new ArrayList<>();
// The current set of adapter items
@@ -211,9 +222,10 @@ public class AlphabeticalAppsList {
private List<ComponentName> mPredictedAppComponents = new ArrayList<>();
// The set of predicted apps resolved from the component names and the current set of apps
private List<AppInfo> mPredictedApps = new ArrayList<>();
// The of ordered component names as a result of a search query
private ArrayList<ComponentName> mSearchResults;
private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
private RecyclerView.Adapter mAdapter;
private Filter mFilter;
private AlphabeticIndexCompat mIndexer;
private AppNameComparator mAppNameComparator;
private MergeAlgorithm mMergeAlgorithm;
@@ -235,6 +247,10 @@ public class AlphabeticalAppsList {
mAdapterChangedCallback = cb;
}
public SimpleAppSearchManagerImpl newSimpleAppSearchManager() {
return new SimpleAppSearchManagerImpl(mApps);
}
/**
* Sets the number of apps per row. Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
*/
@@ -293,22 +309,22 @@ public class AlphabeticalAppsList {
* Returns whether there are is a filter set.
*/
public boolean hasFilter() {
return (mFilter != null);
return (mSearchResults != null);
}
/**
* Returns whether there are no filtered results.
*/
public boolean hasNoFilteredResults() {
return (mFilter != null) && mFilteredApps.isEmpty();
return (mSearchResults != null) && mFilteredApps.isEmpty();
}
/**
* Sets the current filter for this list of apps.
* Sets the sorted list of filtered components.
*/
public void setFilter(Filter f) {
if (mFilter != f) {
mFilter = f;
public void setOrderedFilter(ArrayList<ComponentName> f) {
if (mSearchResults != f) {
mSearchResults = f;
updateAdapterItems();
}
}
@@ -428,7 +444,9 @@ public class AlphabeticalAppsList {
for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
allApps.addAll(entry.getValue());
}
mApps = allApps;
mApps.clear();
mApps.addAll(allApps);
} else {
// Just compute the section headers for use below
for (AppInfo info : mApps) {
@@ -483,16 +501,12 @@ public class AlphabeticalAppsList {
// Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
// ordered set of sections
int numApps = mApps.size();
List<AppInfo> apps = getFiltersAppInfos();
int numApps = apps.size();
for (int i = 0; i < numApps; i++) {
AppInfo info = mApps.get(i);
AppInfo info = apps.get(i);
String sectionName = getAndUpdateCachedSectionName(info.title);
// Check if we want to retain this app
if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
continue;
}
// Create a new section if the section names do not match
if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
lastSectionName = sectionName;
@@ -533,6 +547,41 @@ public class AlphabeticalAppsList {
}
}
private List<AppInfo> getFiltersAppInfos() {
if (mSearchResults == null) {
return mApps;
}
int total = mSearchResults.size();
final HashMap<ComponentName, Integer> sortOrder = new HashMap<>(total);
for (int i = 0; i < total; i++) {
sortOrder.put(mSearchResults.get(i), i);
}
ArrayList<AppInfo> result = new ArrayList<>();
for (AppInfo info : mApps) {
if (sortOrder.containsKey(info.componentName)) {
result.add(info);
}
}
Collections.sort(result, new AbstractUserComparator<AppInfo>(
LauncherAppState.getInstance().getContext()) {
@Override
public int compare(AppInfo lhs, AppInfo rhs) {
Integer indexA = sortOrder.get(lhs.componentName);
int result = indexA.compareTo(sortOrder.get(rhs.componentName));
if (result == 0) {
return super.compare(lhs, rhs);
} else {
return result;
}
}
});
return result;
}
/**
* Merges multiple sections to reduce visual raggedness.
*/
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2015 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;
import android.content.ComponentName;
import java.util.ArrayList;
/**
* Interface for handling app search.
*/
public interface AppSearchManager {
/**
* Called when the search is about to be used. This method is optional for making a query but
* calling this appropriately can improve the initial response time.
*/
void connect();
/**
* Cancels all pending search requests.
*
* @param interruptActiveRequests if true, any active requests which are already executing will
* be invalidated, and the corresponding results will not be sent. The client should usually
* set this to true, before beginning a new search session.
*/
void cancel(boolean interruptActiveRequests);
/**
* Performs a search
*/
void doSearch(String query, AppSearchResultCallback callback);
/**
* Callback for getting search results.
*/
public interface AppSearchResultCallback {
/**
* Called when the search is complete.
*
* @param apps sorted list of matching components or null if in case of failure.
*/
void onSearchResult(ArrayList<ComponentName> apps);
}
}
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 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;
import android.content.ComponentName;
import android.os.Handler;
import com.android.launcher3.AppInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* An {@link AppSearchManager} which does label matching on the UI thread.
*/
public class SimpleAppSearchManagerImpl implements AppSearchManager {
private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
private final List<AppInfo> mApps;
private final Handler mResultHandler;
public SimpleAppSearchManagerImpl(List<AppInfo> apps) {
mApps = apps;
mResultHandler = new Handler();
}
@Override
public void connect() {
// No op
}
@Override
public void cancel(boolean interruptActiveRequests) {
if (interruptActiveRequests) {
mResultHandler.removeCallbacksAndMessages(null);
}
}
@Override
public void doSearch(String query, final AppSearchResultCallback callback) {
// 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 String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
final ArrayList<ComponentName> result = new ArrayList<ComponentName>();
int total = mApps.size();
for (int i = 0; i < total; i++) {
AppInfo info = mApps.get(i);
if (!result.contains(info.componentName) && matches(info, queryWords)) {
result.add(info.componentName);
}
}
mResultHandler.post(new Runnable() {
@Override
public void run() {
callback.onSearchResult(result);
}
});
}
private boolean matches(AppInfo info, String[] queryWords) {
String title = info.title.toString();
String[] words = SPLIT_PATTERN.split(title.toLowerCase());
for (int qi = 0; qi < queryWords.length; qi++) {
boolean foundMatch = false;
for (int i = 0; i < words.length; i++) {
if (words[i].startsWith(queryWords[qi])) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
// If there is a word in the query that does not match any words in this
// title, so skip it.
return false;
}
}
return true;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 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.model;
import android.content.Context;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import java.util.Comparator;
import java.util.HashMap;
/**
* A comparator to arrange items based on user profiles.
*/
public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> {
private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
private final UserManagerCompat mUserManager;
private final UserHandleCompat mMyUser;
public AbstractUserComparator(Context context) {
mUserManager = UserManagerCompat.getInstance(context);
mMyUser = UserHandleCompat.myUserHandle();
}
@Override
public int compare(T lhs, T rhs) {
if (mMyUser.equals(lhs.user)) {
return -1;
} else {
Long aUserSerial = getAndCacheUserSerial(lhs.user);
Long bUserSerial = getAndCacheUserSerial(rhs.user);
return aUserSerial.compareTo(bUserSerial);
}
}
/**
* Returns the user serial for this user, using a cached serial if possible.
*/
private Long getAndCacheUserSerial(UserHandleCompat user) {
Long userSerial = mUserSerialCache.get(user);
if (userSerial == null) {
userSerial = mUserManager.getSerialNumberForUser(user);
mUserSerialCache.put(user, userSerial);
}
return userSerial;
}
public void clearUserCache() {
mUserSerialCache.clear();
}
}
@@ -1,14 +1,27 @@
/*
* Copyright (C) 2015 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.model;
import android.content.Context;
import com.android.launcher3.AppInfo;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import java.text.Collator;
import java.util.Comparator;
import java.util.HashMap;
/**
* Class to manage access to an app name comparator.
@@ -16,17 +29,15 @@ import java.util.HashMap;
* Used to sort application name in all apps view and widget tray view.
*/
public class AppNameComparator {
private final UserManagerCompat mUserManager;
private final Collator mCollator;
private final Comparator<ItemInfo> mAppInfoComparator;
private final AbstractUserComparator<ItemInfo> mAppInfoComparator;
private final Comparator<String> mSectionNameComparator;
private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
public AppNameComparator(Context context) {
mCollator = Collator.getInstance();
mUserManager = UserManagerCompat.getInstance(context);
mAppInfoComparator = new Comparator<ItemInfo>() {
mAppInfoComparator = new AbstractUserComparator<ItemInfo>(context) {
@Override
public final int compare(ItemInfo a, ItemInfo b) {
// Order by the title in the current locale
int result = compareTitles(a.title.toString(), b.title.toString());
@@ -38,13 +49,7 @@ public class AppNameComparator {
if (result == 0) {
// If the two apps are the same component, then prioritize by the order that
// the app user was created (prioritizing the main user's apps)
if (UserHandleCompat.myUserHandle().equals(a.user)) {
return -1;
} else {
Long aUserSerial = getAndCacheUserSerial(a.user);
Long bUserSerial = getAndCacheUserSerial(b.user);
return aUserSerial.compareTo(bUserSerial);
}
return super.compare(a, b);
}
}
return result;
@@ -63,7 +68,7 @@ public class AppNameComparator {
*/
public Comparator<ItemInfo> getAppInfoComparator() {
// Clear the user serial cache so that we get serials as needed in the comparator
mUserSerialCache.clear();
mAppInfoComparator.clearUserCache();
return mAppInfoComparator;
}
@@ -90,16 +95,4 @@ public class AppNameComparator {
// Order by the title in the current locale
return mCollator.compare(titleA, titleB);
}
/**
* Returns the user serial for this user, using a cached serial if possible.
*/
private Long getAndCacheUserSerial(UserHandleCompat user) {
Long userSerial = mUserSerialCache.get(user);
if (userSerial == null) {
userSerial = mUserManager.getSerialNumberForUser(user);
mUserSerialCache.put(user, userSerial);
}
return userSerial;
}
}