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:
27
res/drawable/ic_find_in_page_24px.xml
Normal file
27
res/drawable/ic_find_in_page_24px.xml
Normal 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>
|
@@ -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"
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user