blob: acab52bbe8fcebd2b647c1e1d50d3e54ce95f177 [file] [log] [blame]
/*
* 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;
}
}