diff options
40 files changed, 801 insertions, 206 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b61b90c5c..fce469114 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -83,6 +83,7 @@ android:theme="@style/Theme" android:windowSoftInputMode="adjustPan" android:screenOrientation="nosensor" + android:resumeWhilePausing="true" android:enabled="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -148,6 +149,12 @@ </intent-filter> </activity> + <activity + android:name="com.android.launcher3.SettingsActivity" + android:label="@string/settings_button_text" + android:autoRemoveFromRecents="true"> + </activity> + <!-- Debugging tools --> <activity android:name="com.android.launcher3.MemoryDumpActivity" diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java index 96238717e..94159416a 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java @@ -1142,6 +1142,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { @Override public boolean enableRotation() { - return Utilities.isRotationEnabled(getContext()); + // Check if rotation is enabled for this device. + if (Utilities.isRotationAllowedForDevice(getContext())) + return true; + + // Check if the user has specifically enabled rotation via preferences. + return Utilities.isAllowRotationPrefEnabled(getApplicationContext()); } } diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml index 67b69cabf..ecf7def48 100644 --- a/res/layout/user_folder.xml +++ b/res/layout/user_folder.xml @@ -40,7 +40,7 @@ android:layout_height="match_parent" android:paddingLeft="4dp" android:paddingRight="4dp" - android:paddingTop="4dp" + android:paddingTop="8dp" launcher:pageIndicator="@+id/folder_page_indicator" /> </FrameLayout> @@ -48,6 +48,7 @@ android:id="@+id/folder_footer" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clipChildren="false" android:orientation="horizontal" android:paddingLeft="8dp" android:paddingRight="8dp" > @@ -63,8 +64,8 @@ android:gravity="center_horizontal" android:hint="@string/folder_hint_text" android:imeOptions="flagNoExtractUi" - android:paddingBottom="@dimen/folder_name_padding" - android:paddingTop="@dimen/folder_name_padding" + android:paddingBottom="8dp" + android:paddingTop="4dp" android:singleLine="true" android:textColor="#ff777777" android:textColorHighlight="#ffCCCCCC" @@ -78,6 +79,7 @@ android:layout_height="12dp" android:layout_gravity="center_vertical" layout="@layout/page_indicator" /> + </LinearLayout> </com.android.launcher3.Folder>
\ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 246adcdad..7950862db 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -114,7 +114,6 @@ <!-- Folders --> <!-- The amount that the preview contents are inset from the preview background --> <dimen name="folder_preview_padding">4dp</dimen> - <dimen name="folder_name_padding">10dp</dimen> <!-- Sizes for managed profile badges --> <dimen name="profile_badge_size">24dp</dimen> diff --git a/res/values/strings.xml b/res/values/strings.xml index a8c668d61..440a5378d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -166,6 +166,12 @@ <!-- Text for settings button --> <string name="settings_button_text">Settings</string> + <!-- Strings for settings --> + <!-- Title for Allow Rotation setting. [CHAR LIMIT=50] [DO NOT TRANSLATE] --> + <string name="allow_rotation_title">Allow rotation</string> + <!-- Summary for Allow Rotation setting. [CHAR LIMIT=150] [DO NOT TRANSLATE] --> + <string name="allow_rotation_summary">Allow rotation of the home screen</string> + <!-- Label on an icon that references an uninstalled package, for which we have no information about when it might be installed. [CHAR_LIMIT=15] --> <string name="package_state_unknown">Unknown</string> diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml new file mode 100644 index 000000000..f283575f0 --- /dev/null +++ b/res/xml/launcher_preferences.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 Google Inc. + + 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. +--> + +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + + <SwitchPreference + android:key="pref_allowRotation" + android:title="@string/allow_rotation_title" + android:summary="@string/allow_rotation_summary" + android:defaultValue="@bool/allow_rotation" + android:persistent="true" + /> + +</PreferenceScreen> diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 798eec8e7..314c21f64 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -28,7 +28,6 @@ import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.KeyEvent; @@ -36,7 +35,6 @@ import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewParent; import android.widget.TextView; - import com.android.launcher3.IconCache.IconLoadRequest; import com.android.launcher3.model.PackageItemInfo; diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 4cd28c034..09a71b0cc 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -64,7 +64,7 @@ public abstract class ButtonDropTarget extends TextView protected Drawable mDrawable; private AnimatorSet mCurrentColorAnim; - private ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter; + @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter; public ButtonDropTarget(Context context, AttributeSet attrs) { diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index a57c1fe98..b5d0dca24 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -131,10 +131,8 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { private final ClickShadowView mTouchFeedbackView; - @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new - HashMap<CellLayout.LayoutParams, Animator>(); - private HashMap<View, ReorderPreviewAnimation> - mShakeAnimators = new HashMap<View, ReorderPreviewAnimation>(); + @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new HashMap<>(); + @Thunk HashMap<View, ReorderPreviewAnimation> mShakeAnimators = new HashMap<>(); private boolean mItemPlacementDirty = false; diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java index 31641799d..5314ecff1 100644 --- a/src/com/android/launcher3/CommonAppTypeParser.java +++ b/src/com/android/launcher3/CommonAppTypeParser.java @@ -26,6 +26,7 @@ import android.util.Log; import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.backup.BackupProtos.Favorite; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParserException; @@ -44,8 +45,8 @@ public class CommonAppTypeParser implements LayoutParserCallback { private final long mItemId; - private final int mResId; - private final Context mContext; + @Thunk final int mResId; + @Thunk final Context mContext; ContentValues parsedValues; Intent parsedIntent; diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index b3323384d..dfa8202a7 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -44,7 +44,7 @@ public class DragView extends View { private Bitmap mBitmap; private Bitmap mCrossFadeBitmap; - private Paint mPaint; + @Thunk Paint mPaint; private int mRegistrationX; private int mRegistrationY; @@ -62,7 +62,7 @@ public class DragView extends View { // size. This is ignored for non-icons. private float mIntrinsicIconScale = 1f; - private float[] mCurrentFilter; + @Thunk float[] mCurrentFilter; private ValueAnimator mFilterAnimator; /** diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index 46e4902f9..70bb01af0 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -426,7 +426,7 @@ public class FocusHelper { /** * Private helper method to get the CellLayoutChildren given a CellLayout index. */ - private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( ViewGroup container, int i) { CellLayout parent = (CellLayout) container.getChildAt(i); return parent.getShortcutsAndWidgets(); diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index ec4ea044c..94f8fc875 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -29,6 +29,7 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; import android.text.InputType; import android.text.Selection; import android.text.Spannable; @@ -44,6 +45,7 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; @@ -65,7 +67,8 @@ import java.util.Collections; */ public class Folder extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, - View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource { + View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource, + Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Folder"; /** @@ -89,7 +92,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList */ private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; - public static final int FOOTER_ANIMATION_DURATION = 200; + private static final int FOLDER_NAME_ANIMATION_DURATION = 633; private static final int REORDER_DELAY = 250; private static final int ON_EXIT_CLOSE_DELAY = 400; @@ -502,7 +505,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - anim.play(drift); anim.play(iconsAlpha); anim.play(textAlpha); @@ -545,7 +547,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList - mFooter.getPaddingLeft() - mFooter.getPaddingRight(); float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString()); - mFolderName.setTranslationX((footerWidth - textWidth) / 2); + float translation = (footerWidth - textWidth) / 2; + mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation); mContent.setMarkerScale(0); // Do not update the flag if we are in drag mode. The flag will be updated, when we @@ -555,7 +558,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void onAnimationEnd(Animator animation) { - mFolderName.animate().setDuration(FOOTER_ANIMATION_DURATION).translationX(0); + mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION) + .translationX(0) + .setInterpolator(Utilities.isLmpOrAbove() ? + AnimationUtils.loadInterpolator(mLauncher, + android.R.interpolator.fast_out_slow_in) + : new LogDecelerateInterpolator(100, 0)); mContent.animateMarkers(); if (updateAnimationFlag) { @@ -917,7 +925,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList View v = list.get(i); ItemInfo info = (ItemInfo) v.getTag(); LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); + info.cellX, info.cellY); } } @@ -1032,6 +1040,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mContent.setFixedSize(contentWidth, contentHeight); mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec); + + if (mContent.getChildCount() > 0) { + int cellIconGap = (mContent.getPageAt(0).getCellWidth() + - mLauncher.getDeviceProfile().iconSizePx) / 2; + mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap, + mFooter.getPaddingTop(), + mContent.getPaddingRight() + cellIconGap, + mFooter.getPaddingBottom()); + } mFooter.measure(contentAreaWidthSpec, MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); @@ -1323,6 +1340,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList outRect.right += mScrollAreaOffset; } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + // Fill in from the folder icon's launch source provider first + Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData); + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER); + sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage()); + } + private class OnScrollHintListener implements OnAlarmListener { private final DragObject mDragObject; diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index 0bd6501ed..7d90ba2f7 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -24,6 +24,7 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; @@ -46,7 +47,12 @@ public class FolderPagedView extends PagedView { private static final int START_VIEW_REORDER_DELAY = 30; private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f; - private static final int PAGE_INDICATOR_ANIMATION_DELAY = 150; + private static final int PAGE_INDICATOR_ANIMATION_START_DELAY = 300; + private static final int PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY = 150; + private static final int PAGE_INDICATOR_ANIMATION_DURATION = 400; + + // This value approximately overshoots to 1.5 times the original size. + private static final float PAGE_INDICATOR_OVERSHOOT_TENSION = 4.9f; /** * Fraction of the width to scroll when showing the next page hint. @@ -274,6 +280,7 @@ public class FolderPagedView extends PagedView { arrangeChildren(list, itemCount, true); } + @SuppressLint("RtlHardcoded") private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) { ArrayList<CellLayout> pages = new ArrayList<CellLayout>(); for (int i = 0; i < getChildCount(); i++) { @@ -340,7 +347,9 @@ public class FolderPagedView extends PagedView { // Update footer mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); - mFolder.mFolderName.setGravity(getPageCount() > 1 ? Gravity.START : Gravity.CENTER_HORIZONTAL); + // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text. + mFolder.mFolderName.setGravity(getPageCount() > 1 ? + (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); } public int getDesiredWidth() { @@ -645,12 +654,13 @@ public class FolderPagedView extends PagedView { public void animateMarkers() { int count = mPageIndicator.getChildCount(); - OvershootInterpolator interpolator = new OvershootInterpolator(4); + Interpolator interpolator = new OvershootInterpolator(PAGE_INDICATOR_OVERSHOOT_TENSION); for (int i = 0; i < count; i++) { mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1) .setInterpolator(interpolator) - .setDuration(Folder.FOOTER_ANIMATION_DURATION) - .setStartDelay(PAGE_INDICATOR_ANIMATION_DELAY * i); + .setDuration(PAGE_INDICATOR_ANIMATION_DURATION) + .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i + + PAGE_INDICATOR_ANIMATION_START_DELAY); } } diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index ce33164fa..6f097449c 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -19,15 +19,16 @@ package com.android.launcher3; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.TextView; -public class Hotseat extends FrameLayout { +public class Hotseat extends FrameLayout + implements Stats.LaunchSourceProvider{ private CellLayout mContent; @@ -160,4 +161,9 @@ public class Hotseat extends FrameLayout { } return false; } + + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOTSEAT); + } } diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 6dfca9ef3..a16067d16 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -69,7 +69,7 @@ public class IconCache { private static final int LOW_RES_SCALE_FACTOR = 8; - private static final Object ICON_UPDATE_TOKEN = new Object(); + @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); @Thunk static class CacheEntry { public Bitmap icon; @@ -79,18 +79,18 @@ public class IconCache { } private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>(); - private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); + @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); private final Context mContext; private final PackageManager mPackageManager; - private final UserManagerCompat mUserManager; + @Thunk final UserManagerCompat mUserManager; private final LauncherAppsCompat mLauncherApps; private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); private final int mIconDpi; - private final IconDB mIconDb; + @Thunk final IconDB mIconDb; - private final Handler mWorkerHandler; + @Thunk final Handler mWorkerHandler; public IconCache(Context context, InvariantDeviceProfile inv) { ActivityManager activityManager = @@ -320,7 +320,7 @@ public class IconCache { } } - private void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, + @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, long userSerial) { // Reuse the existing entry if it already exists in the DB. This ensures that we do not // create bitmap if it was already created during loader. @@ -342,7 +342,7 @@ public class IconCache { SQLiteDatabase.CONFLICT_REPLACE); } - private ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, + @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, boolean replaceExisting) { final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); CacheEntry entry = null; @@ -688,14 +688,14 @@ public class IconCache { * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the * worker thread doesn't get blocked. */ - private class SerializedIconUpdateTask implements Runnable { + @Thunk class SerializedIconUpdateTask implements Runnable { private final long mUserSerial; private final HashMap<String, PackageInfo> mPkgInfoMap; private final Stack<LauncherActivityInfoCompat> mAppsToAdd; private final Stack<LauncherActivityInfoCompat> mAppsToUpdate; private final HashSet<String> mUpdatedPackages = new HashSet<String>(); - private SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, + @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, Stack<LauncherActivityInfoCompat> appsToAdd, Stack<LauncherActivityInfoCompat> appsToUpdate) { mUserSerial = userSerial; diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 5dac3b3da..867a6e71d 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -100,6 +100,7 @@ import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.PagedView.PageSwitchListener; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.AppSearchManager; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; @@ -218,7 +219,8 @@ public class Launcher extends Activity public static final String USER_HAS_MIGRATED = "launcher.user_migrated_from_old_data"; /** The different states that Launcher can be in. */ - enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED }; + enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED } + @Thunk State mState = State.WORKSPACE; @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation; @@ -263,7 +265,7 @@ public class Launcher extends Activity private int[] mTmpAddItemCellCoordinates = new int[2]; - private Hotseat mHotseat; + @Thunk Hotseat mHotseat; private ViewGroup mOverviewPanel; private View mAllAppsButton; @@ -400,6 +402,24 @@ public class Launcher extends Activity FocusIndicatorView mFocusHandler; + @Thunk boolean mRotationEnabled = false; + private boolean mPreferenceObserverRegistered = false; + + final private SharedPreferences.OnSharedPreferenceChangeListener mSettingsObserver = + new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(key)) { + if (mRotationEnabled = sharedPreferences.getBoolean( + Utilities.ALLOW_ROTATION_PREFERENCE_KEY, false)) { + unlockScreenOrientation(true); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); + } + } + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG_STRICT_MODE) { @@ -499,7 +519,19 @@ public class Launcher extends Activity IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mCloseSystemDialogsReceiver, filter); - // On large interfaces, we want the screen to auto-rotate based on the current orientation + mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext()); + // In case we are on a device with locked rotation, we should look at preferences to check + // if the user has specifically allowed rotation. + if (!mRotationEnabled) { + getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE, + Context.MODE_MULTI_PROCESS).registerOnSharedPreferenceChangeListener( + mSettingsObserver); + mPreferenceObserverRegistered = true; + mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext()); + } + + // On large interfaces, or on devices that a user has specifically enabled screen rotation, + // we want the screen to auto-rotate based on the current orientation unlockScreenOrientation(true); if (mLauncherCallbacks != null) { @@ -552,6 +584,11 @@ public class Launcher extends Activity } } } + + @Override + public void setSearchManager(AppSearchManager manager) { + mAppsView.setSearchManager(manager); + } }); mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() { private boolean mImportanceStored = false; @@ -1127,6 +1164,11 @@ public class Launcher extends Activity * Called to dismiss all apps if it is showing. */ public void dismissAllApps(); + + /** + * Sets the search manager to be used for app search. + */ + public void setSearchManager(AppSearchManager manager); } public interface LauncherSearchCallbacks { @@ -1192,8 +1234,11 @@ public class Launcher extends Activity protected boolean hasSettings() { if (mLauncherCallbacks != null) { return mLauncherCallbacks.hasSettings(); + } else { + // On devices with a locked orientation, we will at least have the allow rotation + // setting. + return !Utilities.isRotationAllowedForDevice(this); } - return false; } public void addToCustomContentPage(View customContent, @@ -1486,7 +1531,6 @@ public class Launcher extends Activity * Add a shortcut to the workspace. * * @param data The intent describing the shortcut. - * @param cellInfo The position on screen where to create the shortcut. */ private void completeAddShortcut(Intent data, long container, long screenId, int cellX, int cellY) { @@ -1975,6 +2019,13 @@ public class Launcher extends Activity public void onDestroy() { super.onDestroy(); + if (mPreferenceObserverRegistered) { + getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE, + Context.MODE_MULTI_PROCESS).unregisterOnSharedPreferenceChangeListener( + mSettingsObserver); + mPreferenceObserverRegistered = false; + } + // Remove all pending runnables mHandler.removeMessages(ADVANCE_MSG); mHandler.removeMessages(0); @@ -2497,13 +2548,6 @@ public class Launcher extends Activity } } - public void onClickPagedViewIcon(View v) { - startAppShortcutOrInfoActivity(v); - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onClickPagedViewIcon(v); - } - } - @SuppressLint("ClickableViewAccessibility") public boolean onTouch(View v, MotionEvent event) { return false; @@ -2662,7 +2706,7 @@ public class Launcher extends Activity } boolean success = startActivitySafely(v, intent, tag); - mStats.recordLaunch(intent, shortcut); + mStats.recordLaunch(v, intent, shortcut); if (success && v instanceof BubbleTextView) { mWaitingForResume = (BubbleTextView) v; @@ -2757,6 +2801,8 @@ public class Launcher extends Activity if (LOGD) Log.d(TAG, "onClickSettingsButton"); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickSettingsButton(v); + } else { + showSettingsActivity(); } } @@ -2882,7 +2928,7 @@ public class Launcher extends Activity } } - boolean startActivity(View v, Intent intent, Object tag) { + private boolean startActivity(View v, Intent intent, Object tag) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { // Only launch using the new animation if the shortcut has not opted out (this is a @@ -2953,7 +2999,7 @@ public class Launcher extends Activity return false; } - boolean startActivitySafely(View v, Intent intent, Object tag) { + private boolean startActivitySafely(View v, Intent intent, Object tag) { boolean success = false; if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); @@ -4364,7 +4410,7 @@ public class Launcher extends Activity } public void lockScreenOrientation() { - if (Utilities.isRotationEnabled(this)) { + if (mRotationEnabled) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() .getConfiguration().orientation)); @@ -4373,8 +4419,9 @@ public class Launcher extends Activity } } } + public void unlockScreenOrientation(boolean immediate) { - if (Utilities.isRotationEnabled(this)) { + if (mRotationEnabled) { if (immediate) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } else { @@ -4479,6 +4526,10 @@ public class Launcher extends Activity editor.apply(); } + private void showSettingsActivity() { + startActivity(new Intent(this, SettingsActivity.class)); + } + /** * To be overridden by subclasses to indicate that there is an in-activity full-screen intro * screen that must be displayed and dismissed. diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index af4101221..b40ace3fb 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -51,13 +51,13 @@ import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -135,7 +135,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final int SCREEN_RANK_INDEX = 2; - private final Context mContext; + @Thunk final Context mContext; private final HashSet<String> mExistingKeys; private final ArrayList<Key> mKeys; private final ItemTypeMatcher[] mItemTypeMatchers; @@ -1157,15 +1157,15 @@ public class LauncherBackupHelper implements BackupHelper { .getSerialNumberForUser(UserHandleCompat.myUserHandle()); } - private class InvalidBackupException extends IOException { + @Thunk class InvalidBackupException extends IOException { private static final long serialVersionUID = 8931456637211665082L; - private InvalidBackupException(Throwable cause) { + @Thunk InvalidBackupException(Throwable cause) { super(cause); } - public InvalidBackupException(String reason) { + @Thunk InvalidBackupException(String reason) { super(reason); } } diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index a5f36ba93..70e400bca 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -56,6 +56,8 @@ public interface LauncherCallbacks { public void bindAllApplications(ArrayList<AppInfo> apps); public void onClickFolderIcon(View v); public void onClickAppShortcut(View v); + + @Deprecated public void onClickPagedViewIcon(View v); public void onClickWallpaperPicker(View v); public void onClickSettingsButton(View v); diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index c08cd0bf5..ec4e4f942 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -26,6 +26,8 @@ public class LauncherFiles { public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db"; public static final String APP_ICONS_DB = "app_icons.db"; + public static final String ROTATION_PREF_FILE = "com.android.launcher3.rotation.prefs"; + public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList( DEFAULT_WALLPAPER_THUMBNAIL, DEFAULT_WALLPAPER_THUMBNAIL_OLD, @@ -35,7 +37,8 @@ public class LauncherFiles { WALLPAPER_IMAGES_DB, WIDGET_PREVIEWS_DB, MANAGED_USER_PREFERENCES_KEY, - APP_ICONS_DB)); + APP_ICONS_DB, + ROTATION_PREF_FILE)); // TODO: Delete these files on upgrade public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList( diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 224ebbf89..776c2bd63 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -275,7 +275,7 @@ public class LauncherModel extends BroadcastReceiver /** * Runs the specified runnable after the loader is complete */ - private void runAfterBindCompletes(Runnable r) { + @Thunk void runAfterBindCompletes(Runnable r) { if (isLoadingWorkspace() || !mHasLoaderCompletedOnce) { synchronized (mBindCompleteRunnables) { mBindCompleteRunnables.add(r); @@ -3350,7 +3350,7 @@ public class LauncherModel extends BroadcastReceiver * * @see #loadAndBindWidgetsAndShortcuts */ - private WidgetsModel createWidgetsModel(Context context, boolean refresh) { + @Thunk WidgetsModel createWidgetsModel(Context context, boolean refresh) { PackageManager packageManager = context.getPackageManager(); final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>(); widgetsAndShortcuts.addAll(getWidgetProviders(context, refresh)); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 27511527d..45070d190 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -88,7 +88,7 @@ public class LauncherProvider extends ContentProvider { static final Uri CONTENT_APPWIDGET_RESET_URI = Uri.parse("content://" + AUTHORITY + "/appWidgetReset"); - private DatabaseHelper mOpenHelper; + @Thunk DatabaseHelper mOpenHelper; @Override public boolean onCreate() { @@ -665,7 +665,7 @@ public class LauncherProvider extends ContentProvider { * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. */ - private void convertShortcutsToLauncherActivities(SQLiteDatabase db) { + @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) { db.beginTransaction(); Cursor c = null; SQLiteStatement updateStmt = null; diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 9271e8b15..18832c680 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -22,11 +22,13 @@ import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -45,9 +47,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; - import com.android.launcher3.util.Thunk; - import java.util.ArrayList; /** @@ -186,7 +186,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // We use the min scale to determine how much to expand the actually PagedView measured // dimensions such that when we are zoomed out, the view is not clipped private static int REORDERING_DROP_REPOSITION_DURATION = 200; - private static int REORDERING_REORDER_REPOSITION_DURATION = 300; + @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300; private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; private float mMinScale = 1f; @@ -956,7 +956,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return 0; } - private void updateMaxScrollX() { + @Thunk void updateMaxScrollX() { int childCount = getChildCount(); if (childCount > 0) { final int index = mIsRtl ? 0 : childCount - 1; @@ -2322,6 +2322,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private static final int ANIM_TAG_KEY = 100; /* Accessibility */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java new file mode 100644 index 000000000..a1da1b6fd --- /dev/null +++ b/src/com/android/launcher3/SettingsActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 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; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +/** + * Settings activity for Launcher. Currently implements the following setting: + * LockToPortrait + */ +public class SettingsActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new LauncherSettingsFragment()) + .commit(); + } + + /** + * This fragment shows the launcher preferences. + */ + @SuppressWarnings("WeakerAccess") + public static class LauncherSettingsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getPreferenceManager().setSharedPreferencesMode(Context.MODE_PRIVATE); + getPreferenceManager().setSharedPreferencesName(LauncherFiles.ROTATION_PREF_FILE); + addPreferencesFromResource(R.xml.launcher_preferences); + } + } +} diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index 9d06f755f..cb0e252b2 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -20,9 +20,63 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Bundle; import android.util.Log; +import android.view.View; +import android.view.ViewParent; public class Stats { + + /** + * Implemented by containers to provide a launch source for a given child. + */ + public interface LaunchSourceProvider { + void fillInLaunchSourceData(Bundle sourceData); + } + + /** + * Helpers to add the source to a launch intent. + */ + public static class LaunchSourceUtils { + /** + * Create a default bundle for LaunchSourceProviders to fill in their data. + */ + public static Bundle createSourceData() { + Bundle sourceData = new Bundle(); + sourceData.putString(SOURCE_EXTRA_CONTAINER, CONTAINER_HOMESCREEN); + // Have default container/sub container pages + sourceData.putInt(SOURCE_EXTRA_CONTAINER_PAGE, 0); + sourceData.putInt(SOURCE_EXTRA_SUB_CONTAINER_PAGE, 0); + return sourceData; + } + + /** + * Finds the next launch source provider in the parents of the view hierarchy and populates + * the source data from that provider. + */ + public static void populateSourceDataFromAncestorProvider(View v, Bundle sourceData) { + if (v == null) { + return; + } + + Stats.LaunchSourceProvider provider = null; + ViewParent parent = v.getParent(); + while (parent != null && parent instanceof View) { + if (parent instanceof Stats.LaunchSourceProvider) { + provider = (Stats.LaunchSourceProvider) parent; + break; + } + parent = parent.getParent(); + } + + if (provider != null) { + provider.fillInLaunchSourceData(sourceData); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new RuntimeException("Expected LaunchSourceProvider"); + } + } + } + private static final boolean DEBUG_BROADCASTS = false; public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH"; @@ -31,6 +85,22 @@ public class Stats { public static final String EXTRA_SCREEN = "screen"; public static final String EXTRA_CELLX = "cellX"; public static final String EXTRA_CELLY = "cellY"; + public static final String EXTRA_SOURCE = "source"; + + public static final String SOURCE_EXTRA_CONTAINER = "container"; + public static final String SOURCE_EXTRA_CONTAINER_PAGE = "container_page"; + public static final String SOURCE_EXTRA_SUB_CONTAINER = "sub_container"; + public static final String SOURCE_EXTRA_SUB_CONTAINER_PAGE = "sub_container_page"; + + public static final String CONTAINER_SEARCH_BOX = "search_box"; + public static final String CONTAINER_ALL_APPS = "all_apps"; + public static final String CONTAINER_HOMESCREEN = "homescreen"; // aka. Workspace + public static final String CONTAINER_HOTSEAT = "hotseat"; + + public static final String SUB_CONTAINER_FOLDER = "folder"; + public static final String SUB_CONTAINER_ALL_APPS_A_Z = "a-z"; + public static final String SUB_CONTAINER_ALL_APPS_PREDICTION = "prediction"; + public static final String SUB_CONTAINER_ALL_APPS_SEARCH = "search"; private final Launcher mLauncher; private final String mLaunchBroadcastPermission; @@ -56,11 +126,7 @@ public class Stats { } } - public void recordLaunch(Intent intent) { - recordLaunch(intent, null); - } - - public void recordLaunch(Intent intent, ShortcutInfo shortcut) { + public void recordLaunch(View v, Intent intent, ShortcutInfo shortcut) { intent = new Intent(intent); intent.setSourceBounds(null); @@ -72,6 +138,10 @@ public class Stats { .putExtra(EXTRA_CELLX, shortcut.cellX) .putExtra(EXTRA_CELLY, shortcut.cellY); } + + Bundle sourceExtras = LaunchSourceUtils.createSourceData(); + LaunchSourceUtils.populateSourceDataFromAncestorProvider(v, sourceExtras); + broadcastIntent.putExtra(EXTRA_SOURCE, sourceExtras); mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); } } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 256eba020..a9cbf6970 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -25,6 +25,7 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -67,6 +68,7 @@ import java.util.regex.Pattern; * Various utilities shared amongst the Launcher's classes. */ public final class Utilities { + private static final String TAG = "Launcher.Utilities"; private static int sIconWidth = -1; @@ -93,6 +95,8 @@ public final class Utilities { static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; + /** * Returns a FastBitmapDrawable with the icon, accurately sized. */ @@ -114,10 +118,15 @@ public final class Utilities { return Log.isLoggable(propertyName, Log.VERBOSE); } - public static boolean isRotationEnabled(Context c) { - boolean enableRotation = sForceEnableRotation || - c.getResources().getBoolean(R.bool.allow_rotation); - return enableRotation; + public static boolean isAllowRotationPrefEnabled(Context context) { + SharedPreferences sharedPrefs = context.getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE, + Context.MODE_MULTI_PROCESS); + boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false); + return sForceEnableRotation || allowRotationPref; + } + + public static boolean isRotationAllowedForDevice(Context context) { + return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); } /** diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index e8cc48685..a62177142 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -56,7 +56,7 @@ public class WidgetPreviewLoader { * Weak reference objects, do not prevent their referents from being made finalizable, * finalized, and then reclaimed. */ - private Set<Bitmap> mUnusedBitmaps = + @Thunk Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); private final Context mContext; @@ -67,7 +67,7 @@ public class WidgetPreviewLoader { private final InvariantDeviceProfile mDeviceProfile; private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); - private final Handler mWorkerHandler; + @Thunk final Handler mWorkerHandler; public WidgetPreviewLoader(Context context, InvariantDeviceProfile inv, IconCache iconCache) { mContext = context; @@ -290,7 +290,7 @@ public class WidgetPreviewLoader { /** * Reads the preview bitmap from the DB or null if the preview is not in the DB. */ - private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { + @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { Cursor cursor = null; try { cursor = mDb.getReadableDatabase().query( @@ -329,7 +329,7 @@ public class WidgetPreviewLoader { return null; } - private Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle, + @Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle, int previewWidth, int previewHeight) { if (info instanceof LauncherAppWidgetProviderInfo) { return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info, @@ -512,7 +512,7 @@ public class WidgetPreviewLoader { /** * @return an array of containing versionCode and lastUpdatedTime for the package. */ - private long[] getPackageVersion(String packageName) { + @Thunk long[] getPackageVersion(String packageName) { synchronized (mPackageVersions) { long[] versions = mPackageVersions.get(packageName); if (versions == null) { @@ -561,14 +561,13 @@ public class WidgetPreviewLoader { } public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> { - - private final WidgetCacheKey mKey; + @Thunk final WidgetCacheKey mKey; private final Object mInfo; private final int mPreviewHeight; private final int mPreviewWidth; private final WidgetCell mCaller; - private long[] mVersions; - private Bitmap mBitmapToRecycle; + @Thunk long[] mVersions; + @Thunk Bitmap mBitmapToRecycle; PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth, int previewHeight, WidgetCell caller) { @@ -674,7 +673,7 @@ public class WidgetPreviewLoader { private static final class WidgetCacheKey extends ComponentKey { // TODO: remove dependency on size - private final String size; + @Thunk final String size; public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) { super(componentName, user); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 6d5affb59..193a0af6f 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -28,6 +28,7 @@ import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.content.res.TypedArray; @@ -42,6 +43,7 @@ import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; @@ -86,7 +88,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class Workspace extends PagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener, - Insettable, UninstallSource, AccessibilityDragSource { + Insettable, UninstallSource, AccessibilityDragSource, Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Workspace"; private static boolean ENFORCE_DRAG_EVENT_ORDER = false; @@ -4461,6 +4463,12 @@ public class Workspace extends PagedView mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOMESCREEN); + sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage()); + } + /** * Used as a workaround to ensure that the AppWidgetService receives the * PACKAGE_ADDED broadcast before updating widgets. diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 93cf8d050..3c49ccc41 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -251,7 +251,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { return actions; } - private void performResizeAction(int action, View host, LauncherAppWidgetInfo info) { + @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams(); CellLayout layout = (CellLayout) host.getParent().getParent(); layout.markCellsAsUnoccupiedForView(host); diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index d81f97f24..9386500be 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.InsetDrawable; import android.os.Build; +import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; @@ -41,6 +42,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import android.widget.TextView; + import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; import com.android.launcher3.BubbleTextView; @@ -54,15 +56,16 @@ import com.android.launcher3.Folder; import com.android.launcher3.Insettable; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherTransitionable; import com.android.launcher3.R; +import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; +import com.android.launcher3.allapps.AppSearchManager.AppSearchResultCallback; import com.android.launcher3.util.Thunk; +import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; /** @@ -171,7 +174,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable, AlphabeticalAppsList.AdapterChangedCallback, AllAppsGridAdapter.PredictionBarSpacerCallbacks, View.OnTouchListener, View.OnClickListener, View.OnLongClickListener, - ViewTreeObserver.OnPreDrawListener { + ViewTreeObserver.OnPreDrawListener, AppSearchResultCallback, Stats.LaunchSourceProvider { public static final boolean GRID_MERGE_SECTIONS = true; @@ -183,8 +186,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private static final int FADE_OUT_DURATION = 100; private static final int SEARCH_TRANSLATION_X_DP = 18; - private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); - @Thunk Launcher mLauncher; @Thunk AlphabeticalAppsList mApps; private LayoutInflater mLayoutInflater; @@ -192,14 +193,14 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private RecyclerView.LayoutManager mLayoutManager; private RecyclerView.ItemDecoration mItemDecoration; - private FrameLayout mContentView; + @Thunk FrameLayout mContentView; @Thunk AllAppsRecyclerView mAppsRecyclerView; - private ViewGroup mPredictionBarView; + @Thunk ViewGroup mPredictionBarView; private View mHeaderView; - private View mSearchBarContainerView; + @Thunk View mSearchBarContainerView; private View mSearchButtonView; private View mDismissSearchButtonView; - private AllAppsSearchEditView mSearchBarEditView; + @Thunk AllAppsSearchEditView mSearchBarEditView; private HeaderElevationController mElevationController; @@ -216,11 +217,13 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private int mContainerInset; private int mPredictionBarHeight; private int mLastRecyclerViewScrollPos = -1; - private boolean mFocusPredictionBarOnFirstBind; + @Thunk boolean mFocusPredictionBarOnFirstBind; private CheckLongPressHelper mPredictionIconCheckForLongPress; private View mPredictionIconUnderTouch; + private AppSearchManager mSearchManager; + public AllAppsContainerView(Context context) { this(context, null); } @@ -231,7 +234,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - LauncherAppState app = LauncherAppState.getInstance(); Resources res = context.getResources(); mLauncher = (Launcher) context; @@ -258,6 +260,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mContentMarginStart = mAdapter.getContentMarginStart(); mApps.setAdapter(mAdapter); + mSearchManager = mApps.newSimpleAppSearchManager(); } /** @@ -281,6 +284,11 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mApps.addApps(apps); } + public void setSearchManager(AppSearchManager searchManager) { + mSearchManager.cancel(true); + mSearchManager = searchManager; + } + /** * Updates existing apps in the list */ @@ -664,45 +672,26 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void afterTextChanged(final Editable s) { String queryText = s.toString(); if (queryText.isEmpty()) { - mApps.setFilter(null); + mSearchManager.cancel(true); + mApps.setOrderedFilter(null); } else { String formatStr = getResources().getString(R.string.all_apps_no_search_results); mAdapter.setEmptySearchText(String.format(formatStr, queryText)); - // 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 = queryText.toLowerCase(); - final String[] queryWords = SPLIT_PATTERN.split(queryTextLower); - mApps.setFilter(new AlphabeticalAppsList.Filter() { - @Override - public boolean retainApp(AppInfo info, String sectionName) { - if (sectionName.toLowerCase().contains(queryTextLower)) { - return true; - } - 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; - } - } - return true; - } - }); + mSearchManager.cancel(false); + mSearchManager.doSearch(queryText, this); } scrollToTop(); } @Override + public void onSearchResult(ArrayList<ComponentName> apps) { + if (apps != null) { + mApps.setOrderedFilter(apps); + } + } + + @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) { // Skip the quick-launch if there isn't exactly one item @@ -796,7 +785,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc * recycler view. */ private boolean handleTouchEvent(MotionEvent ev) { - LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = mLauncher.getDeviceProfile(); int x = (int) ev.getX(); int y = (int) ev.getY(); @@ -884,6 +872,15 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc return false; } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + // Since the other cases are caught by the AllAppsRecyclerView LaunchSourceProvider, we just + // handle the prediction bar icons here + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_PREDICTION); + } + /** * Returns the predicted app in the prediction bar given a set of local coordinates. */ @@ -919,6 +916,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc * Shows the search field. */ private void showSearchField() { + mSearchManager.connect(); + // Show the search bar and focus the search final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, getContext().getResources().getDisplayMetrics()); @@ -948,7 +947,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc /** * Hides the search field. */ - private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) { + @Thunk void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) { + mSearchManager.cancel(true); + final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, getContext().getResources().getDisplayMetrics()); @@ -966,7 +967,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc if (resetTextField) { mSearchBarEditView.setText(""); } - mApps.setFilter(null); + mApps.setOrderedFilter(null); if (returnFocusToRecyclerView) { mAppsRecyclerView.requestFocus(); } @@ -983,7 +984,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc if (resetTextField) { mSearchBarEditView.setText(""); } - mApps.setFilter(null); + mApps.setOrderedFilter(null); mSearchButtonView.setAlpha(1f); mSearchButtonView.setTranslationX(0f); if (returnFocusToRecyclerView) { @@ -1011,7 +1012,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc /** * Returns an input method manager. */ - private InputMethodManager getInputMethodManager() { + @Thunk InputMethodManager getInputMethodManager() { return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index e010270ce..307d9403d 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -288,7 +288,7 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol private GridLayoutManager mGridLayoutMgr; private GridSpanSizer mGridSizer; private GridItemDecoration mItemDecoration; - private PredictionBarSpacerCallbacks mPredictionBarCb; + @Thunk PredictionBarSpacerCallbacks mPredictionBarCb; private View.OnTouchListener mTouchListener; private View.OnClickListener mIconClickListener; private View.OnLongClickListener mIconLongClickListener; diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index cc5add3b2..e1b5d918e 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -18,14 +18,15 @@ package com.android.launcher3.allapps; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; - import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; +import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import java.util.List; @@ -33,7 +34,8 @@ import java.util.List; /** * A RecyclerView with custom fast scroll support for the all apps view. */ -public class AllAppsRecyclerView extends BaseRecyclerView { +public class AllAppsRecyclerView extends BaseRecyclerView + implements Stats.LaunchSourceProvider { private AlphabeticalAppsList mApps; private int mNumAppsPerRow; @@ -125,6 +127,18 @@ public class AllAppsRecyclerView extends BaseRecyclerView { addOnItemTouchListener(this); } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); + if (mApps.hasFilter()) { + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_SEARCH); + } else { + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_A_Z); + } + } + /** * Maps the touch (from 0..1) to the adapter position that should be visible. */ diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 3d1503d46..e284f77c4 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -1,15 +1,36 @@ +/* + * Copyright (C) 2015 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.content.Context; import android.support.v7.widget.RecyclerView; import android.util.Log; + import com.android.launcher3.AppInfo; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.model.AbstractUserComparator; import com.android.launcher3.model.AppNameComparator; +import com.android.launcher3.util.Thunk; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -25,6 +46,7 @@ public class AlphabeticalAppsList { public static final String TAG = "AlphabeticalAppsList"; private static final boolean DEBUG = false; + private static final boolean DEBUG_PREDICTIONS = false; /** * Info about a section in the alphabetic list @@ -113,13 +135,6 @@ public class AlphabeticalAppsList { } /** - * A filter interface to limit the set of applications in the apps list. - */ - public interface Filter { - boolean retainApp(AppInfo info, String sectionName); - } - - /** * Callback to notify when the set of adapter items have changed. */ public interface AdapterChangedCallback { @@ -130,46 +145,63 @@ public class AlphabeticalAppsList { * Common interface for different merging strategies. */ private interface MergeAlgorithm { - boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount); + boolean continueMerging(SectionInfo section, SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount); } /** - * The logic we use to merge sections on tablets. + * The logic we use to merge sections on tablets. Currently, we don't show section names on + * tablet layouts, so just merge all the sections indiscriminately. */ - private static class TabletMergeAlgorithm implements MergeAlgorithm { + @Thunk static class TabletMergeAlgorithm implements MergeAlgorithm { @Override - public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) { + public boolean continueMerging(SectionInfo section, SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { // Merge EVERYTHING return true; } } /** - * The logic we use to merge sections on phones. + * The logic we use to merge sections on phones. We only merge sections when their final row + * contains less than a certain number of icons, and stop at a specified max number of merges. + * In addition, we will try and not merge sections that identify apps from different scripts. */ private static class PhoneMergeAlgorithm implements MergeAlgorithm { private int mMinAppsPerRow; private int mMinRowsInMergedSection; private int mMaxAllowableMerges; + private CharsetEncoder mAsciiEncoder; public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { mMinAppsPerRow = minAppsPerRow; mMinRowsInMergedSection = minRowsInMergedSection; mMaxAllowableMerges = maxNumMerges; + mAsciiEncoder = Charset.forName("US-ASCII").newEncoder(); } @Override - public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) { + public boolean continueMerging(SectionInfo section, SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { // Continue merging if the number of hanging apps on the final row is less than some // fixed number (ragged), the merged rows has yet to exceed some minimum row count, // and while the number of merged sections is less than some fixed number of merges int rows = sectionAppCount / numAppsPerRow; int cols = sectionAppCount % numAppsPerRow; + + // Ensure that we do not merge across scripts, currently we only allow for english and + // native scripts so we can test if both can just be ascii encoded + boolean isCrossScript = false; + if (section.firstAppItem != null && withSection.firstAppItem != null) { + isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) != + mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName); + } return (0 < cols && cols < mMinAppsPerRow) && rows < mMinRowsInMergedSection && - mergeCount < mMaxAllowableMerges; + mergeCount < mMaxAllowableMerges && + !isCrossScript; } } @@ -179,7 +211,7 @@ public class AlphabeticalAppsList { private Launcher mLauncher; // The set of apps from the system not including predictions - private List<AppInfo> mApps = new ArrayList<>(); + private final List<AppInfo> mApps = new ArrayList<>(); // The set of filtered apps with the current filter private List<AppInfo> mFilteredApps = new ArrayList<>(); // The current set of adapter items @@ -192,9 +224,10 @@ public class AlphabeticalAppsList { private List<ComponentName> mPredictedAppComponents = new ArrayList<>(); // The set of predicted apps resolved from the component names and the current set of apps private List<AppInfo> mPredictedApps = new ArrayList<>(); + // The of ordered component names as a result of a search query + private ArrayList<ComponentName> mSearchResults; private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); private RecyclerView.Adapter mAdapter; - private Filter mFilter; private AlphabeticIndexCompat mIndexer; private AppNameComparator mAppNameComparator; private MergeAlgorithm mMergeAlgorithm; @@ -216,6 +249,10 @@ public class AlphabeticalAppsList { mAdapterChangedCallback = cb; } + public SimpleAppSearchManagerImpl newSimpleAppSearchManager() { + return new SimpleAppSearchManagerImpl(mApps); + } + /** * Sets the number of apps per row. Used only for AppsContainerView.SECTIONED_GRID_COALESCED. */ @@ -274,22 +311,22 @@ public class AlphabeticalAppsList { * Returns whether there are is a filter set. */ public boolean hasFilter() { - return (mFilter != null); + return (mSearchResults != null); } /** * Returns whether there are no filtered results. */ public boolean hasNoFilteredResults() { - return (mFilter != null) && mFilteredApps.isEmpty(); + return (mSearchResults != null) && mFilteredApps.isEmpty(); } /** - * Sets the current filter for this list of apps. + * Sets the sorted list of filtered components. */ - public void setFilter(Filter f) { - if (mFilter != f) { - mFilter = f; + public void setOrderedFilter(ArrayList<ComponentName> f) { + if (mSearchResults != f) { + mSearchResults = f; updateAdapterItems(); } } @@ -409,7 +446,9 @@ public class AlphabeticalAppsList { for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { allApps.addAll(entry.getValue()); } - mApps = allApps; + + mApps.clear(); + mApps.addAll(allApps); } else { // Just compute the section headers for use below for (AppInfo info : mApps) { @@ -439,6 +478,15 @@ public class AlphabeticalAppsList { mAdapterItems.clear(); mSections.clear(); + if (DEBUG_PREDICTIONS) { + if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { + mPredictedAppComponents.add(mApps.get(0).componentName); + mPredictedAppComponents.add(mApps.get(0).componentName); + mPredictedAppComponents.add(mApps.get(0).componentName); + mPredictedAppComponents.add(mApps.get(0).componentName); + } + } + // Process the predicted app components mPredictedApps.clear(); if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { @@ -464,16 +512,12 @@ public class AlphabeticalAppsList { // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the // ordered set of sections - int numApps = mApps.size(); + List<AppInfo> apps = getFiltersAppInfos(); + int numApps = apps.size(); for (int i = 0; i < numApps; i++) { - AppInfo info = mApps.get(i); + AppInfo info = apps.get(i); String sectionName = getAndUpdateCachedSectionName(info.title); - // Check if we want to retain this app - if (mFilter != null && !mFilter.retainApp(info, sectionName)) { - continue; - } - // Create a new section if the section names do not match if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { lastSectionName = sectionName; @@ -514,6 +558,41 @@ public class AlphabeticalAppsList { } } + private List<AppInfo> getFiltersAppInfos() { + if (mSearchResults == null) { + return mApps; + } + + int total = mSearchResults.size(); + final HashMap<ComponentName, Integer> sortOrder = new HashMap<>(total); + for (int i = 0; i < total; i++) { + sortOrder.put(mSearchResults.get(i), i); + } + + ArrayList<AppInfo> result = new ArrayList<>(); + for (AppInfo info : mApps) { + if (sortOrder.containsKey(info.componentName)) { + result.add(info); + } + } + + Collections.sort(result, new AbstractUserComparator<AppInfo>( + LauncherAppState.getInstance().getContext()) { + + @Override + public int compare(AppInfo lhs, AppInfo rhs) { + Integer indexA = sortOrder.get(lhs.componentName); + int result = indexA.compareTo(sortOrder.get(rhs.componentName)); + if (result == 0) { + return super.compare(lhs, rhs); + } else { + return result; + } + } + }); + return result; + } + /** * Merges multiple sections to reduce visual raggedness. */ @@ -521,14 +600,15 @@ public class AlphabeticalAppsList { // Go through each section and try and merge some of the sections if (AllAppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) { int sectionAppCount = 0; - for (int i = 0; i < mSections.size(); i++) { + for (int i = 0; i < mSections.size() - 1; i++) { SectionInfo section = mSections.get(i); sectionAppCount = section.numApps; int mergeCount = 1; // Merge rows based on the current strategy - while (mMergeAlgorithm.continueMerging(sectionAppCount, mNumAppsPerRow, mergeCount) && - (i + 1) < mSections.size()) { + while (i < (mSections.size() - 1) && + mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), + sectionAppCount, mNumAppsPerRow, mergeCount)) { SectionInfo nextSection = mSections.remove(i + 1); // Remove the next section break diff --git a/src/com/android/launcher3/allapps/AppSearchManager.java b/src/com/android/launcher3/allapps/AppSearchManager.java new file mode 100644 index 000000000..b6aa22341 --- /dev/null +++ b/src/com/android/launcher3/allapps/AppSearchManager.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 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 java.util.ArrayList; + +/** + * Interface for handling app search. + */ +public interface AppSearchManager { + + /** + * Called when the search is about to be used. This method is optional for making a query but + * calling this appropriately can improve the initial response time. + */ + void connect(); + + /** + * Cancels all pending search requests. + * + * @param interruptActiveRequests if true, any active requests which are already executing will + * be invalidated, and the corresponding results will not be sent. The client should usually + * set this to true, before beginning a new search session. + */ + void cancel(boolean interruptActiveRequests); + + /** + * Performs a search + */ + void doSearch(String query, AppSearchResultCallback callback); + + /** + * Callback for getting search results. + */ + public interface AppSearchResultCallback { + + /** + * Called when the search is complete. + * + * @param apps sorted list of matching components or null if in case of failure. + */ + void onSearchResult(ArrayList<ComponentName> apps); + } +} diff --git a/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java b/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java new file mode 100644 index 000000000..e8a31b546 --- /dev/null +++ b/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 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.os.Handler; + +import com.android.launcher3.AppInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * An {@link AppSearchManager} which does label matching on the UI thread. + */ +public class SimpleAppSearchManagerImpl implements AppSearchManager { + + private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); + + private final List<AppInfo> mApps; + private final Handler mResultHandler; + + public SimpleAppSearchManagerImpl(List<AppInfo> apps) { + mApps = apps; + mResultHandler = new Handler(); + } + + @Override + public void connect() { + // No op + } + + @Override + public void cancel(boolean interruptActiveRequests) { + if (interruptActiveRequests) { + mResultHandler.removeCallbacksAndMessages(null); + } + } + + @Override + public void doSearch(String query, final AppSearchResultCallback callback) { + // 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<ComponentName> result = new ArrayList<ComponentName>(); + int total = mApps.size(); + + for (int i = 0; i < total; i++) { + AppInfo info = mApps.get(i); + if (!result.contains(info.componentName) && matches(info, queryWords)) { + result.add(info.componentName); + } + } + mResultHandler.post(new Runnable() { + + @Override + public void run() { + callback.onSearchResult(result); + } + }); + } + + private boolean matches(AppInfo info, String[] queryWords) { + 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; + } + } + return true; + } + +} diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java new file mode 100644 index 000000000..cf47ce648 --- /dev/null +++ b/src/com/android/launcher3/model/AbstractUserComparator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 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.model; + +import android.content.Context; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Comparator; +import java.util.HashMap; + +/** + * A comparator to arrange items based on user profiles. + */ +public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> { + + private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>(); + private final UserManagerCompat mUserManager; + private final UserHandleCompat mMyUser; + + public AbstractUserComparator(Context context) { + mUserManager = UserManagerCompat.getInstance(context); + mMyUser = UserHandleCompat.myUserHandle(); + } + + @Override + public int compare(T lhs, T rhs) { + if (mMyUser.equals(lhs.user)) { + return -1; + } else { + Long aUserSerial = getAndCacheUserSerial(lhs.user); + Long bUserSerial = getAndCacheUserSerial(rhs.user); + return aUserSerial.compareTo(bUserSerial); + } + } + + /** + * Returns the user serial for this user, using a cached serial if possible. + */ + private Long getAndCacheUserSerial(UserHandleCompat user) { + Long userSerial = mUserSerialCache.get(user); + if (userSerial == null) { + userSerial = mUserManager.getSerialNumberForUser(user); + mUserSerialCache.put(user, userSerial); + } + return userSerial; + } + + public void clearUserCache() { + mUserSerialCache.clear(); + } +} diff --git a/src/com/android/launcher3/model/AppNameComparator.java b/src/com/android/launcher3/model/AppNameComparator.java index 706f7515d..cd45d2c94 100644 --- a/src/com/android/launcher3/model/AppNameComparator.java +++ b/src/com/android/launcher3/model/AppNameComparator.java @@ -1,14 +1,28 @@ +/* + * Copyright (C) 2015 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.model; import android.content.Context; import com.android.launcher3.AppInfo; import com.android.launcher3.ItemInfo; -import com.android.launcher3.compat.UserHandleCompat; -import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.Thunk; + import java.text.Collator; import java.util.Comparator; -import java.util.HashMap; /** * Class to manage access to an app name comparator. @@ -16,17 +30,15 @@ import java.util.HashMap; * Used to sort application name in all apps view and widget tray view. */ public class AppNameComparator { - private final UserManagerCompat mUserManager; private final Collator mCollator; - private final Comparator<ItemInfo> mAppInfoComparator; + private final AbstractUserComparator<ItemInfo> mAppInfoComparator; private final Comparator<String> mSectionNameComparator; - private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>(); public AppNameComparator(Context context) { mCollator = Collator.getInstance(); - mUserManager = UserManagerCompat.getInstance(context); - mAppInfoComparator = new Comparator<ItemInfo>() { + mAppInfoComparator = new AbstractUserComparator<ItemInfo>(context) { + @Override public final int compare(ItemInfo a, ItemInfo b) { // Order by the title in the current locale int result = compareTitles(a.title.toString(), b.title.toString()); @@ -38,13 +50,7 @@ public class AppNameComparator { if (result == 0) { // If the two apps are the same component, then prioritize by the order that // the app user was created (prioritizing the main user's apps) - if (UserHandleCompat.myUserHandle().equals(a.user)) { - return -1; - } else { - Long aUserSerial = getAndCacheUserSerial(a.user); - Long bUserSerial = getAndCacheUserSerial(b.user); - return aUserSerial.compareTo(bUserSerial); - } + return super.compare(a, b); } } return result; @@ -63,7 +69,7 @@ public class AppNameComparator { */ public Comparator<ItemInfo> getAppInfoComparator() { // Clear the user serial cache so that we get serials as needed in the comparator - mUserSerialCache.clear(); + mAppInfoComparator.clearUserCache(); return mAppInfoComparator; } @@ -77,7 +83,7 @@ public class AppNameComparator { /** * Compares two titles with the same return value semantics as Comparator. */ - private int compareTitles(String titleA, String titleB) { + @Thunk int compareTitles(String titleA, String titleB) { // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit boolean aStartsWithLetter = Character.isLetterOrDigit(titleA.codePointAt(0)); boolean bStartsWithLetter = Character.isLetterOrDigit(titleB.codePointAt(0)); @@ -90,16 +96,4 @@ public class AppNameComparator { // Order by the title in the current locale return mCollator.compare(titleA, titleB); } - - /** - * Returns the user serial for this user, using a cached serial if possible. - */ - private Long getAndCacheUserSerial(UserHandleCompat user) { - Long userSerial = mUserSerialCache.get(user); - if (userSerial == null) { - userSerial = mUserManager.getSerialNumberForUser(user); - mUserSerialCache.put(user, userSerial); - } - return userSerial; - } } diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/LongArrayMap.java index e3c96cd42..a337e85bd 100644 --- a/src/com/android/launcher3/util/LongArrayMap.java +++ b/src/com/android/launcher3/util/LongArrayMap.java @@ -43,7 +43,7 @@ public class LongArrayMap<E> extends LongSparseArray<E> implements Iterable<E> { return new ValueIterator(); } - private class ValueIterator implements Iterator<E> { + @Thunk class ValueIterator implements Iterator<E> { private int mNextIndex = 0; diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java index d65455053..887587905 100644 --- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -15,6 +15,7 @@ import com.android.launcher3.DragLayer; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.util.Thunk; public class WidgetHostViewLoader { @@ -38,7 +39,7 @@ public class WidgetHostViewLoader { PendingAddWidgetInfo mCreateWidgetInfo = null; // TODO: technically, this class should not have to know the existence of the launcher. - private Launcher mLauncher; + @Thunk Launcher mLauncher; private Handler mHandler; public WidgetHostViewLoader(Launcher launcher) { @@ -188,7 +189,7 @@ public class WidgetHostViewLoader { return options; } - private void setState(int state) { + @Thunk void setState(int state) { if (DEBUG) { Log.d(TAG, String.format(" state [%d -> %d]", mState, state)); } diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 11c2107f2..8d04be5e3 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -46,6 +46,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.Workspace; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.Thunk; /** * The widgets list view container. @@ -60,7 +61,7 @@ public class WidgetsContainerView extends BaseContainerView private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1; /* Global instances that are used inside this container. */ - private Launcher mLauncher; + @Thunk Launcher mLauncher; private DragController mDragController; private IconCache mIconCache; |