Home | History | Annotate | Download | only in search
      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