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:
parent
751ea1c10e
commit
05d2df1678
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue