Merge "Fixing double system state call in iconCache" into ub-launcher3-calgary-polish
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index 604b164..4c4d67c 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -62,7 +62,7 @@
      */
     int isDisabled = ShortcutInfo.DEFAULT;
 
-    AppInfo() {
+    public AppInfo() {
         itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
     }
 
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
index 10740ec..ac22dd2 100644
--- a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
@@ -22,15 +22,12 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Pattern;
 
 /**
  * The default search implementation.
  */
 public class DefaultAppSearchAlgorithm {
 
-    private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
-
     private final List<AppInfo> mApps;
     protected final Handler mResultHandler;
 
@@ -61,34 +58,79 @@
         // Do an intersection of the words in the query and each title, and filter out all the
         // apps that don't match all of the words in the query.
         final String queryTextLower = query.toLowerCase();
-        final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
-
         final ArrayList<ComponentKey> result = new ArrayList<>();
         for (AppInfo info : mApps) {
-            if (matches(info, queryWords)) {
+            if (matches(info, queryTextLower)) {
                 result.add(info.toComponentKey());
             }
         }
         return result;
     }
 
-    protected boolean matches(AppInfo info, String[] queryWords) {
+    protected boolean matches(AppInfo info, String query) {
+        int queryLength = query.length();
+
         String title = info.title.toString();
-        String[] words = SPLIT_PATTERN.split(title.toLowerCase());
-        for (int qi = 0; qi < queryWords.length; qi++) {
-            boolean foundMatch = false;
-            for (int i = 0; i < words.length; i++) {
-                if (words[i].startsWith(queryWords[qi])) {
-                    foundMatch = true;
-                    break;
-                }
-            }
-            if (!foundMatch) {
-                // If there is a word in the query that does not match any words in this
-                // title, so skip it.
-                return false;
+        int titleLength = title.length();
+
+        if (titleLength < queryLength || queryLength <= 0) {
+            return false;
+        }
+
+        int lastType;
+        int thisType = Character.UNASSIGNED;
+        int nextType = Character.getType(title.codePointAt(0));
+
+        int end = titleLength - queryLength;
+        for (int i = 0; i <= end; i++) {
+            lastType = thisType;
+            thisType = nextType;
+            nextType = i < (titleLength - 1) ?
+                    Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
+            if (isBreak(thisType, lastType, nextType) &&
+                    title.substring(i, i + queryLength).equalsIgnoreCase(query)) {
+                return true;
             }
         }
-        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
+     */
+    protected boolean isBreak(int thisType, int prevType, int nextType) {
+        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;
+            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;
+        }
     }
 }
diff --git a/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java
new file mode 100644
index 0000000..4d0a7a9
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 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.allapps;
+
+import android.content.ComponentName;
+import android.test.InstrumentationTestCase;
+
+import com.android.launcher3.AppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link DefaultAppSearchAlgorithm}
+ */
+public class DefaultAppSearchAlgorithmTest extends InstrumentationTestCase {
+
+    private List<AppInfo> mAppsList;
+    private DefaultAppSearchAlgorithm mAlgorithm;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mAppsList = new ArrayList<>();
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mAlgorithm = new DefaultAppSearchAlgorithm(mAppsList);
+            }
+        });
+    }
+
+    public void testMatches() {
+        assertTrue(mAlgorithm.matches(getInfo("white cow"), "cow"));
+        assertTrue(mAlgorithm.matches(getInfo("whiteCow"), "cow"));
+        assertTrue(mAlgorithm.matches(getInfo("whiteCOW"), "cow"));
+        assertTrue(mAlgorithm.matches(getInfo("whitecowCOW"), "cow"));
+        assertTrue(mAlgorithm.matches(getInfo("white2cow"), "cow"));
+
+        assertFalse(mAlgorithm.matches(getInfo("whitecow"), "cow"));
+        assertFalse(mAlgorithm.matches(getInfo("whitEcow"), "cow"));
+
+        assertTrue(mAlgorithm.matches(getInfo("whitecowCow"), "cow"));
+        assertTrue(mAlgorithm.matches(getInfo("whitecow cow"), "cow"));
+        assertFalse(mAlgorithm.matches(getInfo("whitecowcow"), "cow"));
+        assertFalse(mAlgorithm.matches(getInfo("whit ecowcow"), "cow"));
+
+        assertTrue(mAlgorithm.matches(getInfo("cats&dogs"), "dog"));
+        assertTrue(mAlgorithm.matches(getInfo("cats&Dogs"), "dog"));
+        assertTrue(mAlgorithm.matches(getInfo("cats&Dogs"), "&"));
+
+        assertTrue(mAlgorithm.matches(getInfo("2+43"), "43"));
+        assertFalse(mAlgorithm.matches(getInfo("2+43"), "3"));
+
+        assertTrue(mAlgorithm.matches(getInfo("Q"), "q"));
+        assertTrue(mAlgorithm.matches(getInfo("  Q"), "q"));
+    }
+
+    private AppInfo getInfo(String title) {
+        AppInfo info = new AppInfo();
+        info.title = title;
+        info.componentName = new ComponentName("Test", title);
+        return info;
+    }
+}