Merge "Using DragObject for folder drop instead of maintaining states when drag starts from inside a folder" 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/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index bf4551b..f7737f4 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -19,6 +19,7 @@
 import android.util.AttributeSet;
 import android.view.DragEvent;
 import android.view.KeyEvent;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 
 
@@ -27,6 +28,8 @@
  */
 public class ExtendedEditText extends EditText {
 
+    private boolean mShowImeAfterFirstLayout;
+
     /**
      * Implemented by listeners of the back key.
      */
@@ -37,11 +40,11 @@
     private OnBackKeyListener mBackKeyListener;
 
     public ExtendedEditText(Context context) {
-        super(context);
+        this(context, null, 0);
     }
 
     public ExtendedEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
+        this(context, attrs, 0);
     }
 
     public ExtendedEditText(Context context, AttributeSet attrs, int defStyleAttr) {
@@ -69,4 +72,29 @@
         // We don't want this view to interfere with Launcher own drag and drop.
         return false;
     }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mShowImeAfterFirstLayout) {
+            // soft input only shows one frame after the layout of the EditText happens,
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    showSoftInput();
+                    mShowImeAfterFirstLayout = false;
+                }
+            });
+        }
+    }
+
+    public void showKeyboard() {
+        mShowImeAfterFirstLayout = !showSoftInput();
+    }
+
+    private boolean showSoftInput() {
+        return requestFocus() &&
+                ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE))
+                    .showSoftInput(this, InputMethodManager.SHOW_FORCED);
+    }
 }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index a49162c..d3fb38e 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -853,8 +853,7 @@
         values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(lowResIcon));
 
         values.put(IconDB.COLUMN_LABEL, label);
-        values.put(IconDB.COLUMN_SYSTEM_STATE,
-                mIconProvider.getIconSystemState(mIconProvider.getIconSystemState(packageName)));
+        values.put(IconDB.COLUMN_SYSTEM_STATE, mIconProvider.getIconSystemState(packageName));
 
         return values;
     }
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index df87cc2..d8e58d8 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
 
 import org.json.JSONException;
@@ -146,6 +147,15 @@
         }
         PendingInstallShortcutInfo info = createPendingInfo(context, data);
         if (info != null) {
+            if (!info.isLauncherActivity()) {
+                // Since its a custom shortcut, verify that it is safe to launch.
+                if (!PackageManagerHelper.hasPermissionForActivity(
+                        context, info.launchIntent, null)) {
+                    // Target cannot be launched, or requires some special permission to launch
+                    Log.e(TAG, "Ignoring malicious intent " + info.launchIntent.toUri(0));
+                    return;
+                }
+            }
             queuePendingShortcutInfo(info, context);
         }
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 901900e..9ac075d 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -652,7 +652,7 @@
 
         switch (requestCode) {
             case REQUEST_CREATE_SHORTCUT:
-                completeAddShortcut(intent, info.container, screenId, info.cellX, info.cellY);
+                completeAddShortcut(intent, info.container, screenId, info.cellX, info.cellY, info);
                 break;
             case REQUEST_CREATE_APPWIDGET:
                 completeAddAppWidget(appWidgetId, info, null, null);
@@ -951,7 +951,7 @@
             // view after launching an app, as they may be depending on the UI to be static to
             // switch to another app, otherwise, if it was
             showAppsView(false /* animated */, !launchedFromApp /* updatePredictedApps */,
-                    false /* focusSearchBar */);
+                    mAppsView.shouldRestoreImeState() /* focusSearchBar */);
         } else if (mOnResumeState == State.WIDGETS) {
             showWidgetsView(false, false);
         }
@@ -1456,12 +1456,19 @@
      * @param data The intent describing the shortcut.
      */
     private void completeAddShortcut(Intent data, long container, long screenId, int cellX,
-            int cellY) {
+            int cellY, PendingRequestArgs args) {
         int[] cellXY = mTmpAddItemCellCoordinates;
         CellLayout layout = getCellLayout(container, screenId);
 
         ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(this, data);
-        if (info == null) {
+        if (info == null || args.getRequestCode() != REQUEST_CREATE_SHORTCUT ||
+                args.getPendingIntent().getComponent() == null) {
+            return;
+        }
+        if (!PackageManagerHelper.hasPermissionForActivity(
+                this, info.intent, args.getPendingIntent().getComponent().getPackageName())) {
+            // The app is trying to add a shortcut without sufficient permissions
+            Log.e(TAG, "Ignoring malicious intent " + info.intent.toUri(0));
             return;
         }
         final View view = createShortcut(info);
@@ -2178,10 +2185,9 @@
      * Process a shortcut drop.
      */
     private void processShortcutFromDrop(PendingAddItemInfo info) {
-        setWaitingForResult(new PendingRequestArgs(info));
-        Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
-        createShortcutIntent.setComponent(info.componentName);
-        Utilities.startActivityForResultSafely(this, createShortcutIntent, REQUEST_CREATE_SHORTCUT);
+        Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setComponent(info.componentName);
+        setWaitingForResult(PendingRequestArgs.forIntent(REQUEST_CREATE_SHORTCUT, intent, info));
+        Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_SHORTCUT);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index a3786fa..0a71a69 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -25,6 +25,7 @@
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
@@ -52,7 +53,6 @@
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ComponentKey;
 
@@ -708,4 +708,8 @@
     public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) {
         targetParent.containerType = mAppsRecyclerView.getContainerType(v);
     }
+
+    public boolean shouldRestoreImeState() {
+        return !TextUtils.isEmpty(mSearchInput.getText());
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
index 9a48367..41abb4c 100644
--- a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
@@ -163,8 +163,7 @@
      * Focuses the search field to handle key events.
      */
     public void focusSearchField() {
-        mInput.requestFocus();
-        mInputMethodManager.showSoftInput(mInput, InputMethodManager.SHOW_IMPLICIT);
+        mInput.showKeyboard();
     }
 
     /**
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/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 3c4c79a..3e15d05 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,10 +16,15 @@
 
 package com.android.launcher3.util;
 
+import android.app.AppOpsManager;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.text.TextUtils;
 
 import com.android.launcher3.Utilities;
 
@@ -96,4 +101,53 @@
         }
         return excludePackages.get(0);
     }
+
+    /**
+     * Returns true if {@param srcPackage} has the permission required to start the activity from
+     * {@param intent}. If {@param srcPackage} is null, then the activity should not need
+     * any permissions
+     */
+    public static boolean hasPermissionForActivity(Context context, Intent intent,
+            String srcPackage) {
+        PackageManager pm = context.getPackageManager();
+        ResolveInfo target = pm.resolveActivity(intent, 0);
+        if (target == null) {
+            // Not a valid target
+            return false;
+        }
+        if (TextUtils.isEmpty(target.activityInfo.permission)) {
+            // No permission is needed
+            return true;
+        }
+        if (TextUtils.isEmpty(srcPackage)) {
+            // The activity requires some permission but there is no source.
+            return false;
+        }
+
+        // Source does not have sufficient permissions.
+        if(pm.checkPermission(target.activityInfo.permission, srcPackage) !=
+                PackageManager.PERMISSION_GRANTED) {
+            return false;
+        }
+
+        if (!Utilities.ATLEAST_MARSHMALLOW) {
+            // These checks are sufficient for below M devices.
+            return true;
+        }
+
+        // On M and above also check AppOpsManager for compatibility mode permissions.
+        if (TextUtils.isEmpty(AppOpsManager.permissionToOp(target.activityInfo.permission))) {
+            // There is no app-op for this permission, which could have been disabled.
+            return true;
+        }
+
+        // There is no direct way to check if the app-op is allowed for a particular app. Since
+        // app-op is only enabled for apps running in compatibility mode, simply block such apps.
+
+        try {
+            return pm.getApplicationInfo(srcPackage, 0).targetSdkVersion >= Build.VERSION_CODES.M;
+        } catch (NameNotFoundException e) { }
+
+        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;
+    }
+}