Using collator for string matching

This provides a better matching for non-latin characters on N and above

Bug: 63763127
Change-Id: I220487d242ff547311ddd13e7af380a7e47eec0e
This commit is contained in:
Sunny Goyal 2017-07-18 01:22:01 -07:00
parent 751ea1c10e
commit 05d2df1678
2 changed files with 73 additions and 4 deletions

View File

@ -20,6 +20,7 @@ import android.os.Handler;
import com.android.launcher3.AppInfo; import com.android.launcher3.AppInfo;
import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.ComponentKey;
import java.text.Collator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. // apps that don't match all of the words in the query.
final String queryTextLower = query.toLowerCase(); final String queryTextLower = query.toLowerCase();
final ArrayList<ComponentKey> result = new ArrayList<>(); final ArrayList<ComponentKey> result = new ArrayList<>();
StringMatcher matcher = StringMatcher.getInstance();
for (AppInfo info : mApps) { for (AppInfo info : mApps) {
if (matches(info, queryTextLower)) { if (matches(info, queryTextLower, matcher)) {
result.add(info.toComponentKey()); result.add(info.toComponentKey());
} }
} }
@ -70,6 +72,10 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
} }
public static boolean matches(AppInfo info, String query) { 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(); int queryLength = query.length();
String title = info.title.toString(); String title = info.title.toString();
@ -90,7 +96,7 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
nextType = i < (titleLength - 1) ? nextType = i < (titleLength - 1) ?
Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED; Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
if (isBreak(thisType, lastType, nextType) && if (isBreak(thisType, lastType, nextType) &&
title.substring(i, i + queryLength).equalsIgnoreCase(query)) { matcher.matches(query, title.substring(i, i + queryLength))) {
return true; return true;
} }
} }
@ -106,6 +112,13 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
* 4) Any capital character before a small character * 4) Any capital character before a small character
*/ */
private static boolean isBreak(int thisType, int prevType, int nextType) { 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) { switch (thisType) {
case Character.UPPERCASE_LETTER: case Character.UPPERCASE_LETTER:
if (nextType == Character.UPPERCASE_LETTER) { if (nextType == Character.UPPERCASE_LETTER) {
@ -132,8 +145,44 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
// Always a break point for a symbol // Always a break point for a symbol
return true; return true;
default: default:
// Always a break point at first character return false;
return prevType == Character.UNASSIGNED; }
}
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();
} }
} }
} }

View File

@ -19,6 +19,7 @@ import android.content.ComponentName;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.android.launcher3.AppInfo; import com.android.launcher3.AppInfo;
import com.android.launcher3.Utilities;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -75,6 +76,25 @@ public class DefaultAppSearchAlgorithmTest extends InstrumentationTestCase {
assertTrue(mAlgorithm.matches(getInfo("电子邮件"), "电子")); assertTrue(mAlgorithm.matches(getInfo("电子邮件"), "电子"));
assertFalse(mAlgorithm.matches(getInfo("电子邮件"), "")); assertFalse(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) { private AppInfo getInfo(String title) {