Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 35c066c9 authored by Doris Ling's avatar Doris Ling
Browse files

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
parent 282a0d92
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
<?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>
+7 −0
Original line number Diff line number Diff line
@@ -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"
+73 −1
Original line number Diff line number Diff line
@@ -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 {
+113 −1
Original line number Diff line number Diff line
@@ -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;
    }
}