diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java index 43033024f2..21eb3fba05 100644 --- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java +++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java @@ -20,6 +20,7 @@ import android.os.Handler; import com.android.launcher3.AppInfo; import com.android.launcher3.util.ComponentKey; +import java.text.Collator; import java.util.ArrayList; import java.util.List; @@ -61,8 +62,9 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { // apps that don't match all of the words in the query. final String queryTextLower = query.toLowerCase(); final ArrayList result = new ArrayList<>(); + StringMatcher matcher = StringMatcher.getInstance(); for (AppInfo info : mApps) { - if (matches(info, queryTextLower)) { + if (matches(info, queryTextLower, matcher)) { result.add(info.toComponentKey()); } } @@ -70,6 +72,10 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { } public static boolean matches(AppInfo info, String query) { + return matches(info, query, StringMatcher.getInstance()); + } + + public static boolean matches(AppInfo info, String query, StringMatcher matcher) { int queryLength = query.length(); String title = info.title.toString(); @@ -90,7 +96,7 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { nextType = i < (titleLength - 1) ? Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED; if (isBreak(thisType, lastType, nextType) && - title.substring(i, i + queryLength).equalsIgnoreCase(query)) { + matcher.matches(query, title.substring(i, i + queryLength))) { return true; } } @@ -106,6 +112,13 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { * 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) { @@ -132,8 +145,44 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { // Always a break point for a symbol return true; default: - // Always a break point at first character - return prevType == Character.UNASSIGNED; + 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(); } } } diff --git a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java index 58dc0c43e1..26ec69b1c7 100644 --- a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java +++ b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java @@ -19,6 +19,7 @@ import android.content.ComponentName; import android.test.InstrumentationTestCase; import com.android.launcher3.AppInfo; +import com.android.launcher3.Utilities; import java.util.ArrayList; import java.util.List; @@ -75,6 +76,25 @@ public class DefaultAppSearchAlgorithmTest extends InstrumentationTestCase { assertTrue(mAlgorithm.matches(getInfo("电子邮件"), "电子")); assertFalse(mAlgorithm.matches(getInfo("电子邮件"), "子")); assertFalse(mAlgorithm.matches(getInfo("电子邮件"), "邮件")); + + assertFalse(mAlgorithm.matches(getInfo("Bot"), "ba")); + assertFalse(mAlgorithm.matches(getInfo("bot"), "ba")); + } + + public void testMatchesVN() { + if (!Utilities.ATLEAST_NOUGAT) { + return; + } + assertTrue(mAlgorithm.matches(getInfo("다운로드"), "다")); + assertTrue(mAlgorithm.matches(getInfo("드라이브"), "드")); + assertTrue(mAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ")); + assertTrue(mAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ")); + assertTrue(mAlgorithm.matches(getInfo("abc"), "åbç")); + assertTrue(mAlgorithm.matches(getInfo("Alpha"), "ål")); + + assertFalse(mAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ")); + assertFalse(mAlgorithm.matches(getInfo("로드라이브"), "ㄷ")); + assertFalse(mAlgorithm.matches(getInfo("abc"), "åç")); } private AppInfo getInfo(String title) {