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;
+ }
+}