diff --git a/res/drawable/ic_find_in_page_24px.xml b/res/drawable/ic_find_in_page_24px.xml new file mode 100644 index 00000000000..18895e49a1c --- /dev/null +++ b/res/drawable/ic_find_in_page_24px.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/res/menu/manage_apps.xml b/res/menu/manage_apps.xml index 99dba37d3a4..51189a0722d 100644 --- a/res/menu/manage_apps.xml +++ b/res/menu/manage_apps.xml @@ -15,6 +15,13 @@ --> + + parent) { } + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + mApplications.filterSearch(newText); + return false; + } + public void updateView() { updateOptionsMenu(); final Activity host = getActivity(); @@ -859,6 +881,7 @@ public class ManageApplications extends InstrumentedFragment private AppFilterItem mAppFilter; private ArrayList mEntries; + private ArrayList mOriginalEntries; private boolean mResumed; private int mLastSortMode = -1; private int mWhichSize = SIZE_TOTAL; @@ -866,6 +889,7 @@ public class ManageApplications extends InstrumentedFragment private boolean mHasReceivedLoadEntries; private boolean mHasReceivedBridgeCallback; private FileViewHolderController mExtraViewController; + private SearchFilter mSearchFilter; // This is to remember and restore the last scroll position when this // fragment is paused. We need this special handling because app entries are added gradually @@ -1100,6 +1124,13 @@ public class ManageApplications extends InstrumentedFragment }); } + public void filterSearch(String query) { + if (mSearchFilter == null) { + mSearchFilter = new SearchFilter(); + } + mSearchFilter.filter(query); + } + @VisibleForTesting static boolean shouldUseStableItemHeight(int listType) { return true; @@ -1146,6 +1177,7 @@ public class ManageApplications extends InstrumentedFragment entries = removeDuplicateIgnoringUser(entries); } mEntries = entries; + mOriginalEntries = entries; notifyDataSetChanged(); if (getItemCount() == 0) { mManageApplications.mRecyclerView.setVisibility(View.GONE); @@ -1153,6 +1185,14 @@ public class ManageApplications extends InstrumentedFragment } else { mManageApplications.mEmptyView.setVisibility(View.GONE); mManageApplications.mRecyclerView.setVisibility(View.VISIBLE); + + if (mManageApplications.mSearchView != null + && mManageApplications.mSearchView.isVisibleToUser()) { + final CharSequence query = mManageApplications.mSearchView.getQuery(); + if (!TextUtils.isEmpty(query)) { + filterSearch(query.toString()); + } + } } // Restore the last scroll position if the number of entries added so far is bigger than // it. @@ -1405,6 +1445,38 @@ public class ManageApplications extends InstrumentedFragment } } } + + /** + * An array filter that constrains the content of the array adapter with a substring. + * Item that does not contains the specified substring will be removed from the list.

+ */ + private class SearchFilter extends Filter { + @WorkerThread + @Override + protected FilterResults performFiltering(CharSequence query) { + final ArrayList matchedEntries; + if (TextUtils.isEmpty(query)) { + matchedEntries = mOriginalEntries; + } else { + matchedEntries = new ArrayList<>(); + for (ApplicationsState.AppEntry entry : mOriginalEntries) { + if (entry.label.toLowerCase().contains(query.toString().toLowerCase())) { + matchedEntries.add(entry); + } + } + } + final FilterResults results = new FilterResults(); + results.values = matchedEntries; + results.count = matchedEntries.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + mEntries = (ArrayList) results.values; + notifyDataSetChanged(); + } + } } private static class SummaryProvider implements SummaryLoader.SummaryProvider { diff --git a/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java b/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java index b3f5b5e72e8..4c9cacb6926 100644 --- a/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java +++ b/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java @@ -28,11 +28,12 @@ import static com.android.settings.applications.manageapplications.ManageApplica import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -48,9 +49,11 @@ import android.os.Looper; import android.os.UserManager; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.SearchView; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; @@ -158,6 +161,35 @@ public class ManageApplicationsTest { verify(loadingContainer, never()).setVisibility(View.VISIBLE); } + @Test + public void onCreateOptionsMenu_shouldSetSearchQueryListener() { + final SearchView searchView = mock(SearchView.class); + final MenuItem searchMenu = mock(MenuItem.class); + final MenuItem helpMenu = mock(MenuItem.class); + when(searchMenu.getActionView()).thenReturn(searchView); + when(mMenu.findItem(R.id.search_app_list_menu)).thenReturn(searchMenu); + when(mMenu.add(anyInt() /* groupId */, anyInt() /* itemId */, anyInt() /* order */, + anyInt() /* titleRes */)).thenReturn(helpMenu); + doReturn("Test").when(mFragment).getText(anyInt() /* resId */); + doNothing().when(mFragment).updateOptionsMenu(); + + mFragment.onCreateOptionsMenu(mMenu, mock(MenuInflater.class)); + + verify(searchView).setOnQueryTextListener(mFragment); + } + + @Test + public void onQueryTextChange_shouldFilterSearchInApplicationsAdapter() { + final ManageApplications.ApplicationsAdapter adapter = + mock(ManageApplications.ApplicationsAdapter.class); + final String query = "Test App"; + ReflectionHelpers.setField(mFragment, "mApplications", adapter); + + mFragment.onQueryTextChange(query); + + verify(adapter).filterSearch(query); + } + @Test public void updateLoading_appLoaded_shouldNotDelayCallToHandleLoadingContainer() { ReflectionHelpers.setField(mFragment, "mLoadingContainer", mock(View.class)); @@ -249,6 +281,34 @@ public class ManageApplicationsTest { verify(loadingViewController).showContent(true /* animate */); } + @Test + public void onRebuildComplete_hasSearchQuery_shouldFilterSearch() { + final String query = "Test"; + final RecyclerView recyclerView = mock(RecyclerView.class); + final View emptyView = mock(View.class); + ReflectionHelpers.setField(mFragment, "mRecyclerView", recyclerView); + ReflectionHelpers.setField(mFragment, "mEmptyView", emptyView); + final SearchView searchView = mock(SearchView.class); + ReflectionHelpers.setField(mFragment, "mSearchView", searchView); + when(searchView.isVisibleToUser()).thenReturn(true); + when(searchView.getQuery()).thenReturn(query); + final View listContainer = mock(View.class); + when(listContainer.getVisibility()).thenReturn(View.VISIBLE); + ReflectionHelpers.setField(mFragment, "mListContainer", listContainer); + ReflectionHelpers.setField( + mFragment, "mFilterAdapter", mock(ManageApplications.FilterSpinnerAdapter.class)); + final ArrayList appList = new ArrayList<>(); + appList.add(mock(ApplicationsState.AppEntry.class)); + final ManageApplications.ApplicationsAdapter adapter = + spy(new ManageApplications.ApplicationsAdapter(mState, mFragment, + AppFilterRegistry.getInstance().get(FILTER_APPS_ALL), + null /* savedInstanceState */)); + + adapter.onRebuildComplete(appList); + + verify(adapter).filterSearch(query); + } + @Test public void notifyItemChange_recyclerViewIdle_shouldNotify() { final RecyclerView recyclerView = mock(RecyclerView.class); @@ -343,6 +403,48 @@ public class ManageApplicationsTest { verify(holder, never()).updateSwitch(any(), anyBoolean(), anyBoolean()); } + @Test + public void applicationsAdapter_filterSearch_emptyQuery_shouldShowFullList() { + final ManageApplications.ApplicationsAdapter adapter = + new ManageApplications.ApplicationsAdapter( + mState, mFragment, mock(AppFilterItem.class), Bundle.EMPTY); + final String[] appNames = {"Apricot", "Banana", "Cantaloupe", "Fig", "Mango"}; + ReflectionHelpers.setField(adapter, "mOriginalEntries", getTestAppList(appNames)); + + adapter.filterSearch(""); + + assertThat(adapter.getItemCount()).isEqualTo(5); + } + + @Test + public void applicationsAdapter_filterSearch_noMatch_shouldShowEmptyList() { + final ManageApplications.ApplicationsAdapter adapter = + new ManageApplications.ApplicationsAdapter( + mState, mFragment, mock(AppFilterItem.class), Bundle.EMPTY); + final String[] appNames = {"Apricot", "Banana", "Cantaloupe", "Fig", "Mango"}; + ReflectionHelpers.setField(adapter, "mOriginalEntries", getTestAppList(appNames)); + + adapter.filterSearch("orange"); + + assertThat(adapter.getItemCount()).isEqualTo(0); + } + + @Test + public void applicationsAdapter_filterSearch_shouldShowMatchedItemsOnly() { + final ManageApplications.ApplicationsAdapter adapter = + new ManageApplications.ApplicationsAdapter( + mState, mFragment, mock(AppFilterItem.class), Bundle.EMPTY); + final String[] appNames = {"Apricot", "Banana", "Cantaloupe", "Fig", "Mango"}; + ReflectionHelpers.setField(adapter, "mOriginalEntries", getTestAppList(appNames)); + + adapter.filterSearch("an"); + + assertThat(adapter.getItemCount()).isEqualTo(3); + assertThat(adapter.getAppEntry(0).label).isEqualTo("Banana"); + assertThat(adapter.getAppEntry(1).label).isEqualTo("Cantaloupe"); + assertThat(adapter.getAppEntry(2).label).isEqualTo("Mango"); + } + @Test public void sortOrderSavedOnRebuild() { when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{}); @@ -375,4 +477,14 @@ public class ManageApplicationsTest { return new RoboMenuItem(id); }); } + + private ArrayList getTestAppList(String[] appNames) { + final ArrayList appList = new ArrayList<>(); + for (String name : appNames) { + final ApplicationsState.AppEntry appEntry = mock(ApplicationsState.AppEntry.class); + appEntry.label = name; + appList.add(appEntry); + } + return appList; + } }