Add a WidgetsSearchBar(Launcher3) and a WidgetsSearchController.
- Make WidgetsSearchBar in Launcher3 initialize WidgetsSearchController with SimpleWidgetsSearchPipeline - Modify SimpleWidgetsSearchPipeline to filter widgets entries on widgets/shortcut labels also. Test: Tested prototype locally. Also added robolectric test. Bug: b/157286785 Change-Id: I65f5fa0240ffb6d22023167e4e86d94d83bbd9f7
This commit is contained in:
parent
334e65935b
commit
d07acba048
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorTertiary"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
|
||||
</vector>
|
|
@ -34,16 +34,5 @@
|
|||
android:textSize="24sp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/widget_button_text"/>
|
||||
<!-- Disable the search bar because it has not been implemented. -->
|
||||
<EditText
|
||||
android:id="@+id/widgets_search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_widgets_searchbox"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableStart="@drawable/ic_allapps_search"
|
||||
android:hint="@string/widgets_full_sheet_search_bar_hint"
|
||||
android:padding="12dp" />
|
||||
<include layout="@layout/widgets_search_bar"/>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.android.launcher3.widget.picker.search.WidgetsSearchBar
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widgets_search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_widgets_searchbox"
|
||||
android:padding="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/widgets_search_bar_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableStart="@drawable/ic_allapps_search"
|
||||
android:background="@null"
|
||||
android:hint="@string/widgets_full_sheet_search_bar_hint"
|
||||
android:maxLines="1"
|
||||
android:layout_weight="1"
|
||||
android:inputType="text"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/widgets_search_cancel_button"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:src="@drawable/ic_gm_close_24"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
</com.android.launcher3.widget.picker.search.WidgetsSearchBar>
|
|
@ -19,8 +19,6 @@ package com.android.launcher3.widget.picker.search;
|
|||
import static android.os.Looper.getMainLooper;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
@ -40,6 +38,7 @@ 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;
|
||||
|
@ -56,9 +55,6 @@ import java.util.List;
|
|||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class SimpleWidgetsSearchPipelineTest {
|
||||
private static final SimpleWidgetsSearchPipeline.StringMatcher MATCHER =
|
||||
SimpleWidgetsSearchPipeline.StringMatcher.getInstance();
|
||||
|
||||
@Mock private IconCache mIconCache;
|
||||
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
|
@ -73,9 +69,10 @@ public class SimpleWidgetsSearchPipelineTest {
|
|||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
|
||||
.getComponent().getPackageName())
|
||||
.when(mIconCache).getTitleNoCache(any());
|
||||
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;
|
||||
|
@ -85,54 +82,60 @@ public class SimpleWidgetsSearchPipelineTest {
|
|||
createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
|
||||
mCalendarContentEntry =
|
||||
createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
|
||||
mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 5);
|
||||
mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 5);
|
||||
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_shouldInformCallbackWithResultsMatchedOnAppName() {
|
||||
public void query_shouldMatchOnAppName() {
|
||||
SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
|
||||
List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
|
||||
mCameraContentEntry, mClockHeaderEntry, mClockContentEntry));
|
||||
|
||||
pipeline.query("Ca", results ->
|
||||
assertEquals(results, List.of(mCalendarHeaderEntry, mCalendarContentEntry,
|
||||
mCameraHeaderEntry, mCameraContentEntry)));
|
||||
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 testMatches() {
|
||||
assertTrue(MATCHER.matches("q", "Q"));
|
||||
assertTrue(MATCHER.matches("q", " Q"));
|
||||
assertTrue(MATCHER.matches("e", "elephant"));
|
||||
assertTrue(MATCHER.matches("eL", "Elephant"));
|
||||
assertTrue(MATCHER.matches("elephant ", "elephant"));
|
||||
assertTrue(MATCHER.matches("whitec", "white cow"));
|
||||
assertTrue(MATCHER.matches("white c", "white cow"));
|
||||
assertTrue(MATCHER.matches("white ", "white cow"));
|
||||
assertTrue(MATCHER.matches("white c", "white cow"));
|
||||
assertTrue(MATCHER.matches("电", "电子邮件"));
|
||||
assertTrue(MATCHER.matches("电子", "电子邮件"));
|
||||
assertTrue(MATCHER.matches("다", "다운로드"));
|
||||
assertTrue(MATCHER.matches("드", "드라이브"));
|
||||
assertTrue(MATCHER.matches("åbç", "abc"));
|
||||
assertTrue(MATCHER.matches("ål", "Alpha"));
|
||||
public void query_shouldMatchOnWidgetLabel() {
|
||||
SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
|
||||
List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
|
||||
mCameraContentEntry));
|
||||
|
||||
assertFalse(MATCHER.matches("phant", "elephant"));
|
||||
assertFalse(MATCHER.matches("elephants", "elephant"));
|
||||
assertFalse(MATCHER.matches("cow", "white cow"));
|
||||
assertFalse(MATCHER.matches("cow", "whiteCow"));
|
||||
assertFalse(MATCHER.matches("dog", "cats&Dogs"));
|
||||
assertFalse(MATCHER.matches("ba", "Bot"));
|
||||
assertFalse(MATCHER.matches("ba", "bot"));
|
||||
assertFalse(MATCHER.matches("子", "电子邮件"));
|
||||
assertFalse(MATCHER.matches("邮件", "电子邮件"));
|
||||
assertFalse(MATCHER.matches("ㄷ", "다운로드 드라이브"));
|
||||
assertFalse(MATCHER.matches("ㄷㄷ", "다운로드 드라이브"));
|
||||
assertFalse(MATCHER.matches("åç", "abc"));
|
||||
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,
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.android.launcher3.search.SearchAlgorithm;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class WidgetsSearchBarControllerTest {
|
||||
|
||||
private WidgetsSearchBarController mController;
|
||||
private Context mContext;
|
||||
private EditText mEditText;
|
||||
private ImageButton mCancelButton;
|
||||
@Mock
|
||||
private SearchModeListener mSearchModeListener;
|
||||
@Mock
|
||||
private SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.application;
|
||||
mEditText = new EditText(mContext);
|
||||
mCancelButton = new ImageButton(mContext);
|
||||
mController = new WidgetsSearchBarController(
|
||||
mSearchAlgorithm, mEditText, mCancelButton, mSearchModeListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onSearchResult_shouldInformSearchModeListener() {
|
||||
ArrayList<WidgetsListBaseEntry> entries = new ArrayList<>();
|
||||
mController.onSearchResult("abc", entries);
|
||||
|
||||
verify(mSearchModeListener).onSearchResults(entries);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_shouldInformSearchModeListenerToEnterSearch() {
|
||||
mEditText.setText("abc");
|
||||
|
||||
verify(mSearchModeListener).enterSearchMode();
|
||||
verifyNoMoreInteractions(mSearchModeListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_shouldDoSearch() {
|
||||
mEditText.setText("abc");
|
||||
|
||||
verify(mSearchAlgorithm).doSearch(eq("abc"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_shouldShowCancelButton() {
|
||||
mEditText.setText("abc");
|
||||
|
||||
assertEquals(mCancelButton.getVisibility(), View.VISIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_empty_shouldInformSearchModeListenerToExitSearch() {
|
||||
mEditText.setText("");
|
||||
|
||||
verify(mSearchModeListener).exitSearchMode();
|
||||
verifyNoMoreInteractions(mSearchModeListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_empty_shouldCancelSearch() {
|
||||
mEditText.setText("");
|
||||
|
||||
verify(mSearchAlgorithm).cancel(true);
|
||||
verifyNoMoreInteractions(mSearchAlgorithm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterTextChanged_empty_shouldHideCancelButton() {
|
||||
mEditText.setText("");
|
||||
|
||||
assertEquals(mCancelButton.getVisibility(), View.GONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelSearch_shouldInformSearchModeListenerToExitSearch() {
|
||||
mCancelButton.performClick();
|
||||
|
||||
verify(mSearchModeListener).exitSearchMode();
|
||||
verifyNoMoreInteractions(mSearchModeListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelSearch_shouldCancelSearch() {
|
||||
mCancelButton.performClick();
|
||||
|
||||
verify(mSearchAlgorithm).cancel(true);
|
||||
verifyNoMoreInteractions(mSearchAlgorithm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelSearch_shouldClearSearchBar() {
|
||||
mCancelButton.performClick();
|
||||
|
||||
assertEquals(mEditText.getText().toString(), "");
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import com.android.launcher3.model.AllAppsList;
|
|||
import com.android.launcher3.model.BaseModelUpdateTask;
|
||||
import com.android.launcher3.model.BgDataModel;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.search.StringMatcherUtility;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -67,10 +68,10 @@ public class AppsSearchPipeline implements SearchPipeline {
|
|||
// apps that don't match all of the words in the query.
|
||||
final String queryTextLower = query.toLowerCase();
|
||||
final ArrayList<AppInfo> result = new ArrayList<>();
|
||||
DefaultAppSearchAlgorithm.StringMatcher matcher =
|
||||
DefaultAppSearchAlgorithm.StringMatcher.getInstance();
|
||||
StringMatcherUtility.StringMatcher matcher =
|
||||
StringMatcherUtility.StringMatcher.getInstance();
|
||||
for (AppInfo info : apps) {
|
||||
if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
|
||||
if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) {
|
||||
result.add(info);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,9 @@ import android.os.Handler;
|
|||
|
||||
import com.android.launcher3.LauncherAppState;
|
||||
import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.search.SearchAlgorithm;
|
||||
import com.android.launcher3.search.SearchCallback;
|
||||
|
||||
import java.text.Collator;
|
||||
|
||||
/**
|
||||
* The default search implementation.
|
||||
*/
|
||||
|
@ -54,132 +51,4 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
|
|||
() -> callback.onSearchResult(query, results)),
|
||||
null);
|
||||
}
|
||||
|
||||
public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
|
||||
int queryLength = query.length();
|
||||
|
||||
String title = info.title.toString();
|
||||
int titleLength = title.length();
|
||||
|
||||
if (titleLength < queryLength || queryLength <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestSimpleFuzzySearch(query)) {
|
||||
return title.toLowerCase().contains(query);
|
||||
}
|
||||
|
||||
int lastType;
|
||||
int thisType = Character.UNASSIGNED;
|
||||
int nextType = Character.getType(title.codePointAt(0));
|
||||
|
||||
int end = titleLength - queryLength;
|
||||
for (int i = 0; i <= end; i++) {
|
||||
lastType = thisType;
|
||||
thisType = nextType;
|
||||
nextType = i < (titleLength - 1) ?
|
||||
Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
|
||||
if (isBreak(thisType, lastType, nextType) &&
|
||||
matcher.matches(query, title.substring(i, i + queryLength))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current point should be a break point. Following cases
|
||||
* are considered as break points:
|
||||
* 1) Any non space character after a space character
|
||||
* 2) Any digit after a non-digit character
|
||||
* 3) Any capital character after a digit or small character
|
||||
* 4) Any capital character before a small character
|
||||
*/
|
||||
private static boolean isBreak(int thisType, int prevType, int nextType) {
|
||||
switch (prevType) {
|
||||
case Character.UNASSIGNED:
|
||||
case Character.SPACE_SEPARATOR:
|
||||
case Character.LINE_SEPARATOR:
|
||||
case Character.PARAGRAPH_SEPARATOR:
|
||||
return true;
|
||||
}
|
||||
switch (thisType) {
|
||||
case Character.UPPERCASE_LETTER:
|
||||
if (nextType == Character.UPPERCASE_LETTER) {
|
||||
return true;
|
||||
}
|
||||
// Follow through
|
||||
case Character.TITLECASE_LETTER:
|
||||
// Break point if previous was not a upper case
|
||||
return prevType != Character.UPPERCASE_LETTER;
|
||||
case Character.LOWERCASE_LETTER:
|
||||
// Break point if previous was not a letter.
|
||||
return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
|
||||
case Character.DECIMAL_DIGIT_NUMBER:
|
||||
case Character.LETTER_NUMBER:
|
||||
case Character.OTHER_NUMBER:
|
||||
// Break point if previous was not a number
|
||||
return !(prevType == Character.DECIMAL_DIGIT_NUMBER
|
||||
|| prevType == Character.LETTER_NUMBER
|
||||
|| prevType == Character.OTHER_NUMBER);
|
||||
case Character.MATH_SYMBOL:
|
||||
case Character.CURRENCY_SYMBOL:
|
||||
case Character.OTHER_PUNCTUATION:
|
||||
case Character.DASH_PUNCTUATION:
|
||||
// Always a break point for a symbol
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringMatcher {
|
||||
|
||||
private static final char MAX_UNICODE = '\uFFFF';
|
||||
|
||||
private final Collator mCollator;
|
||||
|
||||
StringMatcher() {
|
||||
// On android N and above, Collator uses ICU implementation which has a much better
|
||||
// support for non-latin locales.
|
||||
mCollator = Collator.getInstance();
|
||||
mCollator.setStrength(Collator.PRIMARY);
|
||||
mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@param query} is a prefix of {@param target}
|
||||
*/
|
||||
public boolean matches(String query, String target) {
|
||||
switch (mCollator.compare(query, target)) {
|
||||
case 0:
|
||||
return true;
|
||||
case -1:
|
||||
// The target string can contain a modifier which would make it larger than
|
||||
// the query string (even though the length is same). If the query becomes
|
||||
// larger after appending a unicode character, it was originally a prefix of
|
||||
// the target string and hence should match.
|
||||
return mCollator.compare(query + MAX_UNICODE, target) > -1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static StringMatcher getInstance() {
|
||||
return new StringMatcher();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean requestSimpleFuzzySearch(String s) {
|
||||
for (int i = 0; i < s.length(); ) {
|
||||
int codepoint = s.codePointAt(i);
|
||||
i += Character.charCount(codepoint);
|
||||
switch (Character.UnicodeScript.of(codepoint)) {
|
||||
case HAN:
|
||||
//Character.UnicodeScript.HAN: use String.contains to match
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,9 @@ public interface SearchAlgorithm<T> {
|
|||
* Cancels any active request.
|
||||
*/
|
||||
void cancel(boolean interruptActiveRequests);
|
||||
|
||||
/**
|
||||
* Cleans up after search is no longer needed.
|
||||
*/
|
||||
default void destroy() {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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.search;
|
||||
|
||||
import java.text.Collator;
|
||||
|
||||
/**
|
||||
* Utilities for matching query string to target string.
|
||||
*/
|
||||
public class StringMatcherUtility {
|
||||
|
||||
/**
|
||||
* Returns {@code true} is {@code query} is a prefix substring of a complete word/phrase in
|
||||
* {@code target}.
|
||||
*/
|
||||
public static boolean matches(String query, String target, StringMatcher matcher) {
|
||||
int queryLength = query.length();
|
||||
|
||||
int targetLength = target.length();
|
||||
|
||||
if (targetLength < queryLength || queryLength <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestSimpleFuzzySearch(query)) {
|
||||
return target.toLowerCase().contains(query);
|
||||
}
|
||||
|
||||
int lastType;
|
||||
int thisType = Character.UNASSIGNED;
|
||||
int nextType = Character.getType(target.codePointAt(0));
|
||||
|
||||
int end = targetLength - queryLength;
|
||||
for (int i = 0; i <= end; i++) {
|
||||
lastType = thisType;
|
||||
thisType = nextType;
|
||||
nextType = i < (targetLength - 1)
|
||||
? Character.getType(target.codePointAt(i + 1)) : Character.UNASSIGNED;
|
||||
if (isBreak(thisType, lastType, nextType)
|
||||
&& matcher.matches(query, target.substring(i, i + queryLength))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current point should be a break point. Following cases
|
||||
* are considered as break points:
|
||||
* 1) Any non space character after a space character
|
||||
* 2) Any digit after a non-digit character
|
||||
* 3) Any capital character after a digit or small character
|
||||
* 4) Any capital character before a small character
|
||||
*/
|
||||
private static boolean isBreak(int thisType, int prevType, int nextType) {
|
||||
switch (prevType) {
|
||||
case Character.UNASSIGNED:
|
||||
case Character.SPACE_SEPARATOR:
|
||||
case Character.LINE_SEPARATOR:
|
||||
case Character.PARAGRAPH_SEPARATOR:
|
||||
return true;
|
||||
}
|
||||
switch (thisType) {
|
||||
case Character.UPPERCASE_LETTER:
|
||||
if (nextType == Character.UPPERCASE_LETTER) {
|
||||
return true;
|
||||
}
|
||||
// Follow through
|
||||
case Character.TITLECASE_LETTER:
|
||||
// Break point if previous was not a upper case
|
||||
return prevType != Character.UPPERCASE_LETTER;
|
||||
case Character.LOWERCASE_LETTER:
|
||||
// Break point if previous was not a letter.
|
||||
return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
|
||||
case Character.DECIMAL_DIGIT_NUMBER:
|
||||
case Character.LETTER_NUMBER:
|
||||
case Character.OTHER_NUMBER:
|
||||
// Break point if previous was not a number
|
||||
return !(prevType == Character.DECIMAL_DIGIT_NUMBER
|
||||
|| prevType == Character.LETTER_NUMBER
|
||||
|| prevType == Character.OTHER_NUMBER);
|
||||
case Character.MATH_SYMBOL:
|
||||
case Character.CURRENCY_SYMBOL:
|
||||
case Character.OTHER_PUNCTUATION:
|
||||
case Character.DASH_PUNCTUATION:
|
||||
// Always a break point for a symbol
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs locale sensitive string comparison using {@link Collator}.
|
||||
*/
|
||||
public static class StringMatcher {
|
||||
|
||||
private static final char MAX_UNICODE = '\uFFFF';
|
||||
|
||||
private final Collator mCollator;
|
||||
|
||||
StringMatcher() {
|
||||
// On android N and above, Collator uses ICU implementation which has a much better
|
||||
// support for non-latin locales.
|
||||
mCollator = Collator.getInstance();
|
||||
mCollator.setStrength(Collator.PRIMARY);
|
||||
mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@param query} is a prefix of {@param target}
|
||||
*/
|
||||
public boolean matches(String query, String target) {
|
||||
switch (mCollator.compare(query, target)) {
|
||||
case 0:
|
||||
return true;
|
||||
case -1:
|
||||
// The target string can contain a modifier which would make it larger than
|
||||
// the query string (even though the length is same). If the query becomes
|
||||
// larger after appending a unicode character, it was originally a prefix of
|
||||
// the target string and hence should match.
|
||||
return mCollator.compare(query + MAX_UNICODE, target) > -1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static StringMatcher getInstance() {
|
||||
return new StringMatcher();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matching optimization to search in Chinese.
|
||||
*/
|
||||
private static boolean requestSimpleFuzzySearch(String s) {
|
||||
for (int i = 0; i < s.length(); ) {
|
||||
int codepoint = s.codePointAt(i);
|
||||
i += Character.charCount(codepoint);
|
||||
switch (Character.UnicodeScript.of(codepoint)) {
|
||||
case HAN:
|
||||
//Character.UnicodeScript.HAN: use String.contains to match
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -20,10 +20,14 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.widget.WidgetItemComparator;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Holder class to store the package information of an entry shown in the widgets list. */
|
||||
public abstract class WidgetsListBaseEntry {
|
||||
|
@ -35,9 +39,14 @@ public abstract class WidgetsListBaseEntry {
|
|||
*/
|
||||
public final String mTitleSectionName;
|
||||
|
||||
public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) {
|
||||
public final List<WidgetItem> mWidgets;
|
||||
|
||||
public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName,
|
||||
List<WidgetItem> items) {
|
||||
mPkgItem = pkgItem;
|
||||
mTitleSectionName = titleSectionName;
|
||||
this.mWidgets =
|
||||
items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,10 +17,8 @@ package com.android.launcher3.widget.model;
|
|||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.widget.WidgetItemComparator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Holder class to store all the information related to a list of widgets from the same app which is
|
||||
|
@ -28,13 +26,9 @@ import java.util.stream.Collectors;
|
|||
*/
|
||||
public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
|
||||
|
||||
public final List<WidgetItem> mWidgets;
|
||||
|
||||
public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
|
||||
List<WidgetItem> items) {
|
||||
super(pkgItem, titleSectionName);
|
||||
this.mWidgets =
|
||||
items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
|
||||
super(pkgItem, titleSectionName, items);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -47,4 +41,12 @@ public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
|
|||
public int getRank() {
|
||||
return RANK_WIDGETS_LIST_CONTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof WidgetsListContentEntry)) return false;
|
||||
WidgetsListContentEntry otherEntry = (WidgetsListContentEntry) obj;
|
||||
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
|
||||
&& mTitleSectionName.equals(otherEntry.mTitleSectionName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,25 +17,21 @@ package com.android.launcher3.widget.model;
|
|||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.widget.WidgetItemComparator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** An information holder for an app which has widgets or/and shortcuts. */
|
||||
public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
|
||||
|
||||
public final int widgetsCount;
|
||||
public final int shortcutsCount;
|
||||
public final List<WidgetItem> mWidgets;
|
||||
|
||||
private boolean mIsWidgetListShown = false;
|
||||
private boolean mHasEntryUpdated = false;
|
||||
|
||||
public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
|
||||
List<WidgetItem> items) {
|
||||
super(pkgItem, titleSectionName);
|
||||
mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
|
||||
super(pkgItem, titleSectionName, items);
|
||||
widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
|
||||
shortcutsCount = Math.max(0, items.size() - widgetsCount);
|
||||
}
|
||||
|
|
|
@ -17,23 +17,18 @@ package com.android.launcher3.widget.model;
|
|||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.widget.WidgetItemComparator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
|
||||
public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry {
|
||||
|
||||
public final List<WidgetItem> mWidgets;
|
||||
|
||||
private boolean mIsWidgetListShown = false;
|
||||
private boolean mHasEntryUpdated = false;
|
||||
|
||||
public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
|
||||
List<WidgetItem> items) {
|
||||
super(pkgItem, titleSectionName);
|
||||
mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
|
||||
super(pkgItem, titleSectionName, items);
|
||||
}
|
||||
|
||||
/** Sets if the widgets list associated with this header is shown. */
|
||||
|
|
|
@ -16,12 +16,19 @@
|
|||
|
||||
package com.android.launcher3.widget.picker.search;
|
||||
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
import static com.android.launcher3.search.StringMatcherUtility.matches;
|
||||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
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.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implementation of {@link WidgetsPickerSearchPipeline} that performs search by prefix matching on
|
||||
|
@ -37,52 +44,29 @@ public final class SimpleWidgetsSearchPipeline implements WidgetsPickerSearchPip
|
|||
|
||||
@Override
|
||||
public void query(String input, Consumer<List<WidgetsListBaseEntry>> callback) {
|
||||
StringMatcher matcher = StringMatcher.getInstance();
|
||||
ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
|
||||
// TODO(b/157286785): Filter entries based on query prefix matching on widget labels also.
|
||||
for (WidgetsListBaseEntry e : mAllEntries) {
|
||||
if (matcher.matches(input, e.mPkgItem.title.toString())) {
|
||||
results.add(e);
|
||||
}
|
||||
}
|
||||
mAllEntries.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));
|
||||
}
|
||||
});
|
||||
callback.accept(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs locale sensitive string comparison using {@link Collator}.
|
||||
*/
|
||||
public static class StringMatcher {
|
||||
|
||||
private static final char MAX_UNICODE = '\uFFFF';
|
||||
|
||||
private final Collator mCollator;
|
||||
|
||||
StringMatcher() {
|
||||
mCollator = Collator.getInstance();
|
||||
mCollator.setStrength(Collator.PRIMARY);
|
||||
mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@param query} is a prefix of {@param target}.
|
||||
*/
|
||||
public boolean matches(String query, String target) {
|
||||
switch (mCollator.compare(query, target)) {
|
||||
case 0:
|
||||
return true;
|
||||
case -1:
|
||||
// The target string can contain a modifier which would make it larger than
|
||||
// the query string (even though the length is same). If the query becomes
|
||||
// larger after appending a unicode character, it was originally a prefix of
|
||||
// the target string and hence should match.
|
||||
return mCollator.compare(query + MAX_UNICODE, target) > -1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static StringMatcher getInstance() {
|
||||
return new StringMatcher();
|
||||
private 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.search.SearchAlgorithm;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* View for a search bar with an edit text with a cancel button.
|
||||
*/
|
||||
public class WidgetsSearchBar extends LinearLayout {
|
||||
private WidgetsSearchBarController mController;
|
||||
private EditText mEditText;
|
||||
private ImageButton mCancelButton;
|
||||
|
||||
public WidgetsSearchBar(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public WidgetsSearchBar(@NonNull Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public WidgetsSearchBar(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a controller to the search bar which interacts with {@code searchModeListener}.
|
||||
*/
|
||||
public void initialize(List<WidgetsListBaseEntry> allWidgets,
|
||||
SearchModeListener searchModeListener) {
|
||||
SearchAlgorithm<WidgetsListBaseEntry> algo =
|
||||
new SimpleWidgetsSearchAlgorithm(new SimpleWidgetsSearchPipeline(allWidgets));
|
||||
mController = new WidgetsSearchBarController(
|
||||
algo, mEditText, mCancelButton, searchModeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mEditText = findViewById(R.id.widgets_search_bar_edit_text);
|
||||
mCancelButton = findViewById(R.id.widgets_search_cancel_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mController.onDestroy();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.android.launcher3.search.SearchAlgorithm;
|
||||
import com.android.launcher3.search.SearchCallback;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Controller for a search bar with an edit text and a cancel button.
|
||||
*/
|
||||
public class WidgetsSearchBarController implements TextWatcher,
|
||||
SearchCallback<WidgetsListBaseEntry> {
|
||||
private static final String TAG = "WidgetsSearchBarController";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
protected SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
|
||||
protected EditText mInput;
|
||||
protected ImageButton mCancelButton;
|
||||
protected SearchModeListener mSearchModeListener;
|
||||
protected String mQuery;
|
||||
|
||||
public WidgetsSearchBarController(
|
||||
SearchAlgorithm<WidgetsListBaseEntry> algo, EditText editText, ImageButton cancelButton,
|
||||
SearchModeListener searchModeListener) {
|
||||
mSearchAlgorithm = algo;
|
||||
mInput = editText;
|
||||
mInput.addTextChangedListener(this);
|
||||
mCancelButton = cancelButton;
|
||||
mCancelButton.setOnClickListener(v -> clearSearchResult());
|
||||
mSearchModeListener = searchModeListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
mQuery = s.toString();
|
||||
if (mQuery.isEmpty()) {
|
||||
mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
|
||||
mSearchModeListener.exitSearchMode();
|
||||
mCancelButton.setVisibility(GONE);
|
||||
} else {
|
||||
mSearchAlgorithm.cancel(/* interruptActiveRequests= */ false);
|
||||
mSearchModeListener.enterSearchMode();
|
||||
mSearchAlgorithm.doSearch(mQuery, this);
|
||||
mCancelButton.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSearchResult query: " + query + " items: " + items);
|
||||
}
|
||||
mSearchModeListener.onSearchResults(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
|
||||
// Not needed.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSearchResult() {
|
||||
mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
|
||||
mInput.getText().clear();
|
||||
mInput.clearFocus();
|
||||
mSearchModeListener.exitSearchMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up after search is no longer needed.
|
||||
*/
|
||||
public void onDestroy() {
|
||||
mSearchAlgorithm.destroy();
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 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.allapps.search;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.ComponentName;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultAppSearchAlgorithm}
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultAppSearchAlgorithmTest {
|
||||
private static final DefaultAppSearchAlgorithm.StringMatcher MATCHER =
|
||||
DefaultAppSearchAlgorithm.StringMatcher.getInstance();
|
||||
|
||||
@Test
|
||||
public void testMatches() {
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white cow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCOW"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCOW"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white2cow"), "cow", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitEcow"), "cow", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecow cow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecowcow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whit ecowcow"), "cow", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&dogs"), "dog", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "dog", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "&", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "43", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "3", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Q"), "q", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo(" Q"), "q", MATCHER));
|
||||
|
||||
// match lower case words
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("elephant"), "e", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电子", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "子", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "邮件", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("Bot"), "ba", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("bot"), "ba", MATCHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesVN() {
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드"), "다", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("드라이브"), "드", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åbç", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Alpha"), "ål", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("로드라이브"), "ㄷ", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åç", MATCHER));
|
||||
}
|
||||
|
||||
private AppInfo getInfo(String title) {
|
||||
AppInfo info = new AppInfo();
|
||||
info.title = title;
|
||||
info.componentName = new ComponentName("Test", title);
|
||||
return info;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (C) 2016 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.search;
|
||||
|
||||
import static com.android.launcher3.search.StringMatcherUtility.matches;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StringMatcherUtility}
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class StringMatcherUtilityTest {
|
||||
private static final StringMatcher MATCHER =
|
||||
StringMatcher.getInstance();
|
||||
|
||||
@Test
|
||||
public void testMatches() {
|
||||
assertTrue(matches("white ", "white cow", MATCHER));
|
||||
assertTrue(matches("white c", "white cow", MATCHER));
|
||||
assertTrue(matches("cow", "white cow", MATCHER));
|
||||
assertTrue(matches("cow", "whiteCow", MATCHER));
|
||||
assertTrue(matches("cow", "whiteCOW", MATCHER));
|
||||
assertTrue(matches("cow", "whitecowCOW", MATCHER));
|
||||
assertTrue(matches("cow", "white2cow", MATCHER));
|
||||
|
||||
assertFalse(matches("cow", "whitecow", MATCHER));
|
||||
assertFalse(matches("cow", "whitEcow", MATCHER));
|
||||
|
||||
assertTrue(matches("cow", "whitecowCow", MATCHER));
|
||||
assertTrue(matches("cow", "whitecow cow", MATCHER));
|
||||
assertFalse(matches("cow", "whitecowcow", MATCHER));
|
||||
assertFalse(matches("cow", "whit ecowcow", MATCHER));
|
||||
|
||||
assertTrue(matches("dog", "cats&dogs", MATCHER));
|
||||
assertTrue(matches("dog", "cats&Dogs", MATCHER));
|
||||
assertTrue(matches("&", "cats&Dogs", MATCHER));
|
||||
|
||||
assertTrue(matches("43", "2+43", MATCHER));
|
||||
assertFalse(matches("3", "2+43", MATCHER));
|
||||
|
||||
assertTrue(matches("q", "Q", MATCHER));
|
||||
assertTrue(matches("q", " Q", MATCHER));
|
||||
|
||||
// match lower case words
|
||||
assertTrue(matches("e", "elephant", MATCHER));
|
||||
assertTrue(matches("eL", "Elephant", MATCHER));
|
||||
|
||||
assertTrue(matches("电", "电子邮件", MATCHER));
|
||||
assertTrue(matches("电子", "电子邮件", MATCHER));
|
||||
assertTrue(matches("子", "电子邮件", MATCHER));
|
||||
assertTrue(matches("邮件", "电子邮件", MATCHER));
|
||||
|
||||
assertFalse(matches("ba", "Bot", MATCHER));
|
||||
assertFalse(matches("ba", "bot", MATCHER));
|
||||
assertFalse(matches("phant", "elephant", MATCHER));
|
||||
assertFalse(matches("elephants", "elephant", MATCHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesVN() {
|
||||
assertTrue(matches("다", "다운로드", MATCHER));
|
||||
assertTrue(matches("드", "드라이브", MATCHER));
|
||||
assertTrue(matches("ㄷ", "다운로드 드라이브", MATCHER));
|
||||
assertTrue(matches("ㄷ", "운로 드라이브", MATCHER));
|
||||
assertTrue(matches("åbç", "abc", MATCHER));
|
||||
assertTrue(matches("ål", "Alpha", MATCHER));
|
||||
|
||||
assertFalse(matches("ㄷㄷ", "다운로드 드라이브", MATCHER));
|
||||
assertFalse(matches("ㄷ", "로드라이브", MATCHER));
|
||||
assertFalse(matches("åç", "abc", MATCHER));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue