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

Commit ce6cc7e7 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Simplifying widget search pipeline

Bug: 183607616
Test: Verified on device
Change-Id: I3e5dd9e280f375475d1e1cf41dff6e6533175ebf
parent 07fb2fea
Loading
Loading
Loading
Loading
+153 −20
Original line number Diff line number Diff line
@@ -18,64 +18,197 @@ package com.android.launcher3.widget.picker.search;

import static android.os.Looper.getMainLooper;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;

import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.search.SearchCallback;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

@RunWith(RobolectricTestRunner.class)
public class SimpleWidgetsSearchAlgorithmTest {

    @Mock private IconCache mIconCache;

    private InvariantDeviceProfile mTestProfile;
    private WidgetsListHeaderEntry mCalendarHeaderEntry;
    private WidgetsListContentEntry mCalendarContentEntry;
    private WidgetsListHeaderEntry mCameraHeaderEntry;
    private WidgetsListContentEntry mCameraContentEntry;
    private WidgetsListHeaderEntry mClockHeaderEntry;
    private WidgetsListContentEntry mClockContentEntry;
    private Context mContext;

    private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
    @Mock
    private WidgetsPickerSearchPipeline mSearchPipeline;
    private PopupDataProvider mDataProvider;
    @Mock
    private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
    @Captor
    private ArgumentCaptor<Consumer<List<WidgetsListBaseEntry>>> mConsumerCaptor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mSimpleWidgetsSearchAlgorithm = new SimpleWidgetsSearchAlgorithm(mSearchPipeline);
        doAnswer(invocation -> {
            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
            return componentWithLabel.getComponent().getShortClassName();
        }).when(mIconCache).getTitleNoCache(any());
        mTestProfile = new InvariantDeviceProfile();
        mTestProfile.numRows = 5;
        mTestProfile.numColumns = 5;
        mContext = RuntimeEnvironment.application;

        mCalendarHeaderEntry =
                createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
        mCalendarContentEntry =
                createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
        mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 11);
        mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 11);
        mClockHeaderEntry = createWidgetsHeaderEntry("com.example.android.Clock", "Clock", 3);
        mClockContentEntry = createWidgetsContentEntry("com.example.android.Clock", "Clock", 3);


        mSimpleWidgetsSearchAlgorithm = new SimpleWidgetsSearchAlgorithm(mDataProvider);
        doReturn(Collections.EMPTY_LIST).when(mDataProvider).getAllWidgets();
    }

    @Test
    public void doSearch_shouldQueryPipeline() {
        mSimpleWidgetsSearchAlgorithm.doSearch("abc", mSearchCallback);
    public void filter_shouldMatchOnAppName() {
        doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                .when(mDataProvider)
                .getAllWidgets();

        verify(mSearchPipeline).query(eq("abc"), any());
        assertEquals(List.of(
                new WidgetsListSearchHeaderEntry(
                        mCalendarHeaderEntry.mPkgItem,
                        mCalendarHeaderEntry.mTitleSectionName,
                        mCalendarHeaderEntry.mWidgets),
                mCalendarContentEntry,
                new WidgetsListSearchHeaderEntry(
                        mCameraHeaderEntry.mPkgItem,
                        mCameraHeaderEntry.mTitleSectionName,
                        mCameraHeaderEntry.mWidgets),
                mCameraContentEntry),
                SimpleWidgetsSearchAlgorithm.getFilteredWidgets(mDataProvider, "Ca"));
    }

    @Test
    public void doSearch_shouldInformSearchCallbackOnQueryResult() {
        ArrayList<WidgetsListBaseEntry> baseEntries = new ArrayList<>();
    public void filter_shouldMatchOnWidgetLabel() {
        doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                mCameraContentEntry))
                .when(mDataProvider)
                .getAllWidgets();

        mSimpleWidgetsSearchAlgorithm.doSearch("abc", mSearchCallback);
        assertEquals(List.of(
                new WidgetsListSearchHeaderEntry(
                        mCalendarHeaderEntry.mPkgItem,
                        mCalendarHeaderEntry.mTitleSectionName,
                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
                new WidgetsListContentEntry(
                        mCalendarHeaderEntry.mPkgItem,
                        mCalendarHeaderEntry.mTitleSectionName,
                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
                new WidgetsListSearchHeaderEntry(
                        mCameraHeaderEntry.mPkgItem,
                        mCameraHeaderEntry.mTitleSectionName,
                        mCameraHeaderEntry.mWidgets.subList(1, 3)),
                new WidgetsListContentEntry(
                        mCameraHeaderEntry.mPkgItem,
                        mCameraHeaderEntry.mTitleSectionName,
                        mCameraHeaderEntry.mWidgets.subList(1, 3))),
                SimpleWidgetsSearchAlgorithm.getFilteredWidgets(mDataProvider, "Widget1"));
    }

        verify(mSearchPipeline).query(eq("abc"), mConsumerCaptor.capture());
        mConsumerCaptor.getValue().accept(baseEntries);
    @Test
    public void doSearch_shouldInformCallback() {
        doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                .when(mDataProvider)
                .getAllWidgets();
        mSimpleWidgetsSearchAlgorithm.doSearch("Ca", mSearchCallback);
        shadowOf(getMainLooper()).idle();
        // Verify SearchCallback#onSearchResult receives a query token along with the search
        // results. The query token is the original query string concatenated with the query
        // timestamp.
        verify(mSearchCallback).onSearchResult(matches("abc\t\\d*"), eq(baseEntries));
        verify(mSearchCallback).onSearchResult(
                matches("Ca"), argThat(a -> a != null && !a.isEmpty()));
    }

    private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
            int numOfWidgets) {
        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
                widgetItems.get(0).user);

        return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
    }

    private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
            int numOfWidgets) {
        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
                widgetItems.get(0).user);

        return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
    }

    private PackageItemInfo createPackageItemInfo(String packageName, String appName,
            UserHandle userHandle) {
        PackageItemInfo pInfo = new PackageItemInfo(packageName);
        pInfo.title = appName;
        pInfo.user = userHandle;
        pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
        return pInfo;
    }

    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
        for (int i = 0; i < numOfWidgets; i++) {
            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
            widgetInfo.provider = cn;
            ReflectionHelpers.setField(widgetInfo, "providerInfo",
                    packageManager.addReceiverIfNotPresent(cn));

            WidgetItem widgetItem = new WidgetItem(
                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
                    mTestProfile, mIconCache);
            widgetItems.add(widgetItem);
        }
        return widgetItems;
    }
}
+0 −185
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.widget.picker.search;

import static android.os.Looper.getMainLooper;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.robolectric.Shadows.shadowOf;

import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
public class SimpleWidgetsSearchPipelineTest {
    @Mock private IconCache mIconCache;

    private InvariantDeviceProfile mTestProfile;
    private WidgetsListHeaderEntry mCalendarHeaderEntry;
    private WidgetsListContentEntry mCalendarContentEntry;
    private WidgetsListHeaderEntry mCameraHeaderEntry;
    private WidgetsListContentEntry mCameraContentEntry;
    private WidgetsListHeaderEntry mClockHeaderEntry;
    private WidgetsListContentEntry mClockContentEntry;
    private Context mContext;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        doAnswer(invocation -> {
            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
            return componentWithLabel.getComponent().getShortClassName();
        }).when(mIconCache).getTitleNoCache(any());
        mTestProfile = new InvariantDeviceProfile();
        mTestProfile.numRows = 5;
        mTestProfile.numColumns = 5;
        mContext = RuntimeEnvironment.application;

        mCalendarHeaderEntry =
                createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
        mCalendarContentEntry =
                createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
        mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 11);
        mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 11);
        mClockHeaderEntry = createWidgetsHeaderEntry("com.example.android.Clock", "Clock", 3);
        mClockContentEntry = createWidgetsContentEntry("com.example.android.Clock", "Clock", 3);
    }

    @Test
    public void query_shouldMatchOnAppName() {
        SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
                List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                        mCameraContentEntry, mClockHeaderEntry, mClockContentEntry));

        pipeline.query("Ca", results ->
                assertEquals(results,
                        List.of(
                                new WidgetsListSearchHeaderEntry(
                                        mCalendarHeaderEntry.mPkgItem,
                                        mCalendarHeaderEntry.mTitleSectionName,
                                        mCalendarHeaderEntry.mWidgets),
                                mCalendarContentEntry,
                                new WidgetsListSearchHeaderEntry(
                                        mCameraHeaderEntry.mPkgItem,
                                        mCameraHeaderEntry.mTitleSectionName,
                                        mCameraHeaderEntry.mWidgets),
                                mCameraContentEntry)));
        shadowOf(getMainLooper()).idle();
    }

    @Test
    public void query_shouldMatchOnWidgetLabel() {
        SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
                List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                        mCameraContentEntry));

        pipeline.query("Widget1", results ->
                assertEquals(results,
                        List.of(
                                new WidgetsListSearchHeaderEntry(
                                        mCalendarHeaderEntry.mPkgItem,
                                        mCalendarHeaderEntry.mTitleSectionName,
                                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
                                new WidgetsListContentEntry(
                                        mCalendarHeaderEntry.mPkgItem,
                                        mCalendarHeaderEntry.mTitleSectionName,
                                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
                                new WidgetsListSearchHeaderEntry(
                                        mCameraHeaderEntry.mPkgItem,
                                        mCameraHeaderEntry.mTitleSectionName,
                                        mCameraHeaderEntry.mWidgets.subList(1, 3)),
                                new WidgetsListContentEntry(
                                        mCameraHeaderEntry.mPkgItem,
                                        mCameraHeaderEntry.mTitleSectionName,
                                        mCameraHeaderEntry.mWidgets.subList(1, 3)))));
        shadowOf(getMainLooper()).idle();
    }

    private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
            int numOfWidgets) {
        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
                widgetItems.get(0).user);

        return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
    }

    private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
            int numOfWidgets) {
        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
                widgetItems.get(0).user);

        return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
    }

    private PackageItemInfo createPackageItemInfo(String packageName, String appName,
            UserHandle userHandle) {
        PackageItemInfo pInfo = new PackageItemInfo(packageName);
        pInfo.title = appName;
        pInfo.user = userHandle;
        pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
        return pInfo;
    }

    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
        for (int i = 0; i < numOfWidgets; i++) {
            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
            widgetInfo.provider = cn;
            ReflectionHelpers.setField(widgetInfo, "providerInfo",
                    packageManager.addReceiverIfNotPresent(cn));

            WidgetItem widgetItem = new WidgetItem(
                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
                    mTestProfile, mIconCache);
            widgetItems.add(widgetItem);
        }
        return widgetItems;
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -169,7 +169,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
        onWidgetsBound();

        mSearchAndRecommendationViewHolder.mSearchBar.initialize(
                mLauncher.getPopupDataProvider().getAllWidgets(), /* searchModeListener= */ this);
                mLauncher.getPopupDataProvider(), /* searchModeListener= */ this);
    }

    @Override
+4 −9
Original line number Diff line number Diff line
@@ -26,10 +26,7 @@ import androidx.annotation.Nullable;

import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.R;
import com.android.launcher3.search.SearchAlgorithm;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;

import java.util.List;
import com.android.launcher3.popup.PopupDataProvider;

/**
 * View for a search bar with an edit text with a cancel button.
@@ -54,12 +51,10 @@ public class LauncherWidgetsSearchBar extends LinearLayout implements WidgetsSea
    }

    @Override
    public void initialize(List<WidgetsListBaseEntry> allWidgets,
            SearchModeListener searchModeListener) {
        SearchAlgorithm<WidgetsListBaseEntry> algo =
                new SimpleWidgetsSearchAlgorithm(new SimpleWidgetsSearchPipeline(allWidgets));
    public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) {
        mController = new WidgetsSearchBarController(
                algo, mEditText, mCancelButton, searchModeListener);
                new SimpleWidgetsSearchAlgorithm(dataProvider),
                mEditText, mCancelButton, searchModeListener);
    }

    @Override
+47 −16
Original line number Diff line number Diff line
@@ -16,42 +16,41 @@

package com.android.launcher3.widget.picker.search;

import static com.android.launcher3.search.StringMatcherUtility.matches;

import android.os.Handler;
import android.util.Log;

import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.search.SearchAlgorithm;
import com.android.launcher3.search.SearchCallback;
import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Implementation of {@link SearchAlgorithm} that posts a task to query on the main thread.
 */
public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {

    private static final boolean DEBUG = false;
    private static final String TAG = "SimpleWidgetsSearchAlgo";
    private static final String DELIM = "\t";

    private final Handler mResultHandler;
    private final WidgetsPickerSearchPipeline mSearchPipeline;
    private final PopupDataProvider mDataProvider;

    public SimpleWidgetsSearchAlgorithm(WidgetsPickerSearchPipeline searchPipeline) {
    public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) {
        mResultHandler = new Handler();
        mSearchPipeline = searchPipeline;
        mDataProvider = dataProvider;
    }

    @Override
    public void doSearch(String query, SearchCallback<WidgetsListBaseEntry> callback) {
        long startTime = System.currentTimeMillis();
        String queryToken = query + DELIM + startTime;
        if (DEBUG) {
            Log.d(TAG, "doSearch queryToken:" + queryToken);
        }
        mSearchPipeline.query(query,
                results -> mResultHandler.post(
                        () -> callback.onSearchResult(queryToken, new ArrayList(results))));
        ArrayList<WidgetsListBaseEntry> result = getFilteredWidgets(mDataProvider, query);
        mResultHandler.post(() -> callback.onSearchResult(query, result));
    }

    @Override
@@ -60,4 +59,36 @@ public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<Widge
            mResultHandler.removeCallbacksAndMessages(/*token= */null);
        }
    }

    /**
     * Returns entries for all matched widgets
     */
    public static ArrayList<WidgetsListBaseEntry> getFilteredWidgets(
            PopupDataProvider dataProvider, String input) {
        ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
        dataProvider.getAllWidgets().stream()
                .filter(entry -> entry instanceof WidgetsListHeaderEntry)
                .forEach(headerEntry -> {
                    List<WidgetItem> matchedWidgetItems = filterWidgetItems(
                            input, headerEntry.mPkgItem.title.toString(), headerEntry.mWidgets);
                    if (matchedWidgetItems.size() > 0) {
                        results.add(new WidgetsListSearchHeaderEntry(headerEntry.mPkgItem,
                                headerEntry.mTitleSectionName, matchedWidgetItems));
                        results.add(new WidgetsListContentEntry(headerEntry.mPkgItem,
                                headerEntry.mTitleSectionName, matchedWidgetItems));
                    }
                });
        return results;
    }

    private static List<WidgetItem> filterWidgetItems(String query, String packageTitle,
            List<WidgetItem> items) {
        StringMatcher matcher = StringMatcher.getInstance();
        if (matches(query, packageTitle, matcher)) {
            return items;
        }
        return items.stream()
                .filter(item -> matches(query, item.label, matcher))
                .collect(Collectors.toList());
    }
}
Loading