1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.allapps.search; 17 18 import android.os.Handler; 19 20 import com.android.launcher3.AppInfo; 21 import com.android.launcher3.util.ComponentKey; 22 23 import java.text.Collator; 24 import java.util.ArrayList; 25 import java.util.List; 26 27 /** 28 * The default search implementation. 29 */ 30 public class DefaultAppSearchAlgorithm implements SearchAlgorithm { 31 32 private final List<AppInfo> mApps; 33 protected final Handler mResultHandler; 34 35 public DefaultAppSearchAlgorithm(List<AppInfo> apps) { 36 mApps = apps; 37 mResultHandler = new Handler(); 38 } 39 40 @Override 41 public void cancel(boolean interruptActiveRequests) { 42 if (interruptActiveRequests) { 43 mResultHandler.removeCallbacksAndMessages(null); 44 } 45 } 46 47 @Override 48 public void doSearch(final String query, 49 final AllAppsSearchBarController.Callbacks callback) { 50 final ArrayList<ComponentKey> result = getTitleMatchResult(query); 51 mResultHandler.post(new Runnable() { 52 53 @Override 54 public void run() { 55 callback.onSearchResult(query, result); 56 } 57 }); 58 } 59 60 private ArrayList<ComponentKey> getTitleMatchResult(String query) { 61 // Do an intersection of the words in the query and each title, and filter out all the 62 // apps that don't match all of the words in the query. 63 final String queryTextLower = query.toLowerCase(); 64 final ArrayList<ComponentKey> result = new ArrayList<>(); 65 StringMatcher matcher = StringMatcher.getInstance(); 66 for (AppInfo info : mApps) { 67 if (matches(info, queryTextLower, matcher)) { 68 result.add(info.toComponentKey()); 69 } 70 } 71 return result; 72 } 73 74 public static boolean matches(AppInfo info, String query, StringMatcher matcher) { 75 int queryLength = query.length(); 76 77 String title = info.title.toString(); 78 int titleLength = title.length(); 79 80 if (titleLength < queryLength || queryLength <= 0) { 81 return false; 82 } 83 84 int lastType; 85 int thisType = Character.UNASSIGNED; 86 int nextType = Character.getType(title.codePointAt(0)); 87 88 int end = titleLength - queryLength; 89 for (int i = 0; i <= end; i++) { 90 lastType = thisType; 91 thisType = nextType; 92 nextType = i < (titleLength - 1) ? 93 Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED; 94 if (isBreak(thisType, lastType, nextType) && 95 matcher.matches(query, title.substring(i, i + queryLength))) { 96 return true; 97 } 98 } 99 return false; 100 } 101 102 /** 103 * Returns true if the current point should be a break point. Following cases 104 * are considered as break points: 105 * 1) Any non space character after a space character 106 * 2) Any digit after a non-digit character 107 * 3) Any capital character after a digit or small character 108 * 4) Any capital character before a small character 109 */ 110 private static boolean isBreak(int thisType, int prevType, int nextType) { 111 switch (prevType) { 112 case Character.UNASSIGNED: 113 case Character.SPACE_SEPARATOR: 114 case Character.LINE_SEPARATOR: 115 case Character.PARAGRAPH_SEPARATOR: 116 return true; 117 } 118 switch (thisType) { 119 case Character.UPPERCASE_LETTER: 120 if (nextType == Character.UPPERCASE_LETTER) { 121 return true; 122 } 123 // Follow through 124 case Character.TITLECASE_LETTER: 125 // Break point if previous was not a upper case 126 return prevType != Character.UPPERCASE_LETTER; 127 case Character.LOWERCASE_LETTER: 128 // Break point if previous was not a letter. 129 return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED; 130 case Character.DECIMAL_DIGIT_NUMBER: 131 case Character.LETTER_NUMBER: 132 case Character.OTHER_NUMBER: 133 // Break point if previous was not a number 134 return !(prevType == Character.DECIMAL_DIGIT_NUMBER 135 || prevType == Character.LETTER_NUMBER 136 || prevType == Character.OTHER_NUMBER); 137 case Character.MATH_SYMBOL: 138 case Character.CURRENCY_SYMBOL: 139 case Character.OTHER_PUNCTUATION: 140 case Character.DASH_PUNCTUATION: 141 // Always a break point for a symbol 142 return true; 143 default: 144 return false; 145 } 146 } 147 148 public static class StringMatcher { 149 150 private static final char MAX_UNICODE = '\uFFFF'; 151 152 private final Collator mCollator; 153 154 StringMatcher() { 155 // On android N and above, Collator uses ICU implementation which has a much better 156 // support for non-latin locales. 157 mCollator = Collator.getInstance(); 158 mCollator.setStrength(Collator.PRIMARY); 159 mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION); 160 } 161 162 /** 163 * Returns true if {@param query} is a prefix of {@param target} 164 */ 165 public boolean matches(String query, String target) { 166 switch (mCollator.compare(query, target)) { 167 case 0: 168 return true; 169 case -1: 170 // The target string can contain a modifier which would make it larger than 171 // the query string (even though the length is same). If the query becomes 172 // larger after appending a unicode character, it was originally a prefix of 173 // the target string and hence should match. 174 return mCollator.compare(query + MAX_UNICODE, target) > -1; 175 default: 176 return false; 177 } 178 } 179 180 public static StringMatcher getInstance() { 181 return new StringMatcher(); 182 } 183 } 184 } 185