From e4a3e0cfaf5b72a54e99bcebdc03eda8aef53091 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 8 Sep 2016 20:31:38 -0700 Subject: [PATCH] Adding support for searching word segments when camelcase is used in the title Bug: 30880421 Change-Id: If642d24408fe0e80b5a0dd9a2a57d2f3c2d92ea9 --- src/com/android/launcher3/AppInfo.java | 2 +- .../allapps/DefaultAppSearchAlgorithm.java | 82 ++++++++++++++++------ .../allapps/DefaultAppSearchAlgorithmTest.java | 78 ++++++++++++++++++++ 3 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index 604b16463..4c4d67c59 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -62,7 +62,7 @@ public class AppInfo extends ItemInfo { */ int isDisabled = ShortcutInfo.DEFAULT; - AppInfo() { + public AppInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; } diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java index 10740ec77..ac22dd279 100644 --- a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java +++ b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java @@ -22,15 +22,12 @@ import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; /** * The default search implementation. */ public class DefaultAppSearchAlgorithm { - private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); - private final List mApps; protected final Handler mResultHandler; @@ -61,34 +58,79 @@ public class DefaultAppSearchAlgorithm { // Do an intersection of the words in the query and each title, and filter out all the // apps that don't match all of the words in the query. final String queryTextLower = query.toLowerCase(); - final String[] queryWords = SPLIT_PATTERN.split(queryTextLower); - final ArrayList result = new ArrayList<>(); for (AppInfo info : mApps) { - if (matches(info, queryWords)) { + if (matches(info, queryTextLower)) { result.add(info.toComponentKey()); } } return result; } - protected boolean matches(AppInfo info, String[] queryWords) { + protected boolean matches(AppInfo info, String query) { + int queryLength = query.length(); + String title = info.title.toString(); - String[] words = SPLIT_PATTERN.split(title.toLowerCase()); - for (int qi = 0; qi < queryWords.length; qi++) { - boolean foundMatch = false; - for (int i = 0; i < words.length; i++) { - if (words[i].startsWith(queryWords[qi])) { - foundMatch = true; - break; - } + int titleLength = title.length(); + + if (titleLength < queryLength || queryLength <= 0) { + return false; + } + + 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) && + title.substring(i, i + queryLength).equalsIgnoreCase(query)) { + return true; } - if (!foundMatch) { - // If there is a word in the query that does not match any words in this - // title, so skip it. + } + 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 + */ + protected boolean isBreak(int thisType, int prevType, int nextType) { + 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; + 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; - } } - return true; } } diff --git a/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java new file mode 100644 index 000000000..4d0a7a9bd --- /dev/null +++ b/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java @@ -0,0 +1,78 @@ +/* + * 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; + +import android.content.ComponentName; +import android.test.InstrumentationTestCase; + +import com.android.launcher3.AppInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link DefaultAppSearchAlgorithm} + */ +public class DefaultAppSearchAlgorithmTest extends InstrumentationTestCase { + + private List mAppsList; + private DefaultAppSearchAlgorithm mAlgorithm; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mAppsList = new ArrayList<>(); + getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mAlgorithm = new DefaultAppSearchAlgorithm(mAppsList); + } + }); + } + + public void testMatches() { + assertTrue(mAlgorithm.matches(getInfo("white cow"), "cow")); + assertTrue(mAlgorithm.matches(getInfo("whiteCow"), "cow")); + assertTrue(mAlgorithm.matches(getInfo("whiteCOW"), "cow")); + assertTrue(mAlgorithm.matches(getInfo("whitecowCOW"), "cow")); + assertTrue(mAlgorithm.matches(getInfo("white2cow"), "cow")); + + assertFalse(mAlgorithm.matches(getInfo("whitecow"), "cow")); + assertFalse(mAlgorithm.matches(getInfo("whitEcow"), "cow")); + + assertTrue(mAlgorithm.matches(getInfo("whitecowCow"), "cow")); + assertTrue(mAlgorithm.matches(getInfo("whitecow cow"), "cow")); + assertFalse(mAlgorithm.matches(getInfo("whitecowcow"), "cow")); + assertFalse(mAlgorithm.matches(getInfo("whit ecowcow"), "cow")); + + assertTrue(mAlgorithm.matches(getInfo("cats&dogs"), "dog")); + assertTrue(mAlgorithm.matches(getInfo("cats&Dogs"), "dog")); + assertTrue(mAlgorithm.matches(getInfo("cats&Dogs"), "&")); + + assertTrue(mAlgorithm.matches(getInfo("2+43"), "43")); + assertFalse(mAlgorithm.matches(getInfo("2+43"), "3")); + + assertTrue(mAlgorithm.matches(getInfo("Q"), "q")); + assertTrue(mAlgorithm.matches(getInfo(" Q"), "q")); + } + + private AppInfo getInfo(String title) { + AppInfo info = new AppInfo(); + info.title = title; + info.componentName = new ComponentName("Test", title); + return info; + } +} -- 2.11.0