Add search to app info list.

- add a search option menu to the manage applications page.
- add a search filter to the app list adapter to remove any app whose
app name does not contain the search query.

Change-Id: Ie749daeef2cdc4a22fade45422ae90f44d00ceb2
Fixes: 119598311
Test: make RunSettingsRoboTests
This commit is contained in:
Doris Ling
2018-12-04 15:40:34 -08:00
parent 282a0d9222
commit 35c066c997
4 changed files with 220 additions and 2 deletions

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,2C4.9,2 4.01,2.9 4.01,4L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6H6zM18,17.59l-2.2,-2.2c0.44,-0.69 0.7,-1.51 0.7,-2.39c0,-2.48 -2.02,-4.5 -4.5,-4.5S7.5,10.52 7.5,13s2.02,4.5 4.5,4.5c0.88,0 1.69,-0.26 2.39,-0.7l3.2,3.2L6,20V4h7.17L18,8.83V17.59zM12,15.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,15.5 12,15.5z"/>
</vector>

View File

@@ -15,6 +15,13 @@
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/search_app_list_menu"
android:title="@string/search_settings"
android:icon="@drawable/ic_find_in_page_24px"
android:showAsAction="always|collapseActionView"
android:actionViewClass="android.widget.SearchView" />
<item
android:id="@+id/advanced"
android:title="@string/advanced_apps"

View File

@@ -67,11 +67,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Filter;
import android.widget.FrameLayout;
import android.widget.SearchView;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -140,7 +143,7 @@ import java.util.Set;
* intent.
*/
public class ManageApplications extends InstrumentedFragment
implements View.OnClickListener, OnItemSelectedListener {
implements View.OnClickListener, OnItemSelectedListener, SearchView.OnQueryTextListener {
static final String TAG = "ManageApplications";
static final boolean DEBUG = true;
@@ -196,6 +199,7 @@ public class ManageApplications extends InstrumentedFragment
private View mListContainer;
private RecyclerView mRecyclerView;
private SearchView mSearchView;
// Size resource used for packages whose size computation failed for some reason
CharSequence mInvalidSizeStr;
@@ -599,6 +603,13 @@ public class ManageApplications extends InstrumentedFragment
mOptionsMenu = menu;
inflater.inflate(R.menu.manage_apps, menu);
final MenuItem searchMenuItem = menu.findItem(R.id.search_app_list_menu);
if (searchMenuItem != null) {
mSearchView = (SearchView) searchMenuItem.getActionView();
mSearchView.setQueryHint(getText(R.string.search_settings));
mSearchView.setOnQueryTextListener(this);
}
updateOptionsMenu();
}
@@ -724,6 +735,17 @@ public class ManageApplications extends InstrumentedFragment
public void onNothingSelected(AdapterView<?> 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<ApplicationsState.AppEntry> mEntries;
private ArrayList<ApplicationsState.AppEntry> 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.</p>
*/
private class SearchFilter extends Filter {
@WorkerThread
@Override
protected FilterResults performFiltering(CharSequence query) {
final ArrayList<ApplicationsState.AppEntry> 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<ApplicationsState.AppEntry>) results.values;
notifyDataSetChanged();
}
}
}
private static class SummaryProvider implements SummaryLoader.SummaryProvider {

View File

@@ -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<ApplicationsState.AppEntry> 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<ApplicationsState.AppEntry> getTestAppList(String[] appNames) {
final ArrayList<ApplicationsState.AppEntry> appList = new ArrayList<>();
for (String name : appNames) {
final ApplicationsState.AppEntry appEntry = mock(ApplicationsState.AppEntry.class);
appEntry.label = name;
appList.add(appEntry);
}
return appList;
}
}