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.model.data.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 
DefaultAppSearchAlgorithm(List<AppInfo> apps)35     public DefaultAppSearchAlgorithm(List<AppInfo> apps) {
36         mApps = apps;
37         mResultHandler = new Handler();
38     }
39 
40     @Override
cancel(boolean interruptActiveRequests)41     public void cancel(boolean interruptActiveRequests) {
42         if (interruptActiveRequests) {
43             mResultHandler.removeCallbacksAndMessages(null);
44         }
45     }
46 
47     @Override
doSearch(final String query, final AllAppsSearchBarController.Callbacks callback)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 
getTitleMatchResult(String query)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 
matches(AppInfo info, String query, StringMatcher matcher)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 
StringMatcher()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          */
matches(String query, String target)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 
getInstance()180         public static StringMatcher getInstance() {
181             return new StringMatcher();
182         }
183     }
184 }
185