diff options
author | Hyunyoung Song <hyunyoungs@google.com> | 2015-07-21 17:01:26 -0700 |
---|---|---|
committer | Ed Heyl <edheyl@google.com> | 2015-07-22 11:31:04 -0700 |
commit | e612775922ec9f8cc4e5cb976bc62b3312a3de0e (patch) | |
tree | 96beaf07307486f6491f022da63ebe381765d1a8 /src/com/android/launcher3 | |
parent | a83129f0bc778fd1ccd799bd8cfa40270f632e1d (diff) | |
download | android_packages_apps_Trebuchet-e612775922ec9f8cc4e5cb976bc62b3312a3de0e.tar.gz android_packages_apps_Trebuchet-e612775922ec9f8cc4e5cb976bc62b3312a3de0e.tar.bz2 android_packages_apps_Trebuchet-e612775922ec9f8cc4e5cb976bc62b3312a3de0e.zip |
resolved conflicts for merge of 13ef17a3 to mnc-dr-dev
b/22609402
Change-Id: I140cf972d57e14737a6f91c0b4a8ec6c7ff1af2b
Diffstat (limited to 'src/com/android/launcher3')
156 files changed, 18858 insertions, 14158 deletions
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 72c6693b3..3b25dca34 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -24,6 +24,7 @@ import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; @@ -98,14 +99,14 @@ class AllAppsList { user); for (LauncherActivityInfoCompat info : matches) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } } /** * Remove the apps for the given apk identified by packageName. */ - public void removePackage(String packageName, UserHandleCompat user, boolean clearCache) { + public void removePackage(String packageName, UserHandleCompat user) { final List<AppInfo> data = this.data; for (int i = data.size() - 1; i >= 0; i--) { AppInfo info = data.get(i); @@ -115,8 +116,15 @@ class AllAppsList { data.remove(i); } } - if (clearCache) { - mIconCache.remove(packageName, user); + } + + public void updateIconsAndLabels(HashSet<String> packages, UserHandleCompat user, + ArrayList<AppInfo> outUpdates) { + for (AppInfo info : data) { + if (info.user.equals(user) && packages.contains(info.componentName.getPackageName())) { + mIconCache.updateTitleAndIcon(info); + outUpdates.add(info); + } } } @@ -137,7 +145,6 @@ class AllAppsList { && packageName.equals(component.getPackageName())) { if (!findActivity(matches, component)) { removed.add(applicationInfo); - mIconCache.remove(component, user); data.remove(i); } } @@ -150,10 +157,9 @@ class AllAppsList { info.getComponentName().getPackageName(), user, info.getComponentName().getClassName()); if (applicationInfo == null) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } else { - mIconCache.remove(applicationInfo.componentName, user); - mIconCache.getTitleAndIcon(applicationInfo, info, null); + mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */); modified.add(applicationInfo); } } diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index a66bac08a..c95d5585a 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -19,19 +19,16 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.util.Log; import com.android.launcher3.compat.LauncherActivityInfoCompat; -import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; /** * Represents an app in AllAppsView. @@ -42,19 +39,24 @@ public class AppInfo extends ItemInfo { /** * The intent used to start the application. */ - Intent intent; + public Intent intent; /** * A bitmap version of the application icon. */ - Bitmap iconBitmap; + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; /** * The time at which the app was first installed. */ long firstInstallTime; - ComponentName componentName; + public ComponentName componentName; static final int DOWNLOADED_FLAG = 1; static final int UPDATED_SYSTEM_APP_FLAG = 2; @@ -77,13 +79,13 @@ public class AppInfo extends ItemInfo { * Must not hold the Context. */ public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, - IconCache iconCache, HashMap<Object, CharSequence> labelCache) { + IconCache iconCache) { this.componentName = info.getComponentName(); this.container = ItemInfo.NO_ID; flags = initFlags(info); firstInstallTime = info.getFirstInstallTime(); - iconCache.getTitleAndIcon(this, info, labelCache); + iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */); intent = makeLaunchIntent(context, info, user); this.user = user; } @@ -104,7 +106,7 @@ public class AppInfo extends ItemInfo { public AppInfo(AppInfo info) { super(info); componentName = info.componentName; - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); flags = info.flags; firstInstallTime = info.firstInstallTime; @@ -113,19 +115,22 @@ public class AppInfo extends ItemInfo { @Override public String toString() { - return "ApplicationInfo(title=" + title.toString() + " id=" + this.id + return "ApplicationInfo(title=" + title + " id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + " user=" + user + ")"; } + /** + * Helper method used for debugging. + */ public static void dumpApplicationInfoList(String tag, String label, ArrayList<AppInfo> list) { Log.d(tag, label + " size=" + list.size()); for (AppInfo info: list) { - Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" - + info.iconBitmap + " firstInstallTime=" - + info.firstInstallTime); + Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap + + " firstInstallTime=" + info.firstInstallTime + + " componentName=" + info.componentName.getPackageName()); } } @@ -133,6 +138,10 @@ public class AppInfo extends ItemInfo { return new ShortcutInfo(this); } + public ComponentKey toComponentKey() { + return new ComponentKey(componentName, user); + } + public static Intent makeLaunchIntent(Context context, LauncherActivityInfoCompat info, UserHandleCompat user) { long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index f57f4d036..ea7c22189 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -8,30 +8,43 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.view.Gravity; import android.widget.FrameLayout; import android.widget.ImageView; public class AppWidgetResizeFrame extends FrameLayout { - private LauncherAppWidgetHostView mWidgetView; - private CellLayout mCellLayout; - private DragLayer mDragLayer; - private ImageView mLeftHandle; - private ImageView mRightHandle; - private ImageView mTopHandle; - private ImageView mBottomHandle; + private static final int SNAP_DURATION = 150; + private static final float DIMMED_HANDLE_ALPHA = 0f; + private static final float RESIZE_THRESHOLD = 0.66f; + + private static Rect sTmpRect = new Rect(); + + private final Launcher mLauncher; + private final LauncherAppWidgetHostView mWidgetView; + private final CellLayout mCellLayout; + private final DragLayer mDragLayer; + + private final ImageView mLeftHandle; + private final ImageView mRightHandle; + private final ImageView mTopHandle; + private final ImageView mBottomHandle; + + private final Rect mWidgetPadding; + + private final int mBackgroundPadding; + private final int mTouchTargetWidth; + + private final int[] mDirectionVector = new int[2]; + private final int[] mLastDirectionVector = new int[2]; + private final int[] mTmpPt = new int[2]; private boolean mLeftBorderActive; private boolean mRightBorderActive; private boolean mTopBorderActive; private boolean mBottomBorderActive; - private int mWidgetPaddingLeft; - private int mWidgetPaddingRight; - private int mWidgetPaddingTop; - private int mWidgetPaddingBottom; - private int mBaselineWidth; private int mBaselineHeight; private int mBaselineX; @@ -47,30 +60,9 @@ public class AppWidgetResizeFrame extends FrameLayout { private int mDeltaXAddOn; private int mDeltaYAddOn; - private int mBackgroundPadding; - private int mTouchTargetWidth; - private int mTopTouchRegionAdjustment = 0; private int mBottomTouchRegionAdjustment = 0; - int[] mDirectionVector = new int[2]; - int[] mLastDirectionVector = new int[2]; - int[] mTmpPt = new int[2]; - - final int SNAP_DURATION = 150; - final int BACKGROUND_PADDING = 24; - final float DIMMED_HANDLE_ALPHA = 0f; - final float RESIZE_THRESHOLD = 0.66f; - - private static Rect mTmpRect = new Rect(); - - public static final int LEFT = 0; - public static final int TOP = 1; - public static final int RIGHT = 2; - public static final int BOTTOM = 3; - - private Launcher mLauncher; - public AppWidgetResizeFrame(Context context, LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) { @@ -78,48 +70,56 @@ public class AppWidgetResizeFrame extends FrameLayout { mLauncher = (Launcher) context; mCellLayout = cellLayout; mWidgetView = widgetView; - mResizeMode = widgetView.getAppWidgetInfo().resizeMode; + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) + widgetView.getAppWidgetInfo(); + mResizeMode = info.resizeMode; mDragLayer = dragLayer; - final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo(); - int[] result = Launcher.getMinSpanForWidget(mLauncher, info); - mMinHSpan = result[0]; - mMinVSpan = result[1]; + mMinHSpan = info.getMinSpanX(mLauncher); + mMinVSpan = info.getMinSpanY(mLauncher); - setBackgroundResource(R.drawable.widget_resize_frame_holo); + setBackgroundResource(R.drawable.widget_resize_shadow); + setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); setPadding(0, 0, 0, 0); + final int handleMargin = getResources().getDimensionPixelSize(R.dimen.widget_handle_margin); LayoutParams lp; mLeftHandle = new ImageView(context); - mLeftHandle.setImageResource(R.drawable.widget_resize_handle_left); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL); + lp.leftMargin = handleMargin; addView(mLeftHandle, lp); mRightHandle = new ImageView(context); - mRightHandle.setImageResource(R.drawable.widget_resize_handle_right); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mRightHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.CENTER_VERTICAL); + lp.rightMargin = handleMargin; addView(mRightHandle, lp); mTopHandle = new ImageView(context); - mTopHandle.setImageResource(R.drawable.widget_resize_handle_top); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mTopHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP); + lp.topMargin = handleMargin; addView(mTopHandle, lp); mBottomHandle = new ImageView(context); - mBottomHandle.setImageResource(R.drawable.widget_resize_handle_bottom); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mBottomHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + lp.bottomMargin = handleMargin; addView(mBottomHandle, lp); - Rect p = AppWidgetHostView.getDefaultPaddingForWidget(context, - widgetView.getAppWidgetInfo().provider, null); - mWidgetPaddingLeft = p.left; - mWidgetPaddingTop = p.top; - mWidgetPaddingRight = p.right; - mWidgetPaddingBottom = p.bottom; + if (!info.isCustomWidget) { + mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, + widgetView.getAppWidgetInfo().provider, null); + } else { + Resources r = context.getResources(); + int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding); + mWidgetPadding = new Rect(padding, padding, padding, padding); + } if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { mTopHandle.setVisibility(GONE); @@ -129,8 +129,8 @@ public class AppWidgetResizeFrame extends FrameLayout { mRightHandle.setVisibility(GONE); } - final float density = mLauncher.getResources().getDisplayMetrics().density; - mBackgroundPadding = (int) Math.ceil(density * BACKGROUND_PADDING); + mBackgroundPadding = getResources() + .getDimensionPixelSize(R.dimen.resize_frame_background_padding); mTouchTargetWidth = 2 * mBackgroundPadding; // When we create the resize frame, we first mark all cells as unoccupied. The appropriate @@ -335,13 +335,12 @@ public class AppWidgetResizeFrame extends FrameLayout { static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY) { - - getWidgetSizeRanges(launcher, spanX, spanY, mTmpRect); - widgetView.updateAppWidgetSize(null, mTmpRect.left, mTmpRect.top, - mTmpRect.right, mTmpRect.bottom); + getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect); + widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top, + sTmpRect.right, sTmpRect.bottom); } - static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { + public static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { if (rect == null) { rect = new Rect(); } @@ -396,19 +395,19 @@ public class AppWidgetResizeFrame extends FrameLayout { public void snapToWidget(boolean animate) { final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft - - mWidgetPaddingRight; - int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop - - mWidgetPaddingBottom; + int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding + - mWidgetPadding.left - mWidgetPadding.right; + int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding + - mWidgetPadding.top - mWidgetPadding.bottom; mTmpPt[0] = mWidgetView.getLeft(); mTmpPt[1] = mWidgetView.getTop(); mDragLayer.getDescendantCoordRelativeToSelf(mCellLayout.getShortcutsAndWidgets(), mTmpPt); - int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPaddingLeft; - int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPaddingTop; + int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPadding.left; + int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPadding.top; - // We need to make sure the frame's touchable regions lie fully within the bounds of the + // We need to make sure the frame's touchable regions lie fully within the bounds of the // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions // down accordingly to provide a proper touch target. if (newY < 0) { diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index 880aaf1ec..5e7a012d2 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -90,5 +90,10 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } + + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } } } diff --git a/src/com/android/launcher3/AppsCustomizeCellLayout.java b/src/com/android/launcher3/AppsCustomizeCellLayout.java deleted file mode 100644 index a50fb6821..000000000 --- a/src/com/android/launcher3/AppsCustomizeCellLayout.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2010 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.content.Context; -import android.view.View; - -public class AppsCustomizeCellLayout extends CellLayout implements Page { - - final FocusIndicatorView mFocusHandlerView; - - public AppsCustomizeCellLayout(Context context) { - super(context); - - mFocusHandlerView = new FocusIndicatorView(context); - addView(mFocusHandlerView, 0); - mFocusHandlerView.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - mFocusHandlerView.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - } - - @Override - public void removeAllViewsOnPage() { - removeAllViews(); - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - removeViewAt(index); - } - - @Override - public int getPageChildCount() { - return getChildCount(); - } - - @Override - public View getChildOnPageAt(int i) { - return getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return indexOfChild(v); - } - - /** - * Clears all the key listeners for the individual icons. - */ - public void resetChildrenOnKeyListeners() { - ShortcutAndWidgetContainer children = getShortcutsAndWidgets(); - int childCount = children.getChildCount(); - for (int j = 0; j < childCount; ++j) { - children.getChildAt(j).setOnKeyListener(null); - } - } -} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java deleted file mode 100644 index c8187f068..000000000 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ /dev/null @@ -1,1567 +0,0 @@ -/* - * Copyright (C) 2011 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.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Process; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.widget.GridLayout; -import android.widget.ImageView; -import android.widget.Toast; - -import com.android.launcher3.DropTarget.DragObject; -import com.android.launcher3.compat.AppWidgetManagerCompat; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -/** - * A simple callback interface which also provides the results of the task. - */ -interface AsyncTaskCallback { - void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data); -} - -/** - * The data needed to perform either of the custom AsyncTasks. - */ -class AsyncTaskPageData { - enum Type { - LoadWidgetPreviewData - } - - AsyncTaskPageData(int p, ArrayList<Object> l, int cw, int ch, AsyncTaskCallback bgR, - AsyncTaskCallback postR, WidgetPreviewLoader w) { - page = p; - items = l; - generatedImages = new ArrayList<Bitmap>(); - maxImageWidth = cw; - maxImageHeight = ch; - doInBackgroundCallback = bgR; - postExecuteCallback = postR; - widgetPreviewLoader = w; - } - void cleanup(boolean cancelled) { - // Clean up any references to source/generated bitmaps - if (generatedImages != null) { - if (cancelled) { - for (int i = 0; i < generatedImages.size(); i++) { - widgetPreviewLoader.recycleBitmap(items.get(i), generatedImages.get(i)); - } - } - generatedImages.clear(); - } - } - int page; - ArrayList<Object> items; - ArrayList<Bitmap> sourceImages; - ArrayList<Bitmap> generatedImages; - int maxImageWidth; - int maxImageHeight; - AsyncTaskCallback doInBackgroundCallback; - AsyncTaskCallback postExecuteCallback; - WidgetPreviewLoader widgetPreviewLoader; -} - -/** - * A generic template for an async task used in AppsCustomize. - */ -class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTaskPageData> { - AppsCustomizeAsyncTask(int p, AsyncTaskPageData.Type ty) { - page = p; - threadPriority = Process.THREAD_PRIORITY_DEFAULT; - dataType = ty; - } - @Override - protected AsyncTaskPageData doInBackground(AsyncTaskPageData... params) { - if (params.length != 1) return null; - // Load each of the widget previews in the background - params[0].doInBackgroundCallback.run(this, params[0]); - return params[0]; - } - @Override - protected void onPostExecute(AsyncTaskPageData result) { - // All the widget previews are loaded, so we can just callback to inflate the page - result.postExecuteCallback.run(this, result); - } - - void setThreadPriority(int p) { - threadPriority = p; - } - void syncThreadPriority() { - Process.setThreadPriority(threadPriority); - } - - // The page that this async task is associated with - AsyncTaskPageData.Type dataType; - int page; - int threadPriority; -} - -/** - * The Apps/Customize page that displays all the applications, widgets, and shortcuts. - */ -public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements - View.OnClickListener, View.OnKeyListener, DragSource, - PagedViewWidget.ShortPressListener, LauncherTransitionable { - static final String TAG = "AppsCustomizePagedView"; - - private static Rect sTmpRect = new Rect(); - - /** - * The different content types that this paged view can show. - */ - public enum ContentType { - Applications, - Widgets - } - private ContentType mContentType = ContentType.Applications; - - // Refs - private Launcher mLauncher; - private DragController mDragController; - private final LayoutInflater mLayoutInflater; - private final PackageManager mPackageManager; - - // Save and Restore - private int mSaveInstanceStateItemIndex = -1; - - // Content - private ArrayList<AppInfo> mApps; - private ArrayList<Object> mWidgets; - - // Caching - private IconCache mIconCache; - - // Dimens - private int mContentWidth, mContentHeight; - private int mWidgetCountX, mWidgetCountY; - private PagedViewCellLayout mWidgetSpacingLayout; - private int mNumAppsPages; - private int mNumWidgetPages; - private Rect mAllAppsPadding = new Rect(); - - // Previews & outlines - ArrayList<AppsCustomizeAsyncTask> mRunningTasks; - private static final int sPageSleepDelay = 200; - - private Runnable mInflateWidgetRunnable = null; - private Runnable mBindWidgetRunnable = null; - static final int WIDGET_NO_CLEANUP_REQUIRED = -1; - static final int WIDGET_PRELOAD_PENDING = 0; - static final int WIDGET_BOUND = 1; - static final int WIDGET_INFLATED = 2; - int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - int mWidgetLoadingId = -1; - PendingAddWidgetInfo mCreateWidgetInfo = null; - private boolean mDraggingWidget = false; - boolean mPageBackgroundsVisible = true; - - private Toast mWidgetInstructionToast; - - // Deferral of loading widget previews during launcher transitions - private boolean mInTransition; - private ArrayList<AsyncTaskPageData> mDeferredSyncWidgetPageItems = - new ArrayList<AsyncTaskPageData>(); - private ArrayList<Runnable> mDeferredPrepareLoadWidgetPreviewsTasks = - new ArrayList<Runnable>(); - - WidgetPreviewLoader mWidgetPreviewLoader; - - private boolean mInBulkBind; - private boolean mNeedToUpdatePageCountsAndInvalidateData; - - public AppsCustomizePagedView(Context context, AttributeSet attrs) { - super(context, attrs); - mLayoutInflater = LayoutInflater.from(context); - mPackageManager = context.getPackageManager(); - mApps = new ArrayList<AppInfo>(); - mWidgets = new ArrayList<Object>(); - mIconCache = (LauncherAppState.getInstance()).getIconCache(); - mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); - - // Save the default widget preview background - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); - mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2); - mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2); - a.recycle(); - mWidgetSpacingLayout = new PagedViewCellLayout(getContext()); - - // The padding on the non-matched dimension for the default widget preview icons - // (top + bottom) - mFadeInAdjacentScreens = false; - - // Unless otherwise specified this view is important for accessibility. - if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - setSinglePageInViewport(); - } - - @Override - protected void init() { - super.init(); - mCenterPagesVertically = false; - - Context context = getContext(); - Resources r = context.getResources(); - setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f); - } - - public void onFinishInflate() { - super.onFinishInflate(); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setPadding(grid.edgeMarginPx, 2 * grid.edgeMarginPx, - grid.edgeMarginPx, 2 * grid.edgeMarginPx); - } - - void setAllAppsPadding(Rect r) { - mAllAppsPadding.set(r); - } - - void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) { - setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight); - } - - WidgetPreviewLoader getWidgetPreviewLoader() { - if (mWidgetPreviewLoader == null) { - mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher); - } - return mWidgetPreviewLoader; - } - - /** Returns the item index of the center item on this page so that we can restore to this - * item index when we rotate. */ - private int getMiddleComponentIndexOnCurrentPage() { - int i = -1; - if (getPageCount() > 0) { - int currentPage = getCurrentPage(); - if (mContentType == ContentType.Applications) { - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(currentPage); - ShortcutAndWidgetContainer childrenLayout = layout.getShortcutsAndWidgets(); - int numItemsPerPage = mCellCountX * mCellCountY; - int childCount = childrenLayout.getChildCount(); - if (childCount > 0) { - i = (currentPage * numItemsPerPage) + (childCount / 2); - } - } else if (mContentType == ContentType.Widgets) { - int numApps = mApps.size(); - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - int childCount = layout.getChildCount(); - if (childCount > 0) { - i = numApps + - (currentPage * numItemsPerPage) + (childCount / 2); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - } - return i; - } - - /** Get the index of the item to restore to if we need to restore the current page. */ - int getSaveInstanceStateIndex() { - if (mSaveInstanceStateItemIndex == -1) { - mSaveInstanceStateItemIndex = getMiddleComponentIndexOnCurrentPage(); - } - return mSaveInstanceStateItemIndex; - } - - /** Returns the page in the current orientation which is expected to contain the specified - * item index. */ - int getPageForComponent(int index) { - if (index < 0) return 0; - - if (index < mApps.size()) { - int numItemsPerPage = mCellCountX * mCellCountY; - return (index / numItemsPerPage); - } else { - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - return (index - mApps.size()) / numItemsPerPage; - } - } - - /** Restores the page for an item at the specified index */ - void restorePageForIndex(int index) { - if (index < 0) return; - mSaveInstanceStateItemIndex = index; - } - - private void updatePageCounts() { - mNumWidgetPages = (int) Math.ceil(mWidgets.size() / - (float) (mWidgetCountX * mWidgetCountY)); - mNumAppsPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY)); - } - - protected void onDataReady(int width, int height) { - // Now that the data is ready, we can calculate the content width, the number of cells to - // use for each page - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mCellCountX = (int) grid.allAppsNumCols; - mCellCountY = (int) grid.allAppsNumRows; - updatePageCounts(); - - // Force a measure to update recalculate the gaps - mContentWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - mContentHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - mWidgetSpacingLayout.measure(widthSpec, heightSpec); - - final boolean hostIsTransitioning = getTabHost().isInTransition(); - int page = getPageForComponent(mSaveInstanceStateItemIndex); - invalidatePageData(Math.max(0, page), hostIsTransitioning); - } - - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - - if (!isDataReady()) { - if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) { - post(new Runnable() { - // This code triggers requestLayout so must be posted outside of the - // layout pass. - public void run() { - if (Utilities.isViewAttachedToWindow(AppsCustomizePagedView.this)) { - setDataIsReady(); - onDataReady(getMeasuredWidth(), getMeasuredHeight()); - } - } - }); - } - } - } - - public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - // Get the list of widgets and shortcuts - mWidgets.clear(); - for (Object o : widgetsAndShortcuts) { - if (o instanceof AppWidgetProviderInfo) { - AppWidgetProviderInfo widget = (AppWidgetProviderInfo) o; - if (!app.shouldShowAppOrWidgetProvider(widget.provider)) { - continue; - } - if (widget.minWidth > 0 && widget.minHeight > 0) { - // Ensure that all widgets we show can be added on a workspace of this size - int[] spanXY = Launcher.getSpanForWidget(mLauncher, widget); - int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, widget); - int minSpanX = Math.min(spanXY[0], minSpanXY[0]); - int minSpanY = Math.min(spanXY[1], minSpanXY[1]); - if (minSpanX <= (int) grid.numColumns && - minSpanY <= (int) grid.numRows) { - mWidgets.add(widget); - } else { - Log.e(TAG, "Widget " + widget.provider + " can not fit on this device (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - Log.e(TAG, "Widget " + widget.provider + " has invalid dimensions (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - // just add shortcuts - mWidgets.add(o); - } - } - updatePageCountsAndInvalidateData(); - } - - public void setBulkBind(boolean bulkBind) { - if (bulkBind) { - mInBulkBind = true; - } else { - mInBulkBind = false; - if (mNeedToUpdatePageCountsAndInvalidateData) { - updatePageCountsAndInvalidateData(); - } - } - } - - private void updatePageCountsAndInvalidateData() { - if (mInBulkBind) { - mNeedToUpdatePageCountsAndInvalidateData = true; - } else { - updatePageCounts(); - invalidateOnDataChange(); - mNeedToUpdatePageCountsAndInvalidateData = false; - } - } - - @Override - public void onClick(View v) { - // When we have exited all apps or are in transition, disregard clicks - if (!mLauncher.isAllAppsVisible() - || mLauncher.getWorkspace().isSwitchingState() - || !(v instanceof PagedViewWidget)) return; - - // Let the user know that they have to long press to add a widget - if (mWidgetInstructionToast != null) { - mWidgetInstructionToast.cancel(); - } - mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, - Toast.LENGTH_SHORT); - mWidgetInstructionToast.show(); - - // Create a little animation to show that the widget can move - float offsetY = getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - final ImageView p = (ImageView) v.findViewById(R.id.widget_preview); - AnimatorSet bounce = LauncherAnimUtils.createAnimatorSet(); - ValueAnimator tyuAnim = LauncherAnimUtils.ofFloat(p, "translationY", offsetY); - tyuAnim.setDuration(125); - ValueAnimator tydAnim = LauncherAnimUtils.ofFloat(p, "translationY", 0f); - tydAnim.setDuration(100); - bounce.play(tyuAnim).before(tydAnim); - bounce.setInterpolator(new AccelerateInterpolator()); - bounce.start(); - } - - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleAppsCustomizeKeyEvent(v, keyCode, event); - } - - /* - * PagedViewWithDraggableItems implementation - */ - @Override - protected void determineDraggingStart(android.view.MotionEvent ev) { - // Disable dragging by pulling an app down for now. - } - - private void beginDraggingApplication(View v) { - mLauncher.getWorkspace().beginDragShared(v, this); - } - - static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { - Bundle options = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, sTmpRect); - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, - info.componentName, null); - - float density = launcher.getResources().getDisplayMetrics().density; - int xPaddingDips = (int) ((padding.left + padding.right) / density); - int yPaddingDips = (int) ((padding.top + padding.bottom) / density); - - options = new Bundle(); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, - sTmpRect.left - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, - sTmpRect.top - yPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - sTmpRect.right - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, - sTmpRect.bottom - yPaddingDips); - } - return options; - } - - private void preloadWidget(final PendingAddWidgetInfo info) { - final AppWidgetProviderInfo pInfo = info.info; - final Bundle options = getDefaultOptionsForWidget(mLauncher, info); - - if (pInfo.configure != null) { - info.bindOptions = options; - return; - } - - mWidgetCleanupState = WIDGET_PRELOAD_PENDING; - mBindWidgetRunnable = new Runnable() { - @Override - public void run() { - mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); - if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( - mWidgetLoadingId, pInfo, options)) { - mWidgetCleanupState = WIDGET_BOUND; - } - } - }; - post(mBindWidgetRunnable); - - mInflateWidgetRunnable = new Runnable() { - @Override - public void run() { - if (mWidgetCleanupState != WIDGET_BOUND) { - return; - } - AppWidgetHostView hostView = mLauncher. - getAppWidgetHost().createView(getContext(), mWidgetLoadingId, pInfo); - info.boundWidget = hostView; - mWidgetCleanupState = WIDGET_INFLATED; - hostView.setVisibility(INVISIBLE); - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(info.spanX, - info.spanY, info, false); - - // We want the first widget layout to be the correct size. This will be important - // for width size reporting to the AppWidgetManager. - DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], - unScaledSize[1]); - lp.x = lp.y = 0; - lp.customPosition = true; - hostView.setLayoutParams(lp); - mLauncher.getDragLayer().addView(hostView); - } - }; - post(mInflateWidgetRunnable); - } - - @Override - public void onShortPress(View v) { - // We are anticipating a long press, and we use this time to load bind and instantiate - // the widget. This will need to be cleaned up if it turns out no long press occurs. - if (mCreateWidgetInfo != null) { - // Just in case the cleanup process wasn't properly executed. This shouldn't happen. - cleanupWidgetPreloading(false); - } - mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag()); - preloadWidget(mCreateWidgetInfo); - } - - private void cleanupWidgetPreloading(boolean widgetWasAdded) { - if (!widgetWasAdded) { - // If the widget was not added, we may need to do further cleanup. - PendingAddWidgetInfo info = mCreateWidgetInfo; - mCreateWidgetInfo = null; - - if (mWidgetCleanupState == WIDGET_PRELOAD_PENDING) { - // We never did any preloading, so just remove pending callbacks to do so - removeCallbacks(mBindWidgetRunnable); - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_BOUND) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // We never got around to inflating the widget, so remove the callback to do so. - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_INFLATED) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // The widget was inflated and added to the DragLayer -- remove it. - AppWidgetHostView widget = info.boundWidget; - mLauncher.getDragLayer().removeView(widget); - } - } - mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - mWidgetLoadingId = -1; - mCreateWidgetInfo = null; - PagedViewWidget.resetShortPressTarget(); - } - - @Override - public void cleanUpShortPress(View v) { - if (!mDraggingWidget) { - cleanupWidgetPreloading(false); - } - } - - private boolean beginDraggingWidget(View v) { - mDraggingWidget = true; - // Get the widget preview as the drag representation - ImageView image = (ImageView) v.findViewById(R.id.widget_preview); - PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); - - // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and - // we abort the drag. - if (image.getDrawable() == null) { - mDraggingWidget = false; - return false; - } - - // Compose the drag image - Bitmap preview; - Bitmap outline; - float scale = 1f; - Point previewPadding = null; - - if (createItemInfo instanceof PendingAddWidgetInfo) { - // This can happen in some weird cases involving multi-touch. We can't start dragging - // the widget if this is null, so we break out. - if (mCreateWidgetInfo == null) { - return false; - } - - PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo; - createItemInfo = createWidgetInfo; - int spanX = createItemInfo.spanX; - int spanY = createItemInfo.spanY; - int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY, - createWidgetInfo, true); - - FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); - float minScale = 1.25f; - int maxWidth, maxHeight; - maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); - maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]); - - int[] previewSizeBeforeScale = new int[1]; - - preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info, - spanX, spanY, maxWidth, maxHeight, null, previewSizeBeforeScale); - - // Compare the size of the drag preview to the preview in the AppsCustomize tray - int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0], - getWidgetPreviewLoader().maxWidthForWidgetPreview(spanX)); - scale = previewWidthInAppsCustomize / (float) preview.getWidth(); - - // The bitmap in the AppsCustomize tray is always the the same size, so there - // might be extra pixels around the preview itself - this accounts for that - if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { - int padding = - (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; - previewPadding = new Point(padding, 0); - } - } else { - PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); - Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo); - preview = Utilities.createIconBitmap(icon, mLauncher); - createItemInfo.spanX = createItemInfo.spanY = 1; - } - - // Don't clip alpha values for the drag outline if we're using the default widget preview - boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && - (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); - - // Save the preview for the outline generation, then dim the preview - outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), - false); - - // Start the drag - mLauncher.lockScreenOrientation(); - mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); - mDragController.startDrag(image, preview, this, createItemInfo, - DragController.DRAG_ACTION_COPY, previewPadding, scale); - outline.recycle(); - preview.recycle(); - return true; - } - - @Override - protected boolean beginDragging(final View v) { - if (!super.beginDragging(v)) return false; - - if (v instanceof BubbleTextView) { - beginDraggingApplication(v); - } else if (v instanceof PagedViewWidget) { - if (!beginDraggingWidget(v)) { - return false; - } - } - - // We delay entering spring-loaded mode slightly to make sure the UI - // thready is free of any work. - postDelayed(new Runnable() { - @Override - public void run() { - // We don't enter spring-loaded mode if the drag has been cancelled - if (mLauncher.getDragController().isDragging()) { - // Go into spring loaded mode (must happen before we startDrag()) - mLauncher.enterSpringLoadedDragMode(); - } - } - }, 150); - - return true; - } - - /** - * Clean up after dragging. - * - * @param target where the item was dragged to (can be null if the item was flung) - */ - private void endDragging(View target, boolean isFlingToDelete, boolean success) { - if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && - !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { - // Exit spring loaded mode if we have not successfully dropped or have not handled the - // drop in Workspace - mLauncher.exitSpringLoadedDragModeDelayed(true, - Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); - mLauncher.unlockScreenOrientation(false); - } else { - mLauncher.unlockScreenOrientation(false); - } - } - - @Override - public View getContent() { - if (getChildCount() > 0) { - return getChildAt(0); - } - return null; - } - - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = true; - if (toWorkspace) { - cancelAllTasks(); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = false; - for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) { - onSyncWidgetPageItems(d, false); - } - mDeferredSyncWidgetPageItems.clear(); - for (Runnable r : mDeferredPrepareLoadWidgetPreviewsTasks) { - r.run(); - } - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - mForceDrawAllChildrenNextFrame = !toWorkspace; - } - - @Override - public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, - boolean success) { - // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling - if (isFlingToDelete) return; - - endDragging(target, false, success); - - // Display an error message if the drag failed due to there not being enough space on the - // target layout we were dropping on. - if (!success) { - boolean showOutOfSpaceMessage = false; - if (target instanceof Workspace) { - int currentScreen = mLauncher.getCurrentWorkspaceScreen(); - Workspace workspace = (Workspace) target; - CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); - ItemInfo itemInfo = (ItemInfo) d.dragInfo; - if (layout != null) { - layout.calculateSpans(itemInfo); - showOutOfSpaceMessage = - !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); - } - } - if (showOutOfSpaceMessage) { - mLauncher.showOutOfSpaceMessage(false); - } - - d.deferDragViewCleanupPostAnimation = false; - } - cleanupWidgetPreloading(success); - mDraggingWidget = false; - } - - @Override - public void onFlingToDeleteCompleted() { - // We just dismiss the drag when we fling, so cleanup here - endDragging(null, true, true); - cleanupWidgetPreloading(false); - mDraggingWidget = false; - } - - @Override - public boolean supportsFlingToDelete() { - return true; - } - - @Override - public boolean supportsAppInfoDropTarget() { - return true; - } - - @Override - public boolean supportsDeleteDropTarget() { - return false; - } - - @Override - public float getIntrinsicIconScaleFactor() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - return (float) grid.allAppsIconSizePx / grid.iconSizePx; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - cancelAllTasks(); - } - - @Override - public void trimMemory() { - super.trimMemory(); - clearAllWidgetPages(); - } - - public void clearAllWidgetPages() { - cancelAllTasks(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View v = getPageAt(i); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - mDirtyPageContent.set(i, true); - } - } - } - - private void cancelAllTasks() { - // Clean up all the async tasks - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - task.cancel(false); - iter.remove(); - mDirtyPageContent.set(task.page, true); - - // We've already preallocated the views for the data to load into, so clear them as well - View v = getPageAt(task.page); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - } - } - mDeferredSyncWidgetPageItems.clear(); - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - } - - public void setContentType(ContentType type) { - // Widgets appear to be cleared every time you leave, always force invalidate for them - if (mContentType != type || type == ContentType.Widgets) { - int page = (mContentType != type) ? 0 : getCurrentPage(); - mContentType = type; - invalidatePageData(page, true); - } - } - - public ContentType getContentType() { - return mContentType; - } - - protected void snapToPage(int whichPage, int delta, int duration) { - super.snapToPage(whichPage, delta, duration); - - // Update the thread priorities given the direction lookahead - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || - (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } else { - task.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - } - } - } - - /* - * Apps PagedView implementation - */ - private void setVisibilityOnChildren(ViewGroup layout, int visibility) { - int childCount = layout.getChildCount(); - for (int i = 0; i < childCount; ++i) { - layout.getChildAt(i).setVisibility(visibility); - } - } - private void setupPage(AppsCustomizeCellLayout layout) { - layout.setGridSize(mCellCountX, mCellCountY); - - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. That said, we already know the - // expected page width, so we can actually optimize by hiding all the TextView-based - // children that are expensive to measure, and let that happen naturally later. - setVisibilityOnChildren(layout, View.GONE); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - layout.measure(widthSpec, heightSpec); - - Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel); - if (bg != null) { - bg.setAlpha(mPageBackgroundsVisible ? 255: 0); - layout.setBackground(bg); - } - - setVisibilityOnChildren(layout, View.VISIBLE); - } - - public void setPageBackgroundsVisible(boolean visible) { - mPageBackgroundsVisible = visible; - int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - Drawable bg = getChildAt(i).getBackground(); - if (bg != null) { - bg.setAlpha(visible ? 255 : 0); - } - } - } - - public void syncAppsPageItems(int page, boolean immediate) { - // ensure that we have the right number of items on the pages - final boolean isRtl = isLayoutRtl(); - int numCells = mCellCountX * mCellCountY; - int startIndex = page * numCells; - int endIndex = Math.min(startIndex + numCells, mApps.size()); - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(page); - - layout.removeAllViewsOnPage(); - ArrayList<Object> items = new ArrayList<Object>(); - ArrayList<Bitmap> images = new ArrayList<Bitmap>(); - for (int i = startIndex; i < endIndex; ++i) { - AppInfo info = mApps.get(i); - BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( - R.layout.apps_customize_application, layout, false); - icon.applyFromApplicationInfo(info); - icon.setOnClickListener(mLauncher); - icon.setOnLongClickListener(this); - icon.setOnTouchListener(this); - icon.setOnKeyListener(this); - icon.setOnFocusChangeListener(layout.mFocusHandlerView); - - int index = i - startIndex; - int x = index % mCellCountX; - int y = index / mCellCountX; - if (isRtl) { - x = mCellCountX - x - 1; - } - layout.addViewToCellLayout(icon, -1, i, new CellLayout.LayoutParams(x,y, 1,1), false); - - items.add(info); - images.add(info.iconBitmap); - } - - enableHwLayersOnVisiblePages(); - } - - /** - * A helper to return the priority for loading of the specified widget page. - */ - private int getWidgetPageLoadPriority(int page) { - // If we are snapping to another page, use that index as the target page index - int toPage = mCurrentPage; - if (mNextPage > -1) { - toPage = mNextPage; - } - - // We use the distance from the target page as an initial guess of priority, but if there - // are no pages of higher priority than the page specified, then bump up the priority of - // the specified page. - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - int minPageDiff = Integer.MAX_VALUE; - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - minPageDiff = Math.abs(task.page - toPage); - } - - int rawPageDiff = Math.abs(page - toPage); - return rawPageDiff - Math.min(rawPageDiff, minPageDiff); - } - /** - * Return the appropriate thread priority for loading for a given page (we give the current - * page much higher priority) - */ - private int getThreadPriorityForPage(int page) { - // TODO-APPS_CUSTOMIZE: detect number of cores and set thread priorities accordingly below - int pageDiff = getWidgetPageLoadPriority(page); - if (pageDiff <= 0) { - return Process.THREAD_PRIORITY_LESS_FAVORABLE; - } else if (pageDiff <= 1) { - return Process.THREAD_PRIORITY_LOWEST; - } else { - return Process.THREAD_PRIORITY_LOWEST; - } - } - private int getSleepForPage(int page) { - int pageDiff = getWidgetPageLoadPriority(page); - return Math.max(0, pageDiff * sPageSleepDelay); - } - /** - * Creates and executes a new AsyncTask to load a page of widget previews. - */ - private void prepareLoadWidgetPreviewsTask(int page, ArrayList<Object> widgets, - int cellWidth, int cellHeight, int cellCountX) { - - // Prune all tasks that are no longer needed - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int taskPage = task.page; - if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || - taskPage > getAssociatedUpperPageBound(mCurrentPage)) { - task.cancel(false); - iter.remove(); - } else { - task.setThreadPriority(getThreadPriorityForPage(taskPage)); - } - } - - // We introduce a slight delay to order the loading of side pages so that we don't thrash - final int sleepMs = getSleepForPage(page); - AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - try { - Thread.sleep(sleepMs); - } catch (Exception e) {} - loadWidgetPreviewsInBackground(task, data); - } finally { - if (task.isCancelled()) { - data.cleanup(true); - } - } - } - }, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - mRunningTasks.remove(task); - if (task.isCancelled()) return; - // do cleanup inside onSyncWidgetPageItems - onSyncWidgetPageItems(data, false); - } - }, getWidgetPreviewLoader()); - - // Ensure that the task is appropriately prioritized and runs in parallel - AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page, - AsyncTaskPageData.Type.LoadWidgetPreviewData); - t.setThreadPriority(getThreadPriorityForPage(page)); - t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData); - mRunningTasks.add(t); - } - - /* - * Widgets PagedView implementation - */ - private void setupPage(PagedViewGridLayout layout) { - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - - Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel_dark); - if (bg != null) { - bg.setAlpha(mPageBackgroundsVisible ? 255 : 0); - layout.setBackground(bg); - } - layout.measure(widthSpec, heightSpec); - } - - public void syncWidgetPageItems(final int page, final boolean immediate) { - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - - final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - - // Calculate the dimensions of each cell we are giving to each widget - final ArrayList<Object> items = new ArrayList<Object>(); - int contentWidth = mContentWidth - layout.getPaddingLeft() - layout.getPaddingRight(); - final int cellWidth = contentWidth / mWidgetCountX; - int contentHeight = mContentHeight - layout.getPaddingTop() - layout.getPaddingBottom(); - - final int cellHeight = contentHeight / mWidgetCountY; - - // Prepare the set of widgets to load previews for in the background - int offset = page * numItemsPerPage; - for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) { - items.add(mWidgets.get(i)); - } - - // Prepopulate the pages with the other widget info, and fill in the previews later - layout.setColumnCount(layout.getCellCountX()); - for (int i = 0; i < items.size(); ++i) { - Object rawInfo = items.get(i); - PendingAddItemInfo createItemInfo = null; - PagedViewWidget widget = (PagedViewWidget) mLayoutInflater.inflate( - R.layout.apps_customize_widget, layout, false); - if (rawInfo instanceof AppWidgetProviderInfo) { - // Fill in the widget information - AppWidgetProviderInfo info = (AppWidgetProviderInfo) rawInfo; - createItemInfo = new PendingAddWidgetInfo(info, null, null); - - // Determine the widget spans and min resize spans. - int[] spanXY = Launcher.getSpanForWidget(mLauncher, info); - createItemInfo.spanX = spanXY[0]; - createItemInfo.spanY = spanXY[1]; - int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, info); - createItemInfo.minSpanX = minSpanXY[0]; - createItemInfo.minSpanY = minSpanXY[1]; - - widget.applyFromAppWidgetProviderInfo(info, -1, spanXY, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - widget.setShortPressListener(this); - } else if (rawInfo instanceof ResolveInfo) { - // Fill in the shortcuts information - ResolveInfo info = (ResolveInfo) rawInfo; - createItemInfo = new PendingAddShortcutInfo(info.activityInfo); - createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; - createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, - info.activityInfo.name); - widget.applyFromResolveInfo(mPackageManager, info, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - } - widget.setOnClickListener(this); - widget.setOnLongClickListener(this); - widget.setOnTouchListener(this); - widget.setOnKeyListener(this); - - // Layout each widget - int ix = i % mWidgetCountX; - int iy = i / mWidgetCountX; - - if (ix > 0) { - View border = widget.findViewById(R.id.left_border); - border.setVisibility(View.VISIBLE); - } - if (ix < mWidgetCountX - 1) { - View border = widget.findViewById(R.id.right_border); - border.setVisibility(View.VISIBLE); - } - - GridLayout.LayoutParams lp = new GridLayout.LayoutParams( - GridLayout.spec(iy, GridLayout.START), - GridLayout.spec(ix, GridLayout.TOP)); - lp.width = cellWidth; - lp.height = cellHeight; - lp.setGravity(Gravity.TOP | Gravity.START); - layout.addView(widget, lp); - } - - // wait until a call on onLayout to start loading, because - // PagedViewWidget.getPreviewSize() will return 0 if it hasn't been laid out - // TODO: can we do a measure/layout immediately? - layout.setOnLayoutListener(new Runnable() { - public void run() { - // Load the widget previews - int maxPreviewWidth = cellWidth; - int maxPreviewHeight = cellHeight; - if (layout.getChildCount() > 0) { - PagedViewWidget w = (PagedViewWidget) layout.getChildAt(0); - int[] maxSize = w.getPreviewSize(); - maxPreviewWidth = maxSize[0]; - maxPreviewHeight = maxSize[1]; - } - - getWidgetPreviewLoader().setPreviewSize( - maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout); - if (immediate) { - AsyncTaskPageData data = new AsyncTaskPageData(page, items, - maxPreviewWidth, maxPreviewHeight, null, null, getWidgetPreviewLoader()); - loadWidgetPreviewsInBackground(null, data); - onSyncWidgetPageItems(data, immediate); - } else { - if (mInTransition) { - mDeferredPrepareLoadWidgetPreviewsTasks.add(this); - } else { - prepareLoadWidgetPreviewsTask(page, items, - maxPreviewWidth, maxPreviewHeight, mWidgetCountX); - } - } - layout.setOnLayoutListener(null); - } - }); - } - private void loadWidgetPreviewsInBackground(AppsCustomizeAsyncTask task, - AsyncTaskPageData data) { - // loadWidgetPreviewsInBackground can be called without a task to load a set of widget - // previews synchronously - if (task != null) { - // Ensure that this task starts running at the correct priority - task.syncThreadPriority(); - } - - // Load each of the widget/shortcut previews - ArrayList<Object> items = data.items; - ArrayList<Bitmap> images = data.generatedImages; - int count = items.size(); - for (int i = 0; i < count; ++i) { - if (task != null) { - // Ensure we haven't been cancelled yet - if (task.isCancelled()) break; - // Before work on each item, ensure that this task is running at the correct - // priority - task.syncThreadPriority(); - } - - images.add(getWidgetPreviewLoader().getPreview(items.get(i))); - } - } - - private void onSyncWidgetPageItems(AsyncTaskPageData data, boolean immediatelySyncItems) { - if (!immediatelySyncItems && mInTransition) { - mDeferredSyncWidgetPageItems.add(data); - return; - } - try { - int page = data.page; - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - - ArrayList<Object> items = data.items; - int count = items.size(); - for (int i = 0; i < count; ++i) { - PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); - if (widget != null) { - Bitmap preview = data.generatedImages.get(i); - widget.applyPreview(new FastBitmapDrawable(preview), i); - } - } - - enableHwLayersOnVisiblePages(); - - // Update all thread priorities - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } - } finally { - data.cleanup(false); - } - } - - @Override - public void syncPages() { - disablePagedViewAnimations(); - - removeAllViews(); - cancelAllTasks(); - - Context context = getContext(); - if (mContentType == ContentType.Applications) { - for (int i = 0; i < mNumAppsPages; ++i) { - AppsCustomizeCellLayout layout = new AppsCustomizeCellLayout(context); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else if (mContentType == ContentType.Widgets) { - for (int j = 0; j < mNumWidgetPages; ++j) { - PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, - mWidgetCountY); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - - enablePagedViewAnimations(); - } - - @Override - public void syncPageItems(int page, boolean immediate) { - if (mContentType == ContentType.Widgets) { - syncWidgetPageItems(page, immediate); - } else { - syncAppsPageItems(page, immediate); - } - } - - // We want our pages to be z-ordered such that the further a page is to the left, the higher - // it is in the z-order. This is important to insure touch events are handled correctly. - View getPageAt(int index) { - return getChildAt(indexToPage(index)); - } - - @Override - protected int indexToPage(int index) { - return getChildCount() - index - 1; - } - - // In apps customize, we have a scrolling effect which emulates pulling cards off of a stack. - @Override - protected void screenScrolled(int screenCenter) { - super.screenScrolled(screenCenter); - enableHwLayersOnVisiblePages(); - } - - private void enableHwLayersOnVisiblePages() { - final int screenCount = getChildCount(); - - getVisiblePages(mTempVisiblePagesRange); - int leftScreen = mTempVisiblePagesRange[0]; - int rightScreen = mTempVisiblePagesRange[1]; - int forceDrawScreen = -1; - if (leftScreen == rightScreen) { - // make sure we're caching at least two pages always - if (rightScreen < screenCount - 1) { - rightScreen++; - forceDrawScreen = rightScreen; - } else if (leftScreen > 0) { - leftScreen--; - forceDrawScreen = leftScreen; - } - } else { - forceDrawScreen = leftScreen + 1; - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - if (!(leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout)))) { - layout.setLayerType(LAYER_TYPE_NONE, null); - } - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - - if (leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout))) { - if (layout.getLayerType() != LAYER_TYPE_HARDWARE) { - layout.setLayerType(LAYER_TYPE_HARDWARE, null); - } - } - } - } - - protected void overScroll(float amount) { - dampedOverScroll(amount); - } - - /** - * Used by the parent to get the content width to set the tab bar to - * @return - */ - public int getPageContentWidth() { - return mContentWidth; - } - - @Override - protected void onPageEndMoving() { - super.onPageEndMoving(); - mForceDrawAllChildrenNextFrame = true; - // We reset the save index when we change pages so that it will be recalculated on next - // rotation - mSaveInstanceStateItemIndex = -1; - } - - /* - * AllAppsView implementation - */ - public void setup(Launcher launcher, DragController dragController) { - mLauncher = launcher; - mDragController = dragController; - } - - /** - * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can - * appropriately determine when to invalidate the PagedView page data. In cases where the data - * has yet to be set, we can requestLayout() and wait for onDataReady() to be called in the - * next onMeasure() pass, which will trigger an invalidatePageData() itself. - */ - private void invalidateOnDataChange() { - if (!isDataReady()) { - // The next layout pass will trigger data-ready if both widgets and apps are set, so - // request a layout to trigger the page data when ready. - requestLayout(); - } else { - cancelAllTasks(); - invalidatePageData(); - } - } - - public void setApps(ArrayList<AppInfo> list) { - if (!LauncherAppState.isDisableAllApps()) { - mApps = list; - Collections.sort(mApps, LauncherModel.getAppNameComparator()); - updatePageCountsAndInvalidateData(); - } - } - private void addAppsWithoutInvalidate(ArrayList<AppInfo> list) { - // We add it in place, in alphabetical order - int count = list.size(); - for (int i = 0; i < count; ++i) { - AppInfo info = list.get(i); - int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator()); - if (index < 0) { - mApps.add(-(index + 1), info); - } - } - } - public void addApps(ArrayList<AppInfo> list) { - if (!LauncherAppState.isDisableAllApps()) { - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - private int findAppByComponent(List<AppInfo> list, AppInfo item) { - ComponentName removeComponent = item.intent.getComponent(); - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - if (info.user.equals(item.user) - && info.intent.getComponent().equals(removeComponent)) { - return i; - } - } - return -1; - } - private void removeAppsWithoutInvalidate(ArrayList<AppInfo> list) { - // loop through all the apps and remove apps that have the same component - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - int removeIndex = findAppByComponent(mApps, info); - if (removeIndex > -1) { - mApps.remove(removeIndex); - } - } - } - public void removeApps(ArrayList<AppInfo> appInfos) { - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(appInfos); - updatePageCountsAndInvalidateData(); - } - } - public void updateApps(ArrayList<AppInfo> list) { - // We remove and re-add the updated applications list because it's properties may have - // changed (ie. the title), and this will ensure that the items will be in their proper - // place in the list. - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(list); - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - - public void reset() { - // If we have reset, then we should not continue to restore the previous state - mSaveInstanceStateItemIndex = -1; - - if (mContentType != ContentType.Applications) { - setContentType(ContentType.Applications); - } - - if (mCurrentPage != 0) { - invalidatePageData(0); - } - } - - private AppsCustomizeTabHost getTabHost() { - return (AppsCustomizeTabHost) mLauncher.findViewById(R.id.apps_customize_pane); - } - - public void dumpState() { - // TODO: Dump information related to current list of Applications, Widgets, etc. - AppInfo.dumpApplicationInfoList(TAG, "mApps", mApps); - dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets); - } - - private void dumpAppWidgetProviderInfoList(String tag, String label, - ArrayList<Object> list) { - Log.d(tag, label + " size=" + list.size()); - for (Object i: list) { - if (i instanceof AppWidgetProviderInfo) { - AppWidgetProviderInfo info = (AppWidgetProviderInfo) i; - Log.d(tag, " label=\"" + info.label + "\" previewImage=" + info.previewImage - + " resizeMode=" + info.resizeMode + " configure=" + info.configure - + " initialLayout=" + info.initialLayout - + " minWidth=" + info.minWidth + " minHeight=" + info.minHeight); - } else if (i instanceof ResolveInfo) { - ResolveInfo info = (ResolveInfo) i; - Log.d(tag, " label=\"" + info.loadLabel(mPackageManager) + "\" icon=" - + info.icon); - } - } - } - - public void surrender() { - // TODO: If we are in the middle of any process (ie. for holographic outlines, etc) we - // should stop this now. - - // Stop all background tasks - cancelAllTasks(); - } - - /* - * We load an extra page on each side to prevent flashes from scrolling and loading of the - * widget previews in the background with the AsyncTasks. - */ - final static int sLookBehindPageCount = 2; - final static int sLookAheadPageCount = 2; - protected int getAssociatedLowerPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMinIndex = Math.max(Math.min(page - sLookBehindPageCount, count - windowSize), 0); - return windowMinIndex; - } - protected int getAssociatedUpperPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMaxIndex = Math.min(Math.max(page + sLookAheadPageCount, windowSize - 1), - count - 1); - return windowMaxIndex; - } - - protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - int stringId = R.string.default_scroll_format; - int count = 0; - - if (mContentType == ContentType.Applications) { - stringId = R.string.apps_customize_apps_scroll_format; - count = mNumAppsPages; - } else if (mContentType == ContentType.Widgets) { - stringId = R.string.apps_customize_widgets_scroll_format; - count = mNumWidgetPages; - } else { - throw new RuntimeException("Invalid ContentType"); - } - - return String.format(getContext().getString(stringId), page + 1, count); - } -} diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java deleted file mode 100644 index a2717126d..000000000 --- a/src/com/android/launcher3/AppsCustomizeTabHost.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 2011 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.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.FrameLayout; - -public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransitionable, Insettable { - static final String LOG_TAG = "AppsCustomizeTabHost"; - - private static final String APPS_TAB_TAG = "APPS"; - private static final String WIDGETS_TAB_TAG = "WIDGETS"; - - private AppsCustomizePagedView mPagedView; - private View mContent; - private boolean mInTransition = false; - - private final Rect mInsets = new Rect(); - - public AppsCustomizeTabHost(Context context, AttributeSet attrs) { - super(context, attrs); - } - - /** - * Convenience methods to select specific tabs. We want to set the content type immediately - * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view - * reflects the new content (but doesn't do the animation and logic associated with changing - * tabs manually). - */ - void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { - mPagedView.setContentType(type); - } - - public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { - setContentTypeImmediate(type); - } - - @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - LayoutParams flp = (LayoutParams) mContent.getLayoutParams(); - flp.topMargin = insets.top; - flp.bottomMargin = insets.bottom; - flp.leftMargin = insets.left; - flp.rightMargin = insets.right; - mContent.setLayoutParams(flp); - } - - /** - * Setup the tab host and create all necessary tabs. - */ - @Override - protected void onFinishInflate() { - mPagedView = (AppsCustomizePagedView) findViewById(R.id.apps_customize_pane_content); - mContent = findViewById(R.id.content); - } - - public String getContentTag() { - return getTabTagForContentType(mPagedView.getContentType()); - } - - /** - * Returns the content type for the specified tab tag. - */ - public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) { - if (tag.equals(APPS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Applications; - } else if (tag.equals(WIDGETS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Widgets; - } - return AppsCustomizePagedView.ContentType.Applications; - } - - /** - * Returns the tab tag for a given content type. - */ - public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) { - if (type == AppsCustomizePagedView.ContentType.Applications) { - return APPS_TAB_TAG; - } else if (type == AppsCustomizePagedView.ContentType.Widgets) { - return WIDGETS_TAB_TAG; - } - return APPS_TAB_TAG; - } - - /** - * Disable focus on anything under this view in the hierarchy if we are not visible. - */ - @Override - public int getDescendantFocusability() { - if (getVisibility() != View.VISIBLE) { - return ViewGroup.FOCUS_BLOCK_DESCENDANTS; - } - return super.getDescendantFocusability(); - } - - void reset() { - // Reset immediately - mPagedView.reset(); - } - - void trimMemory() { - mPagedView.trimMemory(); - } - - public void onWindowVisible() { - if (getVisibility() == VISIBLE) { - mContent.setVisibility(VISIBLE); - // We unload the widget previews when the UI is hidden, so need to reload pages - // Load the current page synchronously, and the neighboring pages asynchronously - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage(), true); - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - } - } - @Override - public ViewGroup getContent() { - return mPagedView; - } - - public boolean isInTransition() { - return mInTransition; - } - - /* LauncherTransitionable overrides */ - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionPrepare(l, animated, toWorkspace); - mInTransition = true; - - if (toWorkspace) { - // Going from All Apps -> Workspace - setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); - } else { - // Going from Workspace -> All Apps - mContent.setVisibility(VISIBLE); - - // Make sure the current page is loaded (we start loading the side pages after the - // transition to prevent slowing down the animation) - // TODO: revisit this - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionStart(l, animated, toWorkspace); - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - mPagedView.onLauncherTransitionStep(l, t); - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionEnd(l, animated, toWorkspace); - mInTransition = false; - - if (!toWorkspace) { - // Make sure adjacent pages are loaded (we wait until after the transition to - // prevent slowing down the animation) - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - - // Opening apps, need to announce what page we are on. - AccessibilityManager am = (AccessibilityManager) - getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - if (am.isEnabled()) { - // Notify the user when the page changes - announceForAccessibility(mPagedView.getCurrentPageDescription()); - } - - // Going from Workspace -> All Apps - // NOTE: We should do this at the end since we check visibility state in some of the - // cling initialization/dismiss code above. - setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); - } - } - - private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { - ViewGroup parent = (ViewGroup) getParent(); - if (parent == null) return; - - View overviewPanel = ((Launcher) getContext()).getOverviewPanel(); - final int count = parent.getChildCount(); - if (!isChildrenDrawingOrderEnabled()) { - for (int i = 0; i < count; i++) { - final View child = parent.getChildAt(i); - if (child == this) { - break; - } else { - if (child.getVisibility() == GONE || child == overviewPanel) { - continue; - } - child.setVisibility(visibility); - } - } - } else { - throw new RuntimeException("Failed; can't get z-order of views"); - } - } -} diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index a5d22286d..99a98ddac 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -37,6 +37,7 @@ import android.util.Patterns; import com.android.launcher3.LauncherProvider.SqlArguments; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -44,6 +45,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; /** * Layout parsing code for auto installs layout @@ -56,6 +58,11 @@ public class AutoInstallsLayout { static final String ACTION_LAUNCHER_CUSTOMIZATION = "android.autoinstalls.config.action.PLAY_AUTO_INSTALL"; + /** + * Layout resource which also includes grid size and hotseat count, e.g., default_layout_6x6_h5 + */ + private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s"; + private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d"; private static final String LAYOUT_RES = "default_layout"; static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost, @@ -65,19 +72,44 @@ public class AutoInstallsLayout { if (customizationApkInfo == null) { return null; } + return get(context, customizationApkInfo.first, customizationApkInfo.second, + appWidgetHost, callback); + } + + static AutoInstallsLayout get(Context context, String pkg, Resources targetRes, + AppWidgetHost appWidgetHost, LayoutParserCallback callback) { + InvariantDeviceProfile grid = LauncherAppState.getInstance().getInvariantDeviceProfile(); + + // Try with grid size and hotseat count + String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT, + (int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons); + int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + + // Try with only grid size + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + + " not found. Trying layout without hosteat"); + layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES, + (int) grid.numColumns, (int) grid.numRows); + layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + } + + // Try the default layout + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout"); + layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg); + } - String pkg = customizationApkInfo.first; - Resources res = customizationApkInfo.second; - int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg); if (layoutId == 0) { Log.e(TAG, "Layout definition not found in package: " + pkg); return null; } - return new AutoInstallsLayout(context, appWidgetHost, callback, res, layoutId, + return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId, TAG_WORKSPACE); } // Object Tags + private static final String TAG_INCLUDE = "include"; private static final String TAG_WORKSPACE = "workspace"; private static final String TAG_APP_ICON = "appicon"; private static final String TAG_AUTO_INSTALL = "autoinstall"; @@ -100,6 +132,9 @@ public class AutoInstallsLayout { private static final String ATTR_ICON = "icon"; private static final String ATTR_URL = "url"; + // Attrs for "Include" + private static final String ATTR_WORKSPACE = "workspace"; + // Style attrs -- "Extra" private static final String ATTR_KEY = "key"; private static final String ATTR_VALUE = "value"; @@ -110,9 +145,9 @@ public class AutoInstallsLayout { private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; - private final Context mContext; - private final AppWidgetHost mAppWidgetHost; - private final LayoutParserCallback mCallback; + @Thunk final Context mContext; + @Thunk final AppWidgetHost mAppWidgetHost; + protected final LayoutParserCallback mCallback; protected final PackageManager mPackageManager; protected final Resources mSourceRes; @@ -121,14 +156,21 @@ public class AutoInstallsLayout { private final int mHotseatAllAppsRank; private final long[] mTemp = new long[2]; - private final ContentValues mValues; - private final String mRootTag; + @Thunk final ContentValues mValues; + protected final String mRootTag; protected SQLiteDatabase mDb; public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources res, int layoutId, String rootTag) { + this(context, appWidgetHost, callback, res, layoutId, rootTag, + LauncherAppState.getInstance().getInvariantDeviceProfile().hotseatAllAppsRank); + } + + public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources res, + int layoutId, String rootTag, int hotseatAllAppsRank) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; @@ -139,8 +181,7 @@ public class AutoInstallsLayout { mSourceRes = res; mLayoutId = layoutId; - mHotseatAllAppsRank = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; + mHotseatAllAppsRank = hotseatAllAppsRank; } /** @@ -202,6 +243,17 @@ public class AutoInstallsLayout { HashMap<String, TagParser> tagParserMap, ArrayList<Long> screenIds) throws XmlPullParserException, IOException { + + if (TAG_INCLUDE.equals(parser.getName())) { + final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); + if (resId != 0) { + // recursively load some more favorites, why not? + return parseLayout(resId, screenIds); + } else { + return 0; + } + } + mValues.clear(); parseContainerAndScreen(parser, mTemp); final long container = mTemp[0]; @@ -528,6 +580,7 @@ public class AutoInstallsLayout { int type; int folderDepth = parser.getDepth(); + int rank = 0; while ((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > folderDepth) { if (type != XmlPullParser.START_TAG) { @@ -535,12 +588,14 @@ public class AutoInstallsLayout { } mValues.clear(); mValues.put(Favorites.CONTAINER, folderId); + mValues.put(Favorites.RANK, rank); TagParser tagParser = mFolderElements.get(parser.getName()); if (tagParser != null) { final long id = tagParser.parseAndAdd(parser); if (id >= 0) { folderItems.add(id); + rank++; } } else { throw new RuntimeException("Invalid folder item " + parser.getName()); @@ -554,7 +609,7 @@ public class AutoInstallsLayout { // failed to add, and less than 2 were actually added if (folderItems.size() < 2) { // Delete the folder - Uri uri = Favorites.getContentUri(folderId, false); + Uri uri = Favorites.getContentUri(folderId); SqlArguments args = new SqlArguments(uri, null, null); mDb.delete(args.table, args.where, args.args); addedId = -1; @@ -627,7 +682,7 @@ public class AutoInstallsLayout { long insertAndCheck(SQLiteDatabase db, ContentValues values); } - private static void copyInteger(ContentValues from, ContentValues to, String key) { + @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) { to.put(key, from.getAsInteger(key)); } } diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java new file mode 100644 index 000000000..c8de9df10 --- /dev/null +++ b/src/com/android/launcher3/BaseContainerView.java @@ -0,0 +1,137 @@ +/* + * 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.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.LinearLayout; + +/** + * A base container view, which supports resizing. + */ +public abstract class BaseContainerView extends LinearLayout implements Insettable { + + private final static String TAG = "BaseContainerView"; + + // The window insets + private Rect mInsets = new Rect(); + // The bounds of the search bar. Only the left, top, right are used to inset the + // search bar and the height is determined by the measurement of the layout + private Rect mFixedSearchBarBounds = new Rect(); + // The bounds of the container + protected Rect mContentBounds = new Rect(); + // The padding to apply to the container to achieve the bounds + protected Rect mContentPadding = new Rect(); + // The inset to apply to the edges and between the search bar and the container + private int mContainerBoundsInset; + private boolean mHasSearchBar; + + public BaseContainerView(Context context) { + this(context, null); + } + + public BaseContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset); + } + + @Override + final public void setInsets(Rect insets) { + mInsets.set(insets); + updateBackgroundAndPaddings(); + } + + protected void setHasSearchBar() { + mHasSearchBar = true; + } + + /** + * Sets the search bar bounds for this container view to match. + */ + final public void setSearchBarBounds(Rect bounds) { + if (LauncherAppState.isDogfoodBuild() && !isValidSearchBarBounds(bounds)) { + Log.e(TAG, "Invalid search bar bounds: " + bounds); + } + + mFixedSearchBarBounds.set(bounds); + + // Post the updates since they can trigger a relayout, and this call can be triggered from + // a layout pass itself. + post(new Runnable() { + @Override + public void run() { + updateBackgroundAndPaddings(); + } + }); + } + + /** + * Update the backgrounds and padding in response to a change in the bounds or insets. + */ + protected void updateBackgroundAndPaddings() { + Rect padding; + Rect searchBarBounds = new Rect(mFixedSearchBarBounds); + if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { + // Use the default bounds + padding = new Rect(mInsets.left + mContainerBoundsInset, + (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), + mInsets.right + mContainerBoundsInset, + mInsets.bottom + mContainerBoundsInset); + + // Special case -- we have the search bar, but no specific bounds, so just give it + // the inset bounds without a height. + searchBarBounds.set(mInsets.left + mContainerBoundsInset, + mInsets.top + mContainerBoundsInset, + getMeasuredWidth() - (mInsets.right + mContainerBoundsInset), 0); + } else { + // Use the search bounds, if there is a search bar, the bounds will contain + // the offsets for the insets so we can ignore that + padding = new Rect(mFixedSearchBarBounds.left, + (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), + getMeasuredWidth() - mFixedSearchBarBounds.right, + mInsets.bottom + mContainerBoundsInset); + } + if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mFixedSearchBarBounds)) { + mContentPadding.set(padding); + mContentBounds.set(padding.left, padding.top, + getMeasuredWidth() - padding.right, + getMeasuredHeight() - padding.bottom); + mFixedSearchBarBounds.set(searchBarBounds); + onUpdateBackgroundAndPaddings(mFixedSearchBarBounds, padding); + } + } + + /** + * To be implemented by container views to update themselves when the bounds changes. + */ + protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding); + + /** + * Returns whether the search bar bounds we got are considered valid. + */ + private boolean isValidSearchBarBounds(Rect searchBarBounds) { + return !searchBarBounds.isEmpty() && + searchBarBounds.right <= getMeasuredWidth() && + searchBarBounds.bottom <= getMeasuredHeight(); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java new file mode 100644 index 000000000..0fae427e8 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -0,0 +1,285 @@ +/* + * 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.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import com.android.launcher3.util.Thunk; + + +/** + * A base {@link RecyclerView}, which does the following: + * <ul> + * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold. + * <li> Enable fast scroller. + * </ul> + */ +public abstract class BaseRecyclerView extends RecyclerView + implements RecyclerView.OnItemTouchListener { + + private static final int SCROLL_DELTA_THRESHOLD_DP = 4; + + /** Keeps the last known scrolling delta/velocity along y-axis. */ + @Thunk int mDy = 0; + private float mDeltaThreshold; + + /** + * The current scroll state of the recycler view. We use this in onUpdateScrollbar() + * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so + * that we can calculate what the scroll bar looks like, and where to jump to from the fast + * scroller. + */ + public static class ScrollPositionState { + // The index of the first visible row + public int rowIndex; + // The offset of the first visible row + public int rowTopOffset; + // The height of a given row (they are currently all the same height) + public int rowHeight; + } + + protected BaseRecyclerViewFastScrollBar mScrollbar; + + private int mDownX; + private int mDownY; + private int mLastY; + protected Rect mBackgroundPadding = new Rect(); + + public BaseRecyclerView(Context context) { + this(context, null); + } + + public BaseRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; + mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); + + ScrollListener listener = new ScrollListener(); + setOnScrollListener(listener); + } + + private class ScrollListener extends OnScrollListener { + public ScrollListener() { + // Do nothing + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mDy = dy; + + // TODO(winsonc): If we want to animate the section heads while scrolling, we can + // initiate that here if the recycler view scroll state is not + // RecyclerView.SCROLL_STATE_IDLE. + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. + */ + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + handleTouchEvent(ev); + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + private boolean handleTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = x; + mDownY = mLastY = y; + if (shouldStopScroll(ev)) { + stopScroll(); + } + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + case MotionEvent.ACTION_MOVE: + mLastY = y; + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFastScrollCompleted(); + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + } + return mScrollbar.isDragging(); + } + + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS + } + + /** + * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. + */ + protected boolean shouldStopScroll(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if ((Math.abs(mDy) < mDeltaThreshold && + getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { + // now the touch events are being passed to the {@link WidgetCell} until the + // touch sequence goes over the touch slop. + return true; + } + } + return false; + } + + public void updateBackgroundPadding(Rect padding) { + mBackgroundPadding.set(padding); + } + + public Rect getBackgroundPadding() { + return mBackgroundPadding; + } + + /** + * Returns the scroll bar width when the user is scrolling. + */ + public int getMaxScrollbarWidth() { + return mScrollbar.getThumbMaxWidth(); + } + + /** + * Returns the available scroll height: + * AvailableScrollHeight = Total height of the all items - last page height + * + * This assumes that all rows are the same height. + * + * @param yOffset the offset from the top of the recycler view to start tracking. + */ + protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom(); + int availableScrollHeight = scrollHeight - visibleHeight; + return availableScrollHeight; + } + + /** + * Returns the available scroll bar height: + * AvailableScrollBarHeight = Total height of the visible view - thumb height + */ + protected int getAvailableScrollBarHeight() { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + return availableScrollBarHeight; + } + + /** + * Returns the track color (ignoring alpha), can be overridden by each subclass. + */ + public int getFastScrollerTrackColor(int defaultTrackColor) { + return defaultTrackColor; + } + + /** + * Returns the inactive thumb color, can be overridden by each subclass. + */ + public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { + return defaultInactiveThumbColor; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + onUpdateScrollbar(); + mScrollbar.draw(canvas); + } + + /** + * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does + * this by mapping the available scroll area of the recycler view to the available space for the + * scroll bar. + * + * @param scrollPosState the current scroll position + * @param rowCount the number of rows, used to calculate the total scroll height (assumes that + * all rows are the same height) + * @param yOffset the offset to start tracking in the recycler view (only used for all apps) + */ + protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, + int rowCount, int yOffset) { + int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight, + yOffset); + int availableScrollBarHeight = getAvailableScrollBarHeight(); + + // Only show the scrollbar if there is height to be scrolled + if (availableScrollHeight <= 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Calculate the current scroll position, the scrollY of the recycler view accounts for the + // view padding, while the scrollBarY is drawn right up to the background padding (ignoring + // padding) + int scrollY = getPaddingTop() + yOffset + + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int scrollBarY = mBackgroundPadding.top + + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); + + // Calculate the position and size of the scroll bar + int scrollBarX; + if (Utilities.isRtl(getResources())) { + scrollBarX = mBackgroundPadding.left; + } else { + scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth(); + } + mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + * <p>Override in each subclass of this base class. + */ + public abstract String scrollToPositionAtProgress(float touchFraction); + + /** + * Updates the bounds for the scrollbar. + * <p>Override in each subclass of this base class. + */ + public abstract void onUpdateScrollbar(); + + /** + * <p>Override in each subclass of this base class. + */ + public void onFastScrollCompleted() {} +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java new file mode 100644 index 000000000..2c4184dc4 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -0,0 +1,235 @@ +/* + * 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.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.util.Thunk; + +/** + * The track and scrollbar that shows when you scroll the list. + */ +public class BaseRecyclerViewFastScrollBar { + + public interface FastScrollFocusableView { + void setFastScrollFocused(boolean focused, boolean animated); + } + + private final static int MAX_TRACK_ALPHA = 30; + private final static int SCROLL_BAR_VIS_DURATION = 150; + + @Thunk BaseRecyclerView mRv; + private BaseRecyclerViewFastScrollPopup mPopup; + + private AnimatorSet mScrollbarAnimator; + + private int mThumbInactiveColor; + private int mThumbActiveColor; + @Thunk Point mThumbOffset = new Point(-1, -1); + @Thunk Paint mThumbPaint; + private Paint mTrackPaint; + private int mThumbMinWidth; + private int mThumbMaxWidth; + @Thunk int mThumbWidth; + @Thunk int mThumbHeight; + // The inset is the buffer around which a point will still register as a click on the scrollbar + private int mTouchInset; + private boolean mIsDragging; + + // This is the offset from the top of the scrollbar when the user first starts touching. To + // prevent jumping, this offset is applied as the user scrolls. + private int mTouchOffset; + + private Rect mInvalidateRect = new Rect(); + private Rect mTmpRect = new Rect(); + + public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) { + mRv = rv; + mPopup = new BaseRecyclerViewFastScrollPopup(rv, res); + mTrackPaint = new Paint(); + mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK)); + mTrackPaint.setAlpha(0); + mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor( + res.getColor(R.color.container_fastscroll_thumb_inactive_color)); + mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color); + mThumbPaint = new Paint(); + mThumbPaint.setColor(mThumbInactiveColor); + mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width); + mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width); + mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height); + mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset); + } + + public void setScrollbarThumbOffset(int x, int y) { + if (mThumbOffset.x == x && mThumbOffset.y == y) { + return; + } + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mThumbOffset.set(x, y); + mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight())); + mRv.invalidate(mInvalidateRect); + } + + // Setter/getter for the search bar width for animations + public void setWidth(int width) { + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mThumbWidth = width; + mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight())); + mRv.invalidate(mInvalidateRect); + } + + public int getWidth() { + return mThumbWidth; + } + + // Setter/getter for the track background alpha for animations + public void setTrackAlpha(int alpha) { + mTrackPaint.setAlpha(alpha); + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mRv.invalidate(mInvalidateRect); + } + + public int getTrackAlpha() { + return mTrackPaint.getAlpha(); + } + + public int getThumbHeight() { + return mThumbHeight; + } + + public int getThumbMaxWidth() { + return mThumbMaxWidth; + } + + public boolean isDragging() { + return mIsDragging; + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) { + ViewConfiguration config = ViewConfiguration.get(mRv.getContext()); + + int action = ev.getAction(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + if (isNearPoint(downX, downY)) { + mTouchOffset = downY - mThumbOffset.y; + } + break; + case MotionEvent.ACTION_MOVE: + // Check if we should start scrolling + if (!mIsDragging && isNearPoint(downX, downY) && + Math.abs(y - downY) > config.getScaledTouchSlop()) { + mRv.getParent().requestDisallowInterceptTouchEvent(true); + mIsDragging = true; + mTouchOffset += (lastY - downY); + mPopup.animateVisibility(true); + animateScrollbar(true); + } + if (mIsDragging) { + // Update the fastscroller section name at this touch position + int top = mRv.getBackgroundPadding().top; + int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight; + float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset)); + String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) / + (bottom - top)); + mPopup.setSectionName(sectionName); + mPopup.animateVisibility(!sectionName.isEmpty()); + mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchOffset = 0; + if (mIsDragging) { + mIsDragging = false; + mPopup.animateVisibility(false); + animateScrollbar(false); + } + break; + } + } + + public void draw(Canvas canvas) { + if (mThumbOffset.x < 0 || mThumbOffset.y < 0) { + return; + } + + // Draw the scroll bar track and thumb + if (mTrackPaint.getAlpha() > 0) { + canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint); + } + canvas.drawRect(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight, mThumbPaint); + + // Draw the popup + mPopup.draw(canvas); + } + + /** + * Animates the width and color of the scrollbar. + */ + private void animateScrollbar(boolean isScrolling) { + if (mScrollbarAnimator != null) { + mScrollbarAnimator.cancel(); + } + ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha", + isScrolling ? MAX_TRACK_ALPHA : 0); + ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width", + isScrolling ? mThumbMaxWidth : mThumbMinWidth); + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), + mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + mThumbPaint.setColor((Integer) animator.getAnimatedValue()); + mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight); + } + }); + mScrollbarAnimator = new AnimatorSet(); + mScrollbarAnimator.playTogether(trackAlphaAnim, thumbWidthAnim, colorAnimation); + mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION); + mScrollbarAnimator.start(); + } + + /** + * Returns whether the specified points are near the scroll bar bounds. + */ + private boolean isNearPoint(int x, int y) { + mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight); + mTmpRect.inset(mTouchInset, mTouchInset); + return mTmpRect.contains(x, y); + } +} diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java new file mode 100644 index 000000000..aeeb5156d --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java @@ -0,0 +1,160 @@ +/* + * 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.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * The fast scroller popup that shows the section name the list will jump to. + */ +public class BaseRecyclerViewFastScrollPopup { + + private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; + + private Resources mRes; + private BaseRecyclerView mRv; + + private Drawable mBg; + // The absolute bounds of the fast scroller bg + private Rect mBgBounds = new Rect(); + private int mBgOriginalSize; + private Rect mInvalidateRect = new Rect(); + private Rect mTmpRect = new Rect(); + + private String mSectionName; + private Paint mTextPaint; + private Rect mTextBounds = new Rect(); + private float mAlpha; + + private Animator mAlphaAnimator; + private boolean mVisible; + + public BaseRecyclerViewFastScrollPopup(BaseRecyclerView rv, Resources res) { + mRes = res; + mRv = rv; + mBgOriginalSize = res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_size); + mBg = res.getDrawable(R.drawable.container_fastscroll_popup_bg); + mBg.setBounds(0, 0, mBgOriginalSize, mBgOriginalSize); + mTextPaint = new Paint(); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_text_size)); + } + + /** + * Sets the section name. + */ + public void setSectionName(String sectionName) { + if (!sectionName.equals(mSectionName)) { + mSectionName = sectionName; + mTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTextBounds); + // Update the width to use measureText since that is more accurate + mTextBounds.right = (int) (mTextBounds.left + mTextPaint.measureText(sectionName)); + } + } + + /** + * Updates the bounds for the fast scroller. + * @return the invalidation rect for this update. + */ + public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) { + mInvalidateRect.set(mBgBounds); + + if (isVisible()) { + // Calculate the dimensions and position of the fast scroller popup + int edgePadding = rv.getMaxScrollbarWidth(); + int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2; + int bgHeight = mBgOriginalSize; + int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding)); + if (Utilities.isRtl(mRes)) { + mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.right = mBgBounds.left + bgWidth; + } else { + mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right - + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.left = mBgBounds.right - bgWidth; + } + mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight); + mBgBounds.top = Math.max(edgePadding, + Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight)); + mBgBounds.bottom = mBgBounds.top + bgHeight; + } else { + mBgBounds.setEmpty(); + } + + // Combine the old and new fast scroller bounds to create the full invalidate rect + mInvalidateRect.union(mBgBounds); + return mInvalidateRect; + } + + /** + * Animates the visibility of the fast scroller popup. + */ + public void animateVisibility(boolean visible) { + if (mVisible != visible) { + mVisible = visible; + if (mAlphaAnimator != null) { + mAlphaAnimator.cancel(); + } + mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", visible ? 1f : 0f); + mAlphaAnimator.setDuration(visible ? 200 : 150); + mAlphaAnimator.start(); + } + } + + // Setter/getter for the popup alpha for animations + public void setAlpha(float alpha) { + mAlpha = alpha; + mRv.invalidate(mBgBounds); + } + + public float getAlpha() { + return mAlpha; + } + + public int getHeight() { + return mBgOriginalSize; + } + + public void draw(Canvas c) { + if (isVisible()) { + // Draw the fast scroller popup + int restoreCount = c.save(Canvas.MATRIX_SAVE_FLAG); + c.translate(mBgBounds.left, mBgBounds.top); + mTmpRect.set(mBgBounds); + mTmpRect.offsetTo(0, 0); + mBg.setBounds(mTmpRect); + mBg.setAlpha((int) (mAlpha * 255)); + mBg.draw(c); + mTextPaint.setAlpha((int) (mAlpha * 255)); + c.drawText(mSectionName, (mBgBounds.width() - mTextBounds.width()) / 2, + mBgBounds.height() - (mBgBounds.height() - mTextBounds.height()) / 2, + mTextPaint); + c.restoreToCount(restoreCount); + } + } + + public boolean isVisible() { + return (mAlpha > 0f) && (mSectionName != null); + } +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 07f3045a5..a0be8ea2b 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -23,22 +25,30 @@ import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.widget.TextView; +import com.android.launcher3.IconCache.IconLoadRequest; +import com.android.launcher3.model.PackageItemInfo; /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan * because we want to make the bubble taller than the text and TextView's clip is * too aggressive. */ -public class BubbleTextView extends TextView { +public class BubbleTextView extends TextView + implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView { private static SparseArray<Theme> sPreloaderThemes = new SparseArray<Theme>(2); @@ -47,25 +57,47 @@ public class BubbleTextView extends TextView { private static final float SHADOW_Y_OFFSET = 2.0f; private static final int SHADOW_LARGE_COLOUR = 0xDD000000; private static final int SHADOW_SMALL_COLOUR = 0xCC000000; - static final float PADDING_V = 3.0f; - private HolographicOutlineHelper mOutlineHelper; + private static final int DISPLAY_WORKSPACE = 0; + private static final int DISPLAY_ALL_APPS = 1; + + private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; + private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; + private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; + private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; + private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; + private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; + + private final Launcher mLauncher; + private Drawable mIcon; + private final Drawable mBackground; + private final CheckLongPressHelper mLongPressHelper; + private final HolographicOutlineHelper mOutlineHelper; + private final StylusEventHelper mStylusEventHelper; + + private boolean mBackgroundSizeChanged; + private Bitmap mPressedBackground; private float mSlop; - private int mTextColor; + private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; - private boolean mIsTextVisible; - - // TODO: Remove custom background handling code, as no instance of BubbleTextView use any - // background. - private boolean mBackgroundSizeChanged; - private final Drawable mBackground; + private final boolean mLayoutHorizontal; + private final int mIconSize; + private int mTextColor; private boolean mStayPressed; private boolean mIgnorePressedStateChange; - private CheckLongPressHelper mLongPressHelper; + private boolean mDisableRelayout = false; + + private ObjectAnimator mFastScrollFocusAnimator; + private Paint mFastScrollFocusBgPaint; + private float mFastScrollFocusFraction; + private boolean mFastScrollFocused; + private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; + + private IconLoadRequest mIconLoadRequest; public BubbleTextView(Context context) { this(context, null, 0); @@ -77,10 +109,28 @@ public class BubbleTextView extends TextView { public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mLauncher = (Launcher) context; + DeviceProfile grid = mLauncher.getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleTextView, defStyle, 0); mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); + mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); + mDeferShadowGenerationOnTouch = + a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); + + int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); + int defaultIconSize = grid.iconSizePx; + if (display == DISPLAY_WORKSPACE) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); + } else if (display == DISPLAY_ALL_APPS) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); + defaultIconSize = grid.allAppsIconSizePx; + } + + mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, + defaultIconSize); + a.recycle(); if (mCustomShadowsEnabled) { @@ -90,45 +140,37 @@ public class BubbleTextView extends TextView { } else { mBackground = null; } - init(); - } - - public void onFinishInflate() { - super.onFinishInflate(); - - // Ensure we are using the right text size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - private void init() { mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); if (mCustomShadowsEnabled) { setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); } + + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) { + mFastScrollFocusBgPaint = new Paint(); + mFastScrollFocusBgPaint.setAntiAlias(true); + mFastScrollFocusBgPaint.setColor( + getResources().getColor(R.color.container_fastscroll_thumb_active_color)); + } + + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } - public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding) { - applyFromShortcutInfo(info, iconCache, setDefaultPadding, false); + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + applyFromShortcutInfo(info, iconCache, false); } public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding, boolean promiseStateChanged) { + boolean promiseStateChanged) { Bitmap b = info.getIcon(iconCache); - LauncherAppState app = LauncherAppState.getInstance(); - FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); + FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); iconDrawable.setGhostModeEnabled(info.isDisabled != 0); - setCompoundDrawables(null, iconDrawable, null, null); - if (setDefaultPadding) { - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); - } + setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } @@ -141,20 +183,37 @@ public class BubbleTextView extends TextView { } public void applyFromApplicationInfo(AppInfo info) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); + setText(info.title); + if (info.contentDescription != null) { + setContentDescription(info.contentDescription); + } + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + + // Verify high res immediately + verifyHighRes(); + } - Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap); - topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); - setCompoundDrawables(null, topDrawable, null, null); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); + public void applyFromPackageItemInfo(PackageItemInfo info) { + setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } - setTag(info); + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + + // Verify high res immediately + verifyHighRes(); } + /** + * Overrides the default long press timeout. + */ + public void setLongPressTimeout(int longPressTimeout) { + mLongPressHelper.setLongPressTimeout(longPressTimeout); + } @Override protected boolean setFrame(int left, int top, int right, int bottom) { @@ -186,10 +245,19 @@ public class BubbleTextView extends TextView { } } + /** Returns the icon for this view. */ + public Drawable getIcon() { + return mIcon; + } + + /** Returns whether the layout is horizontal. */ + public boolean isLayoutHorizontal() { + return mLayoutHorizontal; + } + private void updateIconState() { - Drawable top = getCompoundDrawables()[1]; - if (top instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed); + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); } } @@ -199,16 +267,25 @@ public class BubbleTextView extends TextView { // isPressed() on an ACTION_UP boolean result = super.onTouchEvent(event); + // Check for a stylus button press, if it occurs cancel any long press checks. + if (mStylusEventHelper.checkAndPerformStylusEvent(event)) { + mLongPressHelper.cancelLongPress(); + result = true; + } + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // So that the pressed outline is visible immediately on setStayPressed(), // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time // to create it) - if (mPressedBackground == null) { + if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) { mPressedBackground = mOutlineHelper.createMediumDropShadow(this); } - mLongPressHelper.postCheckForLongPress(); + // If we're in a stylus button press, don't check for long press. + if (!mStylusEventHelper.inStylusButtonPressed()) { + mLongPressHelper.postCheckForLongPress(); + } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: @@ -233,12 +310,17 @@ public class BubbleTextView extends TextView { mStayPressed = stayPressed; if (!stayPressed) { mPressedBackground = null; + } else { + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); + } } // Only show the shadow effect when persistent pressed state is set. - if (getParent() instanceof ShortcutAndWidgetContainer) { - CellLayout layout = (CellLayout) getParent().getParent(); - layout.setPressedIcon(this, mPressedBackground, mOutlineHelper.shadowBitmapPadding); + ViewParent parent = getParent(); + if (parent != null && parent.getParent() instanceof BubbleTextShadowHandler) { + ((BubbleTextShadowHandler) parent.getParent()).setPressedIcon( + this, mPressedBackground); } updateIconState(); @@ -278,7 +360,18 @@ public class BubbleTextView extends TextView { @Override public void draw(Canvas canvas) { if (!mCustomShadowsEnabled) { + // Draw the fast scroll focus bg if we have one + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG && + mFastScrollFocusFraction > 0f) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + int iconCenterX = getScrollX() + (getWidth() / 2); + int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2); + canvas.drawCircle(iconCenterX, iconCenterY, + mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint); + } + super.draw(canvas); + return; } @@ -325,10 +418,9 @@ public class BubbleTextView extends TextView { super.onAttachedToWindow(); if (mBackground != null) mBackground.setCallback(this); - Drawable top = getCompoundDrawables()[1]; - if (top instanceof PreloadIconDrawable) { - ((PreloadIconDrawable) top).applyTheme(getPreloaderTheme()); + if (mIcon instanceof PreloadIconDrawable) { + ((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme()); } mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -358,16 +450,6 @@ public class BubbleTextView extends TextView { } else { super.setTextColor(res.getColor(android.R.color.transparent)); } - mIsTextVisible = visible; - } - - public boolean isTextVisible() { - return mIsTextVisible; - } - - @Override - protected boolean onSetAlpha(int alpha) { - return true; } @Override @@ -385,15 +467,13 @@ public class BubbleTextView extends TextView { ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? info.getInstallProgress() : 0)) : 100; - Drawable[] drawables = getCompoundDrawables(); - Drawable top = drawables[1]; - if (top != null) { + if (mIcon != null) { final PreloadIconDrawable preloadDrawable; - if (top instanceof PreloadIconDrawable) { - preloadDrawable = (PreloadIconDrawable) top; + if (mIcon instanceof PreloadIconDrawable) { + preloadDrawable = (PreloadIconDrawable) mIcon; } else { - preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme()); - setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]); + preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme()); + setIcon(preloadDrawable, mIconSize); } preloadDrawable.setLevel(progressLevel); @@ -417,4 +497,132 @@ public class BubbleTextView extends TextView { } return theme; } + + /** + * Sets the icon for this view based on the layout direction. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private Drawable setIcon(Drawable icon, int iconSize) { + mIcon = icon; + if (iconSize != -1) { + mIcon.setBounds(0, 0, iconSize, iconSize); + } + if (mLayoutHorizontal) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + setCompoundDrawablesRelative(mIcon, null, null, null); + } else { + setCompoundDrawables(mIcon, null, null, null); + } + } else { + setCompoundDrawables(null, mIcon, null, null); + } + return icon; + } + + @Override + public void requestLayout() { + if (!mDisableRelayout) { + super.requestLayout(); + } + } + + /** + * Applies the item info if it is same as what the view is pointing to currently. + */ + public void reapplyItemInfo(final ItemInfo info) { + if (getTag() == info) { + mIconLoadRequest = null; + mDisableRelayout = true; + if (info instanceof AppInfo) { + applyFromApplicationInfo((AppInfo) info); + } else if (info instanceof ShortcutInfo) { + applyFromShortcutInfo((ShortcutInfo) info, + LauncherAppState.getInstance().getIconCache()); + } else if (info instanceof PackageItemInfo) { + applyFromPackageItemInfo((PackageItemInfo) info); + } + mDisableRelayout = false; + } + } + + /** + * Verifies that the current icon is high-res otherwise posts a request to load the icon. + */ + public void verifyHighRes() { + if (mIconLoadRequest != null) { + mIconLoadRequest.cancel(); + mIconLoadRequest = null; + } + if (getTag() instanceof AppInfo) { + AppInfo info = (AppInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } else if (getTag() instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } else if (getTag() instanceof PackageItemInfo) { + PackageItemInfo info = (PackageItemInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } + } + + // Setters & getters for the animation + public void setFastScrollFocus(float fraction) { + mFastScrollFocusFraction = fraction; + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { + setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); + setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); + } else { + invalidate(); + } + } + + public float getFastScrollFocus() { + return mFastScrollFocusFraction; + } + + @Override + public void setFastScrollFocused(final boolean focused, boolean animated) { + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { + return; + } + + if (mFastScrollFocused != focused) { + mFastScrollFocused = focused; + + if (animated) { + // Clean up the previous focus animator + if (mFastScrollFocusAnimator != null) { + mFastScrollFocusAnimator.cancel(); + } + mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", + focused ? 1f : 0f); + if (focused) { + mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); + } else { + mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); + } + mFastScrollFocusAnimator.setDuration(focused ? + FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); + mFastScrollFocusAnimator.start(); + } else { + mFastScrollFocusFraction = focused ? 1f : 0f; + } + } + } + + /** + * Interface to be implemented by the grand parent to allow click shadow effect. + */ + public static interface BubbleTextShadowHandler { + void setPressedIcon(BubbleTextView icon, Bitmap background); + } } diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 019f86c21..b7f89d02a 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -16,26 +16,41 @@ package com.android.launcher3; +import android.animation.AnimatorSet; +import android.animation.FloatArrayEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Resources; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; import android.widget.TextView; +import com.android.launcher3.util.Thunk; /** * Implements a DropTarget. */ -public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { +public abstract class ButtonDropTarget extends TextView + implements DropTarget, DragController.DragListener, OnClickListener { - protected final int mTransitionDuration; + private static int DRAG_VIEW_DROP_DURATION = 285; protected Launcher mLauncher; private int mBottomDragPadding; - protected TextView mText; protected SearchDropTargetBar mSearchDropTargetBar; /** Whether this drop target is active for the current drag */ @@ -44,72 +59,197 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro /** The paint applied to the drag view on hover */ protected int mHoverColor = 0; + protected ColorStateList mOriginalTextColor; + protected Drawable mDrawable; + + private AnimatorSet mCurrentColorAnim; + @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter; + + public ButtonDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mBottomDragPadding = getResources().getDimensionPixelSize(R.dimen.drop_target_drag_padding); + } - Resources r = getResources(); - mTransitionDuration = r.getInteger(R.integer.config_dropTargetBgTransitionDuration); - mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding); + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mOriginalTextColor = getTextColors(); + + // Remove the text in the Phone UI in landscape + DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile(); + if (grid.isVerticalBarLayout()) { + setText(""); + } } - void setLauncher(Launcher launcher) { - mLauncher = launcher; + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected void setDrawable(int resId) { + // We do not set the drawable in the xml as that inflates two drawables corresponding to + // drawableLeft and drawableStart. + mDrawable = getResources().getDrawable(resId); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); + } else { + setCompoundDrawablesWithIntrinsicBounds(mDrawable, null, null, null); + } } - public boolean acceptDrop(DragObject d) { - return false; + public void setLauncher(Launcher launcher) { + mLauncher = launcher; } public void setSearchDropTargetBar(SearchDropTargetBar searchDropTargetBar) { mSearchDropTargetBar = searchDropTargetBar; } - protected Drawable getCurrentDrawable() { - Drawable[] drawables = getCompoundDrawablesRelative(); - for (int i = 0; i < drawables.length; ++i) { - if (drawables[i] != null) { - return drawables[i]; + @Override + public void onFlingToDelete(DragObject d, PointF vec) { } + + @Override + public final void onDragEnter(DragObject d) { + d.dragView.setColor(mHoverColor); + if (Utilities.isLmpOrAbove()) { + animateTextColor(mHoverColor); + } else { + if (mCurrentFilter == null) { + mCurrentFilter = new ColorMatrix(); } + DragView.setColorScale(mHoverColor, mCurrentFilter); + mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + setTextColor(mHoverColor); } - return null; } - public void onDrop(DragObject d) { + @Override + public void onDragOver(DragObject d) { + // Do nothing } - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { - // Do nothing + protected void resetHoverColor() { + if (Utilities.isLmpOrAbove()) { + animateTextColor(mOriginalTextColor.getDefaultColor()); + } else { + mDrawable.setColorFilter(null); + setTextColor(mOriginalTextColor); + } } - public void onDragEnter(DragObject d) { - d.dragView.setColor(mHoverColor); + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void animateTextColor(int targetColor) { + if (mCurrentColorAnim != null) { + mCurrentColorAnim.cancel(); + } + + mCurrentColorAnim = new AnimatorSet(); + mCurrentColorAnim.setDuration(DragView.COLOR_CHANGE_DURATION); + + if (mSrcFilter == null) { + mSrcFilter = new ColorMatrix(); + mDstFilter = new ColorMatrix(); + mCurrentFilter = new ColorMatrix(); + } + + DragView.setColorScale(getTextColor(), mSrcFilter); + DragView.setColorScale(targetColor, mDstFilter); + ValueAnimator anim1 = ValueAnimator.ofObject( + new FloatArrayEvaluator(mCurrentFilter.getArray()), + mSrcFilter.getArray(), mDstFilter.getArray()); + anim1.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + invalidate(); + } + }); + + mCurrentColorAnim.play(anim1); + mCurrentColorAnim.play(ObjectAnimator.ofArgb(this, "textColor", targetColor)); + mCurrentColorAnim.start(); } - public void onDragOver(DragObject d) { - // Do nothing + @Override + public final void onDragExit(DragObject d) { + if (!d.dragComplete) { + d.dragView.setColor(0); + resetHoverColor(); + } else { + // Restore the hover color + d.dragView.setColor(mHoverColor); + } } - public void onDragExit(DragObject d) { - d.dragView.setColor(0); + @Override + public final void onDragStart(DragSource source, Object info, int dragAction) { + mActive = supportsDrop(source, info); + mDrawable.setColorFilter(null); + if (mCurrentColorAnim != null) { + mCurrentColorAnim.cancel(); + mCurrentColorAnim = null; + } + setTextColor(mOriginalTextColor); + ((ViewGroup) getParent()).setVisibility(mActive ? View.VISIBLE : View.GONE); } - public void onDragStart(DragSource source, Object info, int dragAction) { - // Do nothing + @Override + public final boolean acceptDrop(DragObject dragObject) { + return supportsDrop(dragObject.dragSource, dragObject.dragInfo); } + protected abstract boolean supportsDrop(DragSource source, Object info); + + @Override public boolean isDropEnabled() { return mActive; } + @Override public void onDragEnd() { - // Do nothing + mActive = false; + } + + /** + * On drop animate the dropView to the icon. + */ + @Override + public void onDrop(final DragObject d) { + final DragLayer dragLayer = mLauncher.getDragLayer(); + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + int width = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + width, height); + final float scale = (float) to.width() / from.width(); + mSearchDropTargetBar.deferOnDragEnd(); + + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + completeDrop(d); + mSearchDropTargetBar.onDragEnd(); + mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); + } + }; + dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, + DRAG_VIEW_DROP_DURATION, new DecelerateInterpolator(2), + new LinearInterpolator(), onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); } @Override + public void prepareAccessibilityDrop() { } + + @Thunk abstract void completeDrop(DragObject d); + + @Override public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { super.getHitRect(outRect); outRect.bottom += mBottomDragPadding; @@ -119,11 +259,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro outRect.offsetTo(coords[0], coords[1]); } - private boolean isRtl() { - return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - } - - Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { + protected Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { DragLayer dragLayer = mLauncher.getDragLayer(); // Find the rect to animate to (the view is center aligned) @@ -136,7 +272,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro final int left; final int right; - if (isRtl()) { + if (Utilities.isRtl(getResources())) { right = to.right - getPaddingRight(); left = right - width; } else { @@ -157,7 +293,26 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro return to; } + @Override public void getLocationInDragLayer(int[] loc) { mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } + + public void enableAccessibleDrag(boolean enable) { + setOnClickListener(enable ? this : null); + } + + protected String getAccessibilityDropConfirmation() { + return null; + } + + @Override + public void onClick(View v) { + LauncherAppState.getInstance().getAccessibilityDelegate() + .handleAccessibleDrop(this, null, getAccessibilityDropConfirmation()); + } + + public int getTextColor() { + return getTextColors().getDefaultColor(); + } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 0ff1ef4ad..809688712 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -22,6 +22,7 @@ import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -33,7 +34,10 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Build; import android.os.Parcelable; +import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -41,11 +45,16 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.animation.Animation; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; -import android.view.animation.LayoutAnimationController; +import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler; import com.android.launcher3.FolderIcon.FolderRingAnimator; +import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; +import com.android.launcher3.accessibility.FolderAccessibilityHelper; +import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.Arrays; @@ -54,55 +63,47 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Stack; -public class CellLayout extends ViewGroup { +public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { + public static final int WORKSPACE_ACCESSIBILITY_DRAG = 2; + public static final int FOLDER_ACCESSIBILITY_DRAG = 1; + static final String TAG = "CellLayout"; private Launcher mLauncher; - private int mCellWidth; - private int mCellHeight; + @Thunk int mCellWidth; + @Thunk int mCellHeight; private int mFixedCellWidth; private int mFixedCellHeight; - private int mCountX; - private int mCountY; + @Thunk int mCountX; + @Thunk int mCountY; private int mOriginalWidthGap; private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; + @Thunk int mWidthGap; + @Thunk int mHeightGap; private int mMaxGap; private boolean mDropPending = false; private boolean mIsDragTarget = true; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. - private final int[] mTmpXY = new int[2]; - private final int[] mTmpPoint = new int[2]; - int[] mTempLocation = new int[2]; + @Thunk final int[] mTmpPoint = new int[2]; + @Thunk final int[] mTempLocation = new int[2]; boolean[][] mOccupied; boolean[][] mTmpOccupied; - private boolean mLastDownOnOccupiedCell = false; private OnTouchListener mInterceptTouchListener; + private StylusEventHelper mStylusEventHelper; private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>(); private int[] mFolderLeaveBehindCell = {-1, -1}; - private float FOREGROUND_ALPHA_DAMPER = 0.65f; - private int mForegroundAlpha = 0; private float mBackgroundAlpha; - private float mBackgroundAlphaMultiplier = 1.0f; - private boolean mDrawBackground = true; - - private Drawable mNormalBackground; - private Drawable mActiveGlowBackground; - private Drawable mOverScrollForegroundDrawable; - private Drawable mOverScrollLeft; - private Drawable mOverScrollRight; - private Rect mBackgroundRect; - private Rect mForegroundRect; - private int mForegroundPadding; + + private static final int BACKGROUND_ACTIVATE_DURATION = 120; + private final TransitionDrawable mBackground; // These values allow a fixed measurement to be set on the CellLayout. private int mFixedWidth = -1; @@ -110,12 +111,11 @@ public class CellLayout extends ViewGroup { // If we're actively dragging something over this screen, mIsDragOverlapping is true private boolean mIsDragOverlapping = false; - boolean mUseActiveGlowBackground = false; // These arrays are used to implement the drag visualization on x-large screens. // They are used as circular arrays, indexed by mDragOutlineCurrent. - private Rect[] mDragOutlines = new Rect[4]; - private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; + @Thunk Rect[] mDragOutlines = new Rect[4]; + @Thunk float[] mDragOutlineAlphas = new float[mDragOutlines.length]; private InterruptibleInOutAnimator[] mDragOutlineAnims = new InterruptibleInOutAnimator[mDragOutlines.length]; @@ -123,12 +123,10 @@ public class CellLayout extends ViewGroup { private int mDragOutlineCurrent = 0; private final Paint mDragOutlinePaint = new Paint(); - private final FastBitmapView mTouchFeedbackView; + private final ClickShadowView mTouchFeedbackView; - private 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; @@ -156,19 +154,22 @@ public class CellLayout extends ViewGroup { private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; private static final int REORDER_ANIMATION_DURATION = 150; - private float mReorderPreviewAnimationMagnitude; + @Thunk float mReorderPreviewAnimationMagnitude; private ArrayList<View> mIntersectingViews = new ArrayList<View>(); private Rect mOccupiedRect = new Rect(); private int[] mDirectionVector = new int[2]; int[] mPreviousReorderDirection = new int[2]; private static final int INVALID_DIRECTION = -100; - private DropTarget.DragEnforcer mDragEnforcer; - private Rect mTempRect = new Rect(); + private final Rect mTempRect = new Rect(); private final static Paint sPaint = new Paint(); + // Related to accessible drag and drop + private DragAndDropAccessibilityDelegate mTouchHelper; + private boolean mUseTouchHelper = false; + public CellLayout(Context context) { this(context, null); } @@ -179,7 +180,6 @@ public class CellLayout extends ViewGroup { public CellLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mDragEnforcer = new DropTarget.DragEnforcer(context); // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show // the user where a dragged item will land when dropped. @@ -187,8 +187,7 @@ public class CellLayout extends ViewGroup { setClipToPadding(false); mLauncher = (Launcher) context; - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); mCellWidth = mCellHeight = -1; @@ -196,8 +195,8 @@ public class CellLayout extends ViewGroup { mWidthGap = mOriginalWidthGap = 0; mHeightGap = mOriginalHeightGap = 0; mMaxGap = Integer.MAX_VALUE; - mCountX = (int) grid.numColumns; - mCountY = (int) grid.numRows; + mCountX = (int) grid.inv.numColumns; + mCountY = (int) grid.inv.numRows; mOccupied = new boolean[mCountX][mCountY]; mTmpOccupied = new boolean[mCountX][mCountY]; mPreviousReorderDirection[0] = INVALID_DIRECTION; @@ -210,20 +209,12 @@ public class CellLayout extends ViewGroup { final Resources res = getResources(); mHotseatScale = (float) grid.hotseatIconSizePx / grid.iconSizePx; - mNormalBackground = res.getDrawable(R.drawable.screenpanel); - mActiveGlowBackground = res.getDrawable(R.drawable.screenpanel_hover); - - mOverScrollLeft = res.getDrawable(R.drawable.overscroll_glow_left); - mOverScrollRight = res.getDrawable(R.drawable.overscroll_glow_right); - mForegroundPadding = - res.getDimensionPixelSize(R.dimen.workspace_overscroll_drawable_padding); + mBackground = (TransitionDrawable) res.getDrawable(R.drawable.bg_screenpanel); + mBackground.setCallback(this); mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * grid.iconSizePx); - mNormalBackground.setFilterBitmap(true); - mActiveGlowBackground.setFilterBitmap(true); - // Initialize the data structures used for the drag visualization. mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out mDragCell[0] = mDragCell[1] = -1; @@ -281,19 +272,78 @@ public class CellLayout extends ViewGroup { mDragOutlineAnims[i] = anim; } - mBackgroundRect = new Rect(); - mForegroundRect = new Rect(); - mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, mCountX, mCountY); - mTouchFeedbackView = new FastBitmapView(context); - // Make the feedback view large enough to hold the blur bitmap. - addView(mTouchFeedbackView, (int) (grid.cellWidthPx * 1.5), (int) (grid.cellHeightPx * 1.5)); + mStylusEventHelper = new StylusEventHelper(this); + + mTouchFeedbackView = new ClickShadowView(context); + addView(mTouchFeedbackView); addView(mShortcutsAndWidgets); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void enableAccessibleDrag(boolean enable, int dragType) { + mUseTouchHelper = enable; + if (!enable) { + ViewCompat.setAccessibilityDelegate(this, null); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + setOnClickListener(mLauncher); + } else { + if (dragType == WORKSPACE_ACCESSIBILITY_DRAG && + !(mTouchHelper instanceof WorkspaceAccessibilityHelper)) { + mTouchHelper = new WorkspaceAccessibilityHelper(this); + } else if (dragType == FOLDER_ACCESSIBILITY_DRAG && + !(mTouchHelper instanceof FolderAccessibilityHelper)) { + mTouchHelper = new FolderAccessibilityHelper(this); + } + ViewCompat.setAccessibilityDelegate(this, mTouchHelper); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setOnClickListener(mTouchHelper); + } + + // Invalidate the accessibility hierarchy + if (getParent() != null) { + getParent().notifySubtreeAccessibilityStateChanged( + this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + } + } + + @Override + public boolean dispatchHoverEvent(MotionEvent event) { + // Always attempt to dispatch hover events to accessibility first. + if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mUseTouchHelper || + (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) { + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = super.onTouchEvent(ev); + // Stylus button press on a home screen should not switch between overview mode and + // the home screen mode, however, once in overview mode stylus button press should be + // enabled to allow rearranging the different home screens. So check what mode + // the workspace is in, and only perform stylus button presses while in overview mode. + if (mLauncher.mWorkspace.isInOverviewMode() + && mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + return true; + } + return handled; + } + public void enableHardwareLayer(boolean hasLayer) { mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint); } @@ -337,47 +387,19 @@ public class CellLayout extends ViewGroup { return mDropPending; } - void setOverScrollAmount(float r, boolean left) { - if (left && mOverScrollForegroundDrawable != mOverScrollLeft) { - mOverScrollForegroundDrawable = mOverScrollLeft; - } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) { - mOverScrollForegroundDrawable = mOverScrollRight; - } - - r *= FOREGROUND_ALPHA_DAMPER; - mForegroundAlpha = (int) Math.round((r * 255)); - mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha); - invalidate(); - } - - void setPressedIcon(BubbleTextView icon, Bitmap background, int padding) { + @Override + public void setPressedIcon(BubbleTextView icon, Bitmap background) { if (icon == null || background == null) { mTouchFeedbackView.setBitmap(null); mTouchFeedbackView.animate().cancel(); } else { - int offset = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - - (mCountX * mCellWidth); - mTouchFeedbackView.setTranslationX(icon.getLeft() + (int) Math.ceil(offset / 2f) - - padding); - mTouchFeedbackView.setTranslationY(icon.getTop() - padding); if (mTouchFeedbackView.setBitmap(background)) { - mTouchFeedbackView.setAlpha(0); - mTouchFeedbackView.animate().alpha(1) - .setDuration(FastBitmapDrawable.CLICK_FEEDBACK_DURATION) - .setInterpolator(FastBitmapDrawable.CLICK_FEEDBACK_INTERPOLATOR) - .start(); + mTouchFeedbackView.alignWithIconView(icon, mShortcutsAndWidgets); + mTouchFeedbackView.animateShadow(); } } } - void setUseActiveGlowBackground(boolean use) { - mUseActiveGlowBackground = use; - } - - void disableBackground() { - mDrawBackground = false; - } - void disableDragTarget() { mIsDragTarget = false; } @@ -389,7 +411,11 @@ public class CellLayout extends ViewGroup { void setIsDragOverlapping(boolean isDragOverlapping) { if (mIsDragOverlapping != isDragOverlapping) { mIsDragOverlapping = isDragOverlapping; - setUseActiveGlowBackground(mIsDragOverlapping); + if (mIsDragOverlapping) { + mBackground.startTransition(BACKGROUND_ACTIVATE_DURATION); + } else { + mBackground.reverseTransition(BACKGROUND_ACTIVATE_DURATION); + } invalidate(); } } @@ -400,24 +426,17 @@ public class CellLayout extends ViewGroup { @Override protected void onDraw(Canvas canvas) { + if (!mIsDragTarget) { + return; + } + // When we're large, we are either drawn in a "hover" state (ie when dragging an item to // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) // When we're small, we are either drawn normally or in the "accepts drops" state (during // a drag). However, we also drag the mini hover background *over* one of those two // backgrounds - if (mDrawBackground && mBackgroundAlpha > 0.0f) { - Drawable bg; - - if (mUseActiveGlowBackground) { - // In the mini case, we draw the active_glow bg *over* the active background - bg = mActiveGlowBackground; - } else { - bg = mNormalBackground; - } - - bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); - bg.setBounds(mBackgroundRect); - bg.draw(canvas); + if (mBackgroundAlpha > 0.0f) { + mBackground.draw(canvas); } final Paint paint = mDragOutlinePaint; @@ -453,8 +472,7 @@ public class CellLayout extends ViewGroup { int previewOffset = FolderRingAnimator.sPreviewSize; // The folder outer / inner ring image(s) - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); for (int i = 0; i < mFolderOuterRings.size(); i++) { FolderRingAnimator fra = mFolderOuterRings.get(i); @@ -513,15 +531,6 @@ public class CellLayout extends ViewGroup { } } - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (mForegroundAlpha > 0) { - mOverScrollForegroundDrawable.setBounds(mForegroundRect); - mOverScrollForegroundDrawable.draw(canvas); - } - } - public void showFolderAccept(FolderRingAnimator fra) { mFolderOuterRings.add(fra); } @@ -578,11 +587,11 @@ public class CellLayout extends ViewGroup { mInterceptTouchListener = listener; } - int getCountX() { + public int getCountX() { return mCountX; } - int getCountY() { + public int getCountY() { return mCountY; } @@ -591,6 +600,10 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.setIsHotseat(isHotseat); } + public boolean isHotseat() { + return mIsHotseat; + } + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, boolean markCells) { final LayoutParams lp = params; @@ -613,7 +626,6 @@ public class CellLayout extends ViewGroup { if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; child.setId(childId); - mShortcutsAndWidgets.addView(child, index, lp); if (markCells) markCellsAsOccupiedForView(child); @@ -637,10 +649,6 @@ public class CellLayout extends ViewGroup { } } - public void removeViewWithoutMarkingCells(View view) { - mShortcutsAndWidgets.removeView(view); - } - @Override public void removeView(View view) { markCellsAsUnoccupiedForView(view); @@ -675,25 +683,13 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.removeViewsInLayout(start, count); } - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // First we clear the tag to ensure that on every touch down we start with a fresh slate, - // even in the case where we return early. Not clearing here was causing bugs whereby on - // long-press we'd end up picking up an item from a previous drag operation. - if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { - return true; - } - - return false; - } - /** * Given a point, return the cell that strictly encloses that point * @param x X coordinate of the point * @param y Y coordinate of the point * @param result Array of 2 ints to hold the x and y coordinate of the cell */ - void pointToCellExact(int x, int y, int[] result) { + public void pointToCellExact(int x, int y, int[] result) { final int hStartPadding = getPaddingLeft(); final int vStartPadding = getPaddingTop(); @@ -782,9 +778,7 @@ public class CellLayout extends ViewGroup { public float getDistanceFromCell(float x, float y, int[] cell) { cellToCenterPoint(cell[0], cell[1], mTmpPoint); - float distance = (float) Math.sqrt( Math.pow(x - mTmpPoint[0], 2) + - Math.pow(y - mTmpPoint[1], 2)); - return distance; + return (float) Math.hypot(x - mTmpPoint[0], y - mTmpPoint[1]); } int getCellWidth() { @@ -803,28 +797,6 @@ public class CellLayout extends ViewGroup { return mHeightGap; } - Rect getContentRect(Rect r) { - if (r == null) { - r = new Rect(); - } - int left = getPaddingLeft(); - int top = getPaddingTop(); - int right = left + getWidth() - getPaddingLeft() - getPaddingRight(); - int bottom = top + getHeight() - getPaddingTop() - getPaddingBottom(); - r.set(left, top, right, bottom); - return r; - } - - /** Return a rect that has the cellWidth/cellHeight (left, top), and - * widthGap/heightGap (right, bottom) */ - static void getMetrics(Rect metrics, int paddedMeasureWidth, - int paddedMeasureHeight, int countX, int countY) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - metrics.set(grid.calculateCellWidth(paddedMeasureWidth, countX), - grid.calculateCellHeight(paddedMeasureHeight, countY), 0, 0); - } - public void setFixedSize(int width, int height) { mFixedWidth = width; mFixedHeight = height; @@ -832,9 +804,6 @@ public class CellLayout extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); @@ -842,8 +811,8 @@ public class CellLayout extends ViewGroup { int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight()); int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom()); if (mFixedCellWidth < 0 || mFixedCellHeight < 0) { - int cw = grid.calculateCellWidth(childWidthSize, mCountX); - int ch = grid.calculateCellHeight(childHeightSize, mCountY); + int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX); + int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY); if (cw != mCellWidth || ch != mCellHeight) { mCellWidth = cw; mCellHeight = ch; @@ -877,19 +846,20 @@ public class CellLayout extends ViewGroup { mWidthGap = mOriginalWidthGap; mHeightGap = mOriginalHeightGap; } - int count = getChildCount(); - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth, - MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, - MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); - maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); - } + + // Make the feedback view large enough to hold the blur bitmap. + mTouchFeedbackView.measure( + MeasureSpec.makeMeasureSpec(mCellWidth + mTouchFeedbackView.getExtraSize(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCellHeight + mTouchFeedbackView.getExtraSize(), + MeasureSpec.EXACTLY)); + + mShortcutsAndWidgets.measure( + MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + + int maxWidth = mShortcutsAndWidgets.getMeasuredWidth(); + int maxHeight = mShortcutsAndWidgets.getMeasuredHeight(); if (mFixedWidth > 0 && mFixedHeight > 0) { setMeasuredDimension(maxWidth, maxHeight); } else { @@ -903,13 +873,13 @@ public class CellLayout extends ViewGroup { (mCountX * mCellWidth); int left = getPaddingLeft() + (int) Math.ceil(offset / 2f); int top = getPaddingTop(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - child.layout(left, top, - left + r - l, - top + b - t); - } + + mTouchFeedbackView.layout(left, top, + left + mTouchFeedbackView.getMeasuredWidth(), + top + mTouchFeedbackView.getMeasuredHeight()); + mShortcutsAndWidgets.layout(left, top, + left + r - l, + top + b - t); } @Override @@ -917,12 +887,9 @@ public class CellLayout extends ViewGroup { super.onSizeChanged(w, h, oldw, oldh); // Expand the background drawing bounds by the padding baked into the background drawable - Rect padding = new Rect(); - mNormalBackground.getPadding(padding); - mBackgroundRect.set(-padding.left, -padding.top, w + padding.right, h + padding.bottom); - - mForegroundRect.set(mForegroundPadding, mForegroundPadding, - w - mForegroundPadding, h - mForegroundPadding); + mBackground.getPadding(mTempRect); + mBackground.setBounds(-mTempRect.left, -mTempRect.top, + w + mTempRect.right, h + mTempRect.bottom); } @Override @@ -939,25 +906,18 @@ public class CellLayout extends ViewGroup { return mBackgroundAlpha; } - public void setBackgroundAlphaMultiplier(float multiplier) { - - if (mBackgroundAlphaMultiplier != multiplier) { - mBackgroundAlphaMultiplier = multiplier; - invalidate(); - } - } - - public float getBackgroundAlphaMultiplier() { - return mBackgroundAlphaMultiplier; - } - public void setBackgroundAlpha(float alpha) { if (mBackgroundAlpha != alpha) { mBackgroundAlpha = alpha; - invalidate(); + mBackground.setAlpha((int) (mBackgroundAlpha * 255)); } } + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mIsDragTarget && who == mBackground); + } + public void setShortcutAndWidgetAlpha(float alpha) { mShortcutsAndWidgets.setAlpha(alpha); } @@ -1054,36 +1014,6 @@ public class CellLayout extends ViewGroup { return false; } - /** - * Estimate where the top left cell of the dragged item will land if it is dropped. - * - * @param originX The X value of the top left corner of the item - * @param originY The Y value of the top left corner of the item - * @param spanX The number of horizontal cells that the item spans - * @param spanY The number of vertical cells that the item spans - * @param result The estimated drop cell X and Y. - */ - void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) { - final int countX = mCountX; - final int countY = mCountY; - - // pointToCellRounded takes the top left of a cell but will pad that with - // cellWidth/2 and cellHeight/2 when finding the matching cell - pointToCellRounded(originX, originY, result); - - // If the item isn't fully on this screen, snap to the edges - int rightOverhang = result[0] + spanX - countX; - if (rightOverhang > 0) { - result[0] -= rightOverhang; // Snap to right - } - result[0] = Math.max(0, result[0]); // Snap to left - int bottomOverhang = result[1] + spanY - countY; - if (bottomOverhang > 0) { - result[1] -= bottomOverhang; // Snap to bottom - } - result[1] = Math.max(0, result[1]); // Snap to top - } - void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX, int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) { final int oldDragCellX = mDragCell[0]; @@ -1167,9 +1097,8 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, - int[] result) { - return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); + int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestVacantArea(pixelX, pixelY, spanX, spanY, spanX, spanY, result, null); } /** @@ -1189,30 +1118,10 @@ public class CellLayout extends ViewGroup { */ int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] result, int[] resultSpan) { - return findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, + return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, true, result, resultSpan); } - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreOccupied If true, the result can be an occupied cell - * @param result Array in which to place the result, or null (in which case a new array will - * be allocated) - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView, - boolean ignoreOccupied, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, - spanX, spanY, ignoreView, ignoreOccupied, result, null, mOccupied); - } - private final Stack<Rect> mTempRectStack = new Stack<Rect>(); private void lazyInitTempRectStack() { if (mTempRectStack.isEmpty()) { @@ -1244,12 +1153,9 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, - View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan, - boolean[][] occupied) { + private int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, + int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) { lazyInitTempRectStack(); - // mark space take by ignoreView as available (method checks if ignoreView is null) - markCellsAsUnoccupiedForView(ignoreView, occupied); // For items with a spanX / spanY > 1, the passed in point (pixelX, pixelY) corresponds // to the center of the item, but we are searching based on the top-left cell, so @@ -1280,7 +1186,7 @@ public class CellLayout extends ViewGroup { // First, let's see if this thing fits anywhere for (int i = 0; i < minSpanX; i++) { for (int j = 0; j < minSpanY; j++) { - if (occupied[x + i][y + j]) { + if (mOccupied[x + i][y + j]) { continue inner; } } @@ -1297,7 +1203,7 @@ public class CellLayout extends ViewGroup { while (!(hitMaxX && hitMaxY)) { if (incX && !hitMaxX) { for (int j = 0; j < ySize; j++) { - if (x + xSize > countX -1 || occupied[x + xSize][y + j]) { + if (x + xSize > countX -1 || mOccupied[x + xSize][y + j]) { // We can't move out horizontally hitMaxX = true; } @@ -1307,7 +1213,7 @@ public class CellLayout extends ViewGroup { } } else if (!hitMaxY) { for (int i = 0; i < xSize; i++) { - if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) { + if (y + ySize > countY - 1 || mOccupied[x + i][y + ySize]) { // We can't move out vertically hitMaxY = true; } @@ -1324,7 +1230,7 @@ public class CellLayout extends ViewGroup { hitMaxX = xSize >= spanX; hitMaxY = ySize >= spanY; } - final int[] cellXY = mTmpXY; + final int[] cellXY = mTmpPoint; cellToCenterPoint(x, y, cellXY); // We verify that the current rect is not a sub-rect of any of our previous @@ -1340,8 +1246,7 @@ public class CellLayout extends ViewGroup { } } validRegions.push(currentRect); - double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) - + Math.pow(cellXY[1] - pixelY, 2)); + double distance = Math.hypot(cellXY[0] - pixelX, cellXY[1] - pixelY); if ((distance <= bestDistance && !contained) || currentRect.contains(bestRect)) { @@ -1356,8 +1261,6 @@ public class CellLayout extends ViewGroup { } } } - // re-mark space taken by ignoreView as occupied - markCellsAsOccupiedForView(ignoreView, occupied); // Return -1, -1 if no suitable location found if (bestDistance == Double.MAX_VALUE) { @@ -1411,8 +1314,7 @@ public class CellLayout extends ViewGroup { } } - float distance = (float) - Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); + float distance = (float) Math.hypot(x - cellX, y - cellY); int[] curDirection = mTmpPoint; computeDirectionVector(x - cellX, y - cellY, curDirection); // The direction score is just the dot product of the two candidate direction @@ -2028,7 +1930,7 @@ public class CellLayout extends ViewGroup { } } - ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, + private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { // Copy the current state into the solution. This solution will be manipulated as necessary. @@ -2264,7 +2166,7 @@ public class CellLayout extends ViewGroup { } } - private void completeAnimationImmediately() { + @Thunk void completeAnimationImmediately() { if (a != null) { a.cancel(); } @@ -2317,7 +2219,7 @@ public class CellLayout extends ViewGroup { mLauncher.getWorkspace().updateItemLocationsInDatabase(this); } - public void setUseTempCoords(boolean useTempCoords) { + private void setUseTempCoords(boolean useTempCoords) { int childCount = mShortcutsAndWidgets.getChildCount(); for (int i = 0; i < childCount; i++) { LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); @@ -2325,11 +2227,11 @@ public class CellLayout extends ViewGroup { } } - ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, + private ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView, ItemConfiguration solution) { int[] result = new int[2]; int[] resultSpan = new int[2]; - findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, result, + findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, result, resultSpan); if (result[0] >= 0 && result[1] >= 0) { copyCurrentStateToSolution(solution, false); @@ -2585,7 +2487,7 @@ public class CellLayout extends ViewGroup { return mItemPlacementDirty; } - private class ItemConfiguration { + @Thunk class ItemConfiguration { HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>(); private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>(); ArrayList<View> sortedViews = new ArrayList<View>(); @@ -2646,45 +2548,6 @@ public class CellLayout extends ViewGroup { } /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreView Considers space occupied by this view as unoccupied - * @param result Previously returned value to possibly recycle. - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestVacantArea( - int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, ignoreView, true, result); - } - - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param minSpanX The minimum horizontal span required - * @param minSpanY The minimum vertical span required - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreView Considers space occupied by this view as unoccupied - * @param result Previously returned value to possibly recycle. - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, - int spanX, int spanY, View ignoreView, int[] result, int[] resultSpan) { - return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, ignoreView, true, - result, resultSpan, mOccupied); - } - - /** * Find a starting cell position that will fit the given bounds nearest the requested * cell location. Uses Euclidean distance to score multiple vacant areas. * @@ -2697,9 +2560,8 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestArea( - int pixelX, int pixelY, int spanX, int spanY, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result); + int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, spanX, spanY, false, result, null); } boolean existsEmptyCell() { @@ -2719,104 +2581,33 @@ public class CellLayout extends ViewGroup { * * @return True if a vacant cell of the specified dimension was found, false otherwise. */ - boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { - return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); - } - - /** - * Like above, but ignores any cells occupied by the item "ignoreView" - * - * @param cellXY The array that will contain the position of a vacant cell if such a cell - * can be found. - * @param spanX The horizontal span of the cell we want to find. - * @param spanY The vertical span of the cell we want to find. - * @param ignoreView The home screen item we should treat as not occupying any space - * @return - */ - boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { - return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, - ignoreView, mOccupied); - } - - /** - * Like above, but if intersectX and intersectY are not -1, then this method will try to - * return coordinates for rectangles that contain the cell [intersectX, intersectY] - * - * @param spanX The horizontal span of the cell we want to find. - * @param spanY The vertical span of the cell we want to find. - * @param ignoreView The home screen item we should treat as not occupying any space - * @param intersectX The X coordinate of the cell that we should try to overlap - * @param intersectX The Y coordinate of the cell that we should try to overlap - * - * @return True if a vacant cell of the specified dimension was found, false otherwise. - */ - boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, - int intersectX, int intersectY) { - return findCellForSpanThatIntersectsIgnoring( - cellXY, spanX, spanY, intersectX, intersectY, null, mOccupied); - } - - /** - * The superset of the above two methods - */ - boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY, - int intersectX, int intersectY, View ignoreView, boolean occupied[][]) { - // mark space take by ignoreView as available (method checks if ignoreView is null) - markCellsAsUnoccupiedForView(ignoreView, occupied); - + public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { boolean foundCell = false; - while (true) { - int startX = 0; - if (intersectX >= 0) { - startX = Math.max(startX, intersectX - (spanX - 1)); - } - int endX = mCountX - (spanX - 1); - if (intersectX >= 0) { - endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0)); - } - int startY = 0; - if (intersectY >= 0) { - startY = Math.max(startY, intersectY - (spanY - 1)); - } - int endY = mCountY - (spanY - 1); - if (intersectY >= 0) { - endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0)); - } - - for (int y = startY; y < endY && !foundCell; y++) { - inner: - for (int x = startX; x < endX; x++) { - for (int i = 0; i < spanX; i++) { - for (int j = 0; j < spanY; j++) { - if (occupied[x + i][y + j]) { - // small optimization: we can skip to after the column we just found - // an occupied cell - x += i; - continue inner; - } + final int endX = mCountX - (spanX - 1); + final int endY = mCountY - (spanY - 1); + + for (int y = 0; y < endY && !foundCell; y++) { + inner: + for (int x = 0; x < endX; x++) { + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (mOccupied[x + i][y + j]) { + // small optimization: we can skip to after the column we just found + // an occupied cell + x += i; + continue inner; } } - if (cellXY != null) { - cellXY[0] = x; - cellXY[1] = y; - } - foundCell = true; - break; } - } - if (intersectX == -1 && intersectY == -1) { + if (cellXY != null) { + cellXY[0] = x; + cellXY[1] = y; + } + foundCell = true; break; - } else { - // if we failed to find anything, try again but without any requirements of - // intersecting - intersectX = -1; - intersectY = -1; - continue; } } - // re-mark space taken by ignoreView as occupied - markCellsAsOccupiedForView(ignoreView, occupied); return foundCell; } @@ -2826,7 +2617,6 @@ public class CellLayout extends ViewGroup { * or it may have begun on another layout. */ void onDragEnter() { - mDragEnforcer.onDragEnter(); mDragging = true; } @@ -2834,7 +2624,6 @@ public class CellLayout extends ViewGroup { * Called when drag has left this CellLayout or has been completed (successfully or not) */ void onDragExit() { - mDragEnforcer.onDragExit(); // This can actually be called when we aren't in a drag, e.g. when adding a new // item to this layout via the customize drawer. // Guard against that case. @@ -2900,18 +2689,20 @@ public class CellLayout extends ViewGroup { * @param height Height in pixels * @param result An array of length 2 in which to store the result (may be null). */ - public static int[] rectToCell(int width, int height, int[] result) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect padding = grid.getWorkspacePadding(grid.isLandscape ? - CellLayout.LANDSCAPE : CellLayout.PORTRAIT); + public static int[] rectToCell(Launcher launcher, int width, int height, int[] result) { + return rectToCell(launcher.getDeviceProfile(), launcher, width, height, result); + } + + public static int[] rectToCell(DeviceProfile grid, Context context, int width, int height, + int[] result) { + Rect padding = grid.getWorkspacePadding(Utilities.isRtl(context.getResources())); // Always assume we're working with the smallest span to make sure we // reserve enough space in both orientations. - int parentWidth = grid.calculateCellWidth(grid.widthPx - - padding.left - padding.right, (int) grid.numColumns); - int parentHeight = grid.calculateCellHeight(grid.heightPx - - padding.top - padding.bottom, (int) grid.numRows); + int parentWidth = DeviceProfile.calculateCellWidth(grid.widthPx + - padding.left - padding.right, (int) grid.inv.numColumns); + int parentHeight = DeviceProfile.calculateCellHeight(grid.heightPx + - padding.top - padding.bottom, (int) grid.inv.numRows); int smallerSize = Math.min(parentWidth, parentHeight); // Always round up to next largest cell @@ -2926,13 +2717,6 @@ public class CellLayout extends ViewGroup { return result; } - public int[] cellSpansToSize(int hSpans, int vSpans) { - int[] size = new int[2]; - size[0] = hSpans * mCellWidth + (hSpans - 1) * mWidthGap; - size[1] = vSpans * mCellHeight + (vSpans - 1) * mHeightGap; - return size; - } - /** * Calculate the grid spans needed to fit given item */ @@ -2951,49 +2735,11 @@ public class CellLayout extends ViewGroup { info.spanX = info.spanY = 1; return; } - int[] spans = rectToCell(minWidth, minHeight, null); + int[] spans = rectToCell(mLauncher, minWidth, minHeight, null); info.spanX = spans[0]; info.spanY = spans[1]; } - /** - * Find the first vacant cell, if there is one. - * - * @param vacant Holds the x and y coordinate of the vacant cell - * @param spanX Horizontal cell span. - * @param spanY Vertical cell span. - * - * @return True if a vacant cell was found - */ - public boolean getVacantCell(int[] vacant, int spanX, int spanY) { - - return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied); - } - - static boolean findVacantCell(int[] vacant, int spanX, int spanY, - int xCount, int yCount, boolean[][] occupied) { - - for (int y = 0; y < yCount; y++) { - for (int x = 0; x < xCount; x++) { - boolean available = !occupied[x][y]; -out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { - for (int j = y; j < y + spanY - 1 && y < yCount; j++) { - available = available && !occupied[i][j]; - if (!available) break out; - } - } - - if (available) { - vacant[0] = x; - vacant[1] = y; - return true; - } - } - } - - return false; - } - private void clearOccupiedCells() { for (int x = 0; x < mCountX; x++) { for (int y = 0; y < mCountY; y++) { @@ -3002,27 +2748,16 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { } } - public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) { - markCellsAsUnoccupiedForView(view); - markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true); - } - public void markCellsAsOccupiedForView(View view) { - markCellsAsOccupiedForView(view, mOccupied); - } - public void markCellsAsOccupiedForView(View view, boolean[][] occupied) { if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); - markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, true); } public void markCellsAsUnoccupiedForView(View view) { - markCellsAsUnoccupiedForView(view, mOccupied); - } - public void markCellsAsUnoccupiedForView(View view, boolean occupied[][]) { if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); - markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, false); } private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied, @@ -3068,17 +2803,6 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { return new CellLayout.LayoutParams(p); } - public static class CellLayoutAnimationController extends LayoutAnimationController { - public CellLayoutAnimationController(Animation animation, float delay) { - super(animation, delay); - } - - @Override - protected long getDelayForView(View view) { - return (int) (Math.random() * 150); - } - } - public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Horizontal location of the item in the grid. @@ -3237,7 +2961,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { // 2. When long clicking on an empty cell in a CellLayout, we save information about the // cellX and cellY coordinates and which page was clicked. We then set this as a tag on // the CellLayout that was long clicked - static final class CellInfo { + public static final class CellInfo { View cell; int cellX = -1; int cellY = -1; @@ -3246,7 +2970,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { long screenId; long container; - CellInfo(View v, ItemInfo info) { + public CellInfo(View v, ItemInfo info) { cell = v; cellX = info.cellX; cellY = info.cellY; @@ -3263,7 +2987,24 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { } } - public boolean lastDownOnOccupiedCell() { - return mLastDownOnOccupiedCell; + public boolean findVacantCell(int spanX, int spanY, int[] outXY) { + return Utilities.findVacantCell(outXY, spanX, spanY, mCountX, mCountY, mOccupied); + } + + public boolean isRegionVacant(int x, int y, int spanX, int spanY) { + int x2 = x + spanX - 1; + int y2 = y + spanY - 1; + if (x < 0 || y < 0 || x2 >= mCountX || y2 >= mCountY) { + return false; + } + for (int i = x; i <= x2; i++) { + for (int j = y; j <= y2; j++) { + if (mOccupied[i][j]) { + return false; + } + } + } + + return true; } } diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java index 81149793d..483c62249 100644 --- a/src/com/android/launcher3/CheckLongPressHelper.java +++ b/src/com/android/launcher3/CheckLongPressHelper.java @@ -18,16 +18,27 @@ package com.android.launcher3; import android.view.View; +import com.android.launcher3.util.Thunk; + public class CheckLongPressHelper { - private View mView; - private boolean mHasPerformedLongPress; + + @Thunk View mView; + @Thunk View.OnLongClickListener mListener; + @Thunk boolean mHasPerformedLongPress; + private int mLongPressTimeout = 300; private CheckForLongPress mPendingCheckForLongPress; class CheckForLongPress implements Runnable { public void run() { if ((mView.getParent() != null) && mView.hasWindowFocus() && !mHasPerformedLongPress) { - if (mView.performLongClick()) { + boolean handled; + if (mListener != null) { + handled = mListener.onLongClick(mView); + } else { + handled = mView.performLongClick(); + } + if (handled) { mView.setPressed(false); mHasPerformedLongPress = true; } @@ -39,14 +50,25 @@ public class CheckLongPressHelper { mView = v; } + public CheckLongPressHelper(View v, View.OnLongClickListener listener) { + mView = v; + mListener = listener; + } + + /** + * Overrides the default long press timeout. + */ + public void setLongPressTimeout(int longPressTimeout) { + mLongPressTimeout = longPressTimeout; + } + public void postCheckForLongPress() { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } - mView.postDelayed(mPendingCheckForLongPress, - LauncherAppState.getInstance().getLongPressTimeout()); + mView.postDelayed(mPendingCheckForLongPress, mLongPressTimeout); } public void cancelLongPress() { diff --git a/src/com/android/launcher3/ClickShadowView.java b/src/com/android/launcher3/ClickShadowView.java new file mode 100644 index 000000000..e31d7f7f6 --- /dev/null +++ b/src/com/android/launcher3/ClickShadowView.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.View; +import android.view.ViewGroup; + +public class ClickShadowView extends View { + + private static final int SHADOW_SIZE_FACTOR = 3; + private static final int SHADOW_LOW_ALPHA = 30; + private static final int SHADOW_HIGH_ALPHA = 60; + + private final Paint mPaint; + + private final float mShadowOffset; + private final float mShadowPadding; + + private Bitmap mBitmap; + + public ClickShadowView(Context context) { + super(context); + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + mPaint.setColor(Color.BLACK); + + mShadowPadding = getResources().getDimension(R.dimen.blur_size_click_shadow); + mShadowOffset = getResources().getDimension(R.dimen.click_shadow_high_shift); + } + + /** + * @return extra space required by the view to show the shadow. + */ + public int getExtraSize() { + return (int) (SHADOW_SIZE_FACTOR * mShadowPadding); + } + + /** + * Applies the new bitmap. + * @return true if the view was invalidated. + */ + public boolean setBitmap(Bitmap b) { + if (b != mBitmap){ + mBitmap = b; + invalidate(); + return true; + } + return false; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + mPaint.setAlpha(SHADOW_LOW_ALPHA); + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + mPaint.setAlpha(SHADOW_HIGH_ALPHA); + canvas.drawBitmap(mBitmap, 0, mShadowOffset, mPaint); + } + } + + public void animateShadow() { + setAlpha(0); + animate().alpha(1) + .setDuration(FastBitmapDrawable.CLICK_FEEDBACK_DURATION) + .setInterpolator(FastBitmapDrawable.CLICK_FEEDBACK_INTERPOLATOR) + .start(); + } + + /** + * Aligns the shadow with {@param view} + * @param viewParent immediate parent of {@param view}. It must be a sibling of this view. + */ + public void alignWithIconView(BubbleTextView view, ViewGroup viewParent) { + float leftShift = view.getLeft() + viewParent.getLeft() - getLeft(); + float topShift = view.getTop() + viewParent.getTop() - getTop(); + int iconWidth = view.getRight() - view.getLeft(); + int iconHSpace = iconWidth - view.getCompoundPaddingRight() - view.getCompoundPaddingLeft(); + float drawableWidth = view.getIcon().getBounds().width(); + + setTranslationX(leftShift + + viewParent.getTranslationX() + + view.getCompoundPaddingLeft() * view.getScaleX() + + (iconHSpace - drawableWidth) * view.getScaleX() / 2 /* drawable gap */ + + iconWidth * (1 - view.getScaleX()) / 2 /* gap due to scale */ + - mShadowPadding /* extra shadow size */ + ); + setTranslationY(topShift + + viewParent.getTranslationY() + + view.getPaddingTop() * view.getScaleY() /* drawable gap */ + + view.getHeight() * (1 - view.getScaleY()) / 2 /* gap due to scale */ + - mShadowPadding /* extra shadow size */ + ); + } +} diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java new file mode 100644 index 000000000..5314ecff1 --- /dev/null +++ b/src/com/android/launcher3/CommonAppTypeParser.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2008 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.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.res.XmlResourceParser; +import android.database.sqlite.SQLiteDatabase; +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; + +import java.io.IOException; + +/** + * A class that parses content values corresponding to some common app types. + */ +public class CommonAppTypeParser implements LayoutParserCallback { + private static final String TAG = "CommonAppTypeParser"; + + // Including TARGET_NONE + public static final int SUPPORTED_TYPE_COUNT = 7; + + private static final int RESTORE_FLAG_BIT_SHIFT = 4; + + + private final long mItemId; + @Thunk final int mResId; + @Thunk final Context mContext; + + ContentValues parsedValues; + Intent parsedIntent; + String parsedTitle; + + public CommonAppTypeParser(long itemId, int itemType, Context context) { + mItemId = itemId; + mContext = context; + mResId = getResourceForItemType(itemType); + } + + @Override + public long generateNewItemId() { + return mItemId; + } + + @Override + public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + parsedValues = values; + + // Remove unwanted values + values.put(Favorites.ICON_TYPE, (Integer) null); + values.put(Favorites.ICON_PACKAGE, (String) null); + values.put(Favorites.ICON_RESOURCE, (String) null); + values.put(Favorites.ICON, (byte[]) null); + return 1; + } + + /** + * Tries to find a suitable app to the provided app type. + */ + public boolean findDefaultApp() { + if (mResId == 0) { + return false; + } + + parsedIntent = null; + parsedValues = null; + new MyLayoutParser().parseValues(); + return (parsedValues != null) && (parsedIntent != null); + } + + private class MyLayoutParser extends DefaultLayoutParser { + + public MyLayoutParser() { + super(CommonAppTypeParser.this.mContext, null, CommonAppTypeParser.this, + CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE, 0); + } + + @Override + protected long addShortcut(String title, Intent intent, int type) { + if (type == Favorites.ITEM_TYPE_APPLICATION) { + parsedIntent = intent; + parsedTitle = title; + } + return super.addShortcut(title, intent, type); + } + + public void parseValues() { + XmlResourceParser parser = mSourceRes.getXml(mLayoutId); + try { + beginDocument(parser, mRootTag); + new ResolveParser().parseAndAdd(parser); + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Unable to parse default app info", e); + } + parser.close(); + } + } + + public static int getResourceForItemType(int type) { + switch (type) { + case Favorite.TARGET_PHONE: + return R.xml.app_target_phone; + + case Favorite.TARGET_MESSENGER: + return R.xml.app_target_messenger; + + case Favorite.TARGET_EMAIL: + return R.xml.app_target_email; + + case Favorite.TARGET_BROWSER: + return R.xml.app_target_browser; + + case Favorite.TARGET_GALLERY: + return R.xml.app_target_gallery; + + case Favorite.TARGET_CAMERA: + return R.xml.app_target_camera; + + default: + return 0; + } + } + + public static int encodeItemTypeToFlag(int itemType) { + return itemType << RESTORE_FLAG_BIT_SHIFT; + } + + public static int decodeItemTypeFromFlag(int flag) { + return (flag & ShortcutInfo.FLAG_RESTORED_APP_TYPE) >> RESTORE_FLAG_BIT_SHIFT; + } + +} diff --git a/src/com/android/launcher3/CustomAppWidget.java b/src/com/android/launcher3/CustomAppWidget.java new file mode 100644 index 000000000..1b4ed79c0 --- /dev/null +++ b/src/com/android/launcher3/CustomAppWidget.java @@ -0,0 +1,14 @@ +package com.android.launcher3; + +public interface CustomAppWidget { + public String getLabel(); + public int getPreviewImage(); + public int getIcon(); + public int getWidgetLayout(); + + public int getSpanX(); + public int getSpanY(); + public int getMinSpanX(); + public int getMinSpanY(); + public int getResizeMode(); +} diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index e3ea40ebb..7b91c675b 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -13,13 +13,13 @@ import android.text.TextUtils; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -29,17 +29,15 @@ import java.util.List; public class DefaultLayoutParser extends AutoInstallsLayout { private static final String TAG = "DefaultLayoutParser"; - private static final String TAG_RESOLVE = "resolve"; + protected static final String TAG_RESOLVE = "resolve"; private static final String TAG_FAVORITES = "favorites"; - private static final String TAG_FAVORITE = "favorite"; + protected static final String TAG_FAVORITE = "favorite"; private static final String TAG_APPWIDGET = "appwidget"; private static final String TAG_SHORTCUT = "shortcut"; private static final String TAG_FOLDER = "folder"; private static final String TAG_PARTNER_FOLDER = "partner-folder"; - private static final String TAG_INCLUDE = "include"; - private static final String ATTR_URI = "uri"; - private static final String ATTR_WORKSPACE = "workspace"; + protected static final String ATTR_URI = "uri"; private static final String ATTR_CONTAINER = "container"; private static final String ATTR_SCREEN = "screen"; private static final String ATTR_FOLDER_ITEMS = "folderItems"; @@ -47,7 +45,12 @@ public class DefaultLayoutParser extends AutoInstallsLayout { public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources sourceRes, int layoutId) { super(context, appWidgetHost, callback, sourceRes, layoutId, TAG_FAVORITES); - Log.e(TAG, "Default layout parser initialized"); + } + + public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag, + int hotseatAllAppsRank) { + super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag, hotseatAllAppsRank); } @Override @@ -55,7 +58,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { return getFolderElementsMap(mSourceRes); } - private HashMap<String, TagParser> getFolderElementsMap(Resources res) { + @Thunk HashMap<String, TagParser> getFolderElementsMap(Resources res) { HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser()); parsers.put(TAG_SHORTCUT, new UriShortcutParser(res)); @@ -84,29 +87,10 @@ public class DefaultLayoutParser extends AutoInstallsLayout { out[1] = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); } - @Override - protected int parseAndAddNode( - XmlResourceParser parser, - HashMap<String, TagParser> tagParserMap, - ArrayList<Long> screenIds) - throws XmlPullParserException, IOException { - if (TAG_INCLUDE.equals(parser.getName())) { - final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); - if (resId != 0) { - // recursively load some more favorites, why not? - return parseLayout(resId, screenIds); - } else { - return 0; - } - } else { - return super.parseAndAddNode(parser, tagParserMap, screenIds); - } - } - /** * AppShortcutParser which also supports adding URI based intents */ - private class AppShortcutWithUriParser extends AppShortcutParser { + @Thunk class AppShortcutWithUriParser extends AppShortcutParser { @Override protected long invalidPackageOrClass(XmlResourceParser parser) { @@ -218,7 +202,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * Contains a list of <favorite> nodes, and accepts the first successfully parsed node. */ - private class ResolveParser implements TagParser { + protected class ResolveParser implements TagParser { private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser(); @@ -248,7 +232,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * A parser which adds a folder whose contents come from partner apk. */ - private class PartnerFolderParser implements TagParser { + @Thunk class PartnerFolderParser implements TagParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, @@ -274,7 +258,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * An extension of FolderParser which allows adding items from a different xml. */ - private class MyFolderParser extends FolderParser { + @Thunk class MyFolderParser extends FolderParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java index a2d121d63..a43ab6723 100644 --- a/src/com/android/launcher3/DeferredHandler.java +++ b/src/com/android/launcher3/DeferredHandler.java @@ -20,10 +20,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; -import android.util.Pair; + +import com.android.launcher3.util.Thunk; import java.util.LinkedList; -import java.util.ListIterator; /** * Queue of things to run on a looper thread. Items posted with {@link #post} will not @@ -33,20 +33,18 @@ import java.util.ListIterator; * This class is fifo. */ public class DeferredHandler { - private LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>(); + @Thunk LinkedList<Runnable> mQueue = new LinkedList<>(); private MessageQueue mMessageQueue = Looper.myQueue(); private Impl mHandler = new Impl(); - private class Impl extends Handler implements MessageQueue.IdleHandler { + @Thunk class Impl extends Handler implements MessageQueue.IdleHandler { public void handleMessage(Message msg) { - Pair<Runnable, Integer> p; Runnable r; synchronized (mQueue) { if (mQueue.size() == 0) { return; } - p = mQueue.removeFirst(); - r = p.first; + r = mQueue.removeFirst(); } r.run(); synchronized (mQueue) { @@ -77,11 +75,8 @@ public class DeferredHandler { /** Schedule runnable to run after everything that's on the queue right now. */ public void post(Runnable runnable) { - post(runnable, 0); - } - public void post(Runnable runnable, int type) { synchronized (mQueue) { - mQueue.add(new Pair<Runnable, Integer>(runnable, type)); + mQueue.add(runnable); if (mQueue.size() == 1) { scheduleNextLocked(); } @@ -90,31 +85,10 @@ public class DeferredHandler { /** Schedule runnable to run when the queue goes idle. */ public void postIdle(final Runnable runnable) { - postIdle(runnable, 0); - } - public void postIdle(final Runnable runnable, int type) { - post(new IdleRunnable(runnable), type); + post(new IdleRunnable(runnable)); } - public void cancelRunnable(Runnable runnable) { - synchronized (mQueue) { - while (mQueue.remove(runnable)) { } - } - } - public void cancelAllRunnablesOfType(int type) { - synchronized (mQueue) { - ListIterator<Pair<Runnable, Integer>> iter = mQueue.listIterator(); - Pair<Runnable, Integer> p; - while (iter.hasNext()) { - p = iter.next(); - if (p.second == type) { - iter.remove(); - } - } - } - } - - public void cancel() { + public void cancelAll() { synchronized (mQueue) { mQueue.clear(); } @@ -122,20 +96,19 @@ public class DeferredHandler { /** Runs all queued Runnables from the calling thread. */ public void flush() { - LinkedList<Pair<Runnable, Integer>> queue = new LinkedList<Pair<Runnable, Integer>>(); + LinkedList<Runnable> queue = new LinkedList<>(); synchronized (mQueue) { queue.addAll(mQueue); mQueue.clear(); } - for (Pair<Runnable, Integer> p : queue) { - p.first.run(); + for (Runnable r : queue) { + r.run(); } } void scheduleNextLocked() { if (mQueue.size() > 0) { - Pair<Runnable, Integer> p = mQueue.getFirst(); - Runnable peek = p.first; + Runnable peek = mQueue.getFirst(); if (peek instanceof IdleRunnable) { mMessageQueue.addIdleHandler(mHandler); } else { diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index ea058ea71..9c8659c29 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -17,45 +17,17 @@ package com.android.launcher3; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.drawable.TransitionDrawable; import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.UserManager; import android.util.AttributeSet; import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.LinearInterpolator; -import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.FlingAnimation; +import com.android.launcher3.util.Thunk; public class DeleteDropTarget extends ButtonDropTarget { - private static int DELETE_ANIMATION_DURATION = 285; - private static int FLING_DELETE_ANIMATION_DURATION = 350; - private static float FLING_TO_DELETE_FRICTION = 0.035f; - private static int MODE_FLING_DELETE_TO_TRASH = 0; - private static int MODE_FLING_DELETE_ALONG_VECTOR = 1; - - private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; - - private ColorStateList mOriginalTextColor; - private TransitionDrawable mUninstallDrawable; - private TransitionDrawable mRemoveDrawable; - private TransitionDrawable mCurrentDrawable; - - private boolean mWaitingForUninstall = false; public DeleteDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -68,442 +40,86 @@ public class DeleteDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - // Get the drawable - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.delete_target_hover_tint); - mUninstallDrawable = (TransitionDrawable) - r.getDrawable(R.drawable.uninstall_target_selector); - mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); - - mRemoveDrawable.setCrossFadeEnabled(true); - mUninstallDrawable.setCrossFadeEnabled(true); - - // The current drawable is set to either the remove drawable or the uninstall drawable - // and is initially set to the remove drawable, as set in the layout xml. - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } - } + mHoverColor = getResources().getColor(R.color.delete_target_hover_tint); - private boolean isAllAppsApplication(DragSource source, Object info) { - return source.supportsAppInfoDropTarget() && (info instanceof AppInfo); - } - private boolean isAllAppsWidget(DragSource source, Object info) { - if (source instanceof AppsCustomizePagedView) { - if (info instanceof PendingAddItemInfo) { - PendingAddItemInfo addInfo = (PendingAddItemInfo) info; - switch (addInfo.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - return true; - } - } - } - return false; - } - private boolean isDragSourceWorkspaceOrFolder(DragObject d) { - return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder); - } - private boolean isWorkspaceOrFolderApplication(DragObject d) { - return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo); - } - private boolean isWorkspaceOrFolderWidget(DragObject d) { - return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo); - } - private boolean isWorkspaceFolder(DragObject d) { - return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo); + setDrawable(R.drawable.ic_remove_launcher); } - private void setHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.startTransition(mTransitionDuration); - } - setTextColor(mHoverColor); - } - private void resetHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.resetTransition(); - } - setTextColor(mOriginalTextColor); + public static boolean supportsDrop(Object info) { + return (info instanceof ShortcutInfo) + || (info instanceof LauncherAppWidgetInfo) + || (info instanceof FolderInfo); } @Override - public boolean acceptDrop(DragObject d) { - return willAcceptDrop(d.dragInfo); - } - - public static boolean willAcceptDrop(Object info) { - if (info instanceof ItemInfo) { - ItemInfo item = (ItemInfo) info; - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof AppInfo) { - AppInfo appInfo = (AppInfo) info; - return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } - - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof ShortcutInfo) { - if (LauncherAppState.isDisableAllApps()) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } else { - return true; - } - } - } - return false; - } - - @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; - boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() && - isAllAppsApplication(source, info); - boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget(); - - // If we are dragging an application from AppsCustomize, only show the control if we can - // delete the app (it was downloaded), and rename the string to "uninstall" in such a case. - // Hide the delete target if it is a widget from AppsCustomize. - if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) { - isVisible = false; - } - if (useUninstallLabel) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - UserManager userManager = (UserManager) - getContext().getSystemService(Context.USER_SERVICE); - Bundle restrictions = userManager.getUserRestrictions(); - if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) - || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) { - isVisible = false; - } - } - } - - if (useUninstallLabel) { - setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); - } else if (useDeleteLabel) { - setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); - } else { - isVisible = false; - } - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - mActive = isVisible; - resetHoverColor(); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); - if (isVisible && getText().length() > 0) { - setText(useUninstallLabel ? R.string.delete_target_uninstall_label - : R.string.delete_target_label); - } + protected boolean supportsDrop(DragSource source, Object info) { + return source.supportsDeleteDropTarget() && supportsDrop(info); } @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; - } - - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - setHoverColor(); - } - - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - resetHoverColor(); - } else { - // Restore the hover color if we are deleting - d.dragView.setColor(mHoverColor); - } - } - - private void animateToTrashAndCompleteDrop(final DragObject d) { - final DragLayer dragLayer = mLauncher.getDragLayer(); - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); - final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - width, height); - final float scale = (float) to.width() / from.width(); - - mSearchDropTargetBar.deferOnDragEnd(); - deferCompleteDropIfUninstalling(d); - - Runnable onAnimationEndRunnable = new Runnable() { - @Override - public void run() { - completeDrop(d); - mSearchDropTargetBar.onDragEnd(); - mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); - } - }; - dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, - DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), - new LinearInterpolator(), onAnimationEndRunnable, - DragLayer.ANIMATION_END_DISAPPEAR, null); - } - - private void deferCompleteDropIfUninstalling(DragObject d) { - mWaitingForUninstall = false; - if (isUninstallFromWorkspace(d)) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } - mWaitingForUninstall = true; + @Thunk void completeDrop(DragObject d) { + ItemInfo item = (ItemInfo) d.dragInfo; + if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) { + removeWorkspaceOrFolderItem(mLauncher, item, null); } } - private boolean isUninstallFromWorkspace(DragObject d) { - if (LauncherAppState.isDisableAllApps() && isWorkspaceOrFolderApplication(d)) { - ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo; - // Only allow manifest shortcuts to initiate an un-install. - return !InstallShortcutReceiver.isValidShortcutLaunchIntent(shortcut.intent); - } - return false; - } + /** + * Removes the item from the workspace. If the view is not null, it also removes the view. + * @return true if the item was removed. + */ + public static boolean removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) { + if (item instanceof ShortcutInfo) { + LauncherModel.deleteItemFromDatabase(launcher, item); + } else if (item instanceof FolderInfo) { + FolderInfo folder = (FolderInfo) item; + launcher.removeFolder(folder); + LauncherModel.deleteFolderContentsFromDatabase(launcher, folder); + } else if (item instanceof LauncherAppWidgetInfo) { + final LauncherAppWidgetInfo widget = (LauncherAppWidgetInfo) item; - private void completeDrop(DragObject d) { - ItemInfo item = (ItemInfo) d.dragInfo; - boolean wasWaitingForUninstall = mWaitingForUninstall; - mWaitingForUninstall = false; - if (isAllAppsApplication(d.dragSource, item)) { - // Uninstall the application if it is being dragged from AppsCustomize - AppInfo appInfo = (AppInfo) item; - mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags, - appInfo.user); - } else if (isUninstallFromWorkspace(d)) { - ShortcutInfo shortcut = (ShortcutInfo) item; - if (shortcut.intent != null && shortcut.intent.getComponent() != null) { - final ComponentName componentName = shortcut.intent.getComponent(); - final DragSource dragSource = d.dragSource; - final UserHandleCompat user = shortcut.user; - mWaitingForUninstall = mLauncher.startApplicationUninstallActivity( - componentName, shortcut.flags, user); - if (mWaitingForUninstall) { - final Runnable checkIfUninstallWasSuccess = new Runnable() { - @Override - public void run() { - mWaitingForUninstall = false; - String packageName = componentName.getPackageName(); - boolean uninstallSuccessful = !AllAppsList.packageHasActivities( - getContext(), packageName, user); - if (dragSource instanceof Folder) { - ((Folder) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } else if (dragSource instanceof Workspace) { - ((Workspace) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } - } - }; - mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); - } - } - } else if (isWorkspaceOrFolderApplication(d)) { - LauncherModel.deleteItemFromDatabase(mLauncher, item); - } else if (isWorkspaceFolder(d)) { - // Remove the folder from the workspace and delete the contents from launcher model - FolderInfo folderInfo = (FolderInfo) item; - mLauncher.removeFolder(folderInfo); - LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo); - } else if (isWorkspaceOrFolderWidget(d)) { // Remove the widget from the workspace - mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); - LauncherModel.deleteItemFromDatabase(mLauncher, item); + launcher.removeAppWidget(widget); + LauncherModel.deleteItemFromDatabase(launcher, widget); + + final LauncherAppWidgetHost appWidgetHost = launcher.getAppWidgetHost(); - final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; - final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); - if ((appWidgetHost != null) && launcherAppWidgetInfo.isWidgetIdValid()) { + if (appWidgetHost != null && !widget.isCustomWidget() + && widget.isWidgetIdValid()) { // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... new AsyncTask<Void, Void, Void>() { public Void doInBackground(Void ... args) { - appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); + appWidgetHost.deleteAppWidgetId(widget.appWidgetId); return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + } else { + return false; } - if (wasWaitingForUninstall && !mWaitingForUninstall) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).onUninstallActivityReturned(false); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).onUninstallActivityReturned(false); - } - } - } - - public void onDrop(DragObject d) { - animateToTrashAndCompleteDrop(d); - } - - /** - * Creates an animation from the current drag view to the delete trash icon. - */ - private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, - DragObject d, PointF vel, ViewConfiguration config) { - - int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); - final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - width, height); - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - // Calculate how far along the velocity vector we should put the intermediate point on - // the bezier curve - float velocity = Math.abs(vel.length()); - float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); - int offsetY = (int) (-from.top * vp); - int offsetX = (int) (offsetY / (vel.y / vel.x)); - final float y2 = from.top + offsetY; // intermediate t/l - final float x2 = from.left + offsetX; - final float x1 = from.left; // drag view t/l - final float y1 = from.top; - final float x3 = to.left; // delete target t/l - final float y3 = to.top; - - final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { - @Override - public float getInterpolation(float t) { - return t * t * t * t * t * t * t * t; - } - }; - return new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final DragView dragView = (DragView) dragLayer.getAnimatedView(); - float t = ((Float) animation.getAnimatedValue()).floatValue(); - float tp = scaleAlphaInterpolator.getInterpolation(t); - float initialScale = dragView.getInitialScale(); - float finalAlpha = 0.5f; - float scale = dragView.getScaleX(); - float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; - float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; - float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + - (t * t) * x3; - float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + - (t * t) * y3; - - dragView.setTranslationX(x); - dragView.setTranslationY(y); - dragView.setScaleX(initialScale * (1f - tp)); - dragView.setScaleY(initialScale * (1f - tp)); - dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); - } - }; - } - - /** - * Creates an animation from the current drag view along its current velocity vector. - * For this animation, the alpha runs for a fixed duration and we update the position - * progressively. - */ - private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { - private DragLayer mDragLayer; - private PointF mVelocity; - private Rect mFrom; - private long mPrevTime; - private boolean mHasOffsetForScale; - private float mFriction; - - private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); - - public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, - long startTime, float friction) { - mDragLayer = dragLayer; - mVelocity = vel; - mFrom = from; - mPrevTime = startTime; - mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction); - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final DragView dragView = (DragView) mDragLayer.getAnimatedView(); - float t = ((Float) animation.getAnimatedValue()).floatValue(); - long curTime = AnimationUtils.currentAnimationTimeMillis(); - - if (!mHasOffsetForScale) { - mHasOffsetForScale = true; - float scale = dragView.getScaleX(); - float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; - float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; - - mFrom.left += xOffset; - mFrom.top += yOffset; - } - - mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); - mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); - dragView.setTranslationX(mFrom.left); - dragView.setTranslationY(mFrom.top); - dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); - - mVelocity.x *= mFriction; - mVelocity.y *= mFriction; - mPrevTime = curTime; + if (view != null) { + launcher.getWorkspace().removeWorkspaceItem(view); + launcher.getWorkspace().stripEmptyScreens(); } - }; - private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, - DragObject d, PointF vel, final long startTime, final int duration, - ViewConfiguration config) { - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime, - FLING_TO_DELETE_FRICTION); + return true; } - public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { - final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; - + @Override + public void onFlingToDelete(final DragObject d, PointF vel) { // Don't highlight the icon as it's animating d.dragView.setColor(0); d.dragView.updateInitialScaleToCurrentScale(); - // Don't highlight the target if we are flinging from AllApps - if (isAllApps) { - resetHoverColor(); - } - - if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { - // Defer animating out the drop target if we are animating to it - mSearchDropTargetBar.deferOnDragEnd(); - mSearchDropTargetBar.finishAnimations(); - } - final ViewConfiguration config = ViewConfiguration.get(mLauncher); final DragLayer dragLayer = mLauncher.getDragLayer(); - final int duration = FLING_DELETE_ANIMATION_DURATION; + FlingAnimation fling = new FlingAnimation(d, vel, + getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()), + dragLayer); + + final int duration = fling.getDuration(); final long startTime = AnimationUtils.currentAnimationTimeMillis(); // NOTE: Because it takes time for the first frame of animation to actually be @@ -527,28 +143,22 @@ public class DeleteDropTarget extends ButtonDropTarget { return Math.min(1f, mOffset + t); } }; - AnimatorUpdateListener updateCb = null; - if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { - updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); - } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { - updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, - duration, config); - } - deferCompleteDropIfUninstalling(d); Runnable onAnimationEndRunnable = new Runnable() { @Override public void run() { - // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up - // itself, otherwise, complete the drop to initiate the deletion process - if (!isAllApps) { - mLauncher.exitSpringLoadedDragMode(); - completeDrop(d); - } + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); mLauncher.getDragController().onDeferredEndFling(d); } }; - dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, + + dragLayer.animateView(d.dragView, fling, duration, tInterpolator, onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); } + + @Override + protected String getAccessibilityDropConfirmation() { + return getResources().getString(R.string.item_removed); + } } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index d6aadcee1..62b05b0d6 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -19,155 +19,103 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; import android.content.ComponentName; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Paint; import android.graphics.Paint.FontMetrics; import android.graphics.Point; -import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; -import android.view.Display; import android.view.Gravity; -import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; -import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; - - -class DeviceProfileQuery { - DeviceProfile profile; - float widthDps; - float heightDps; - float value; - PointF dimens; - - DeviceProfileQuery(DeviceProfile p, float v) { - widthDps = p.minWidthDps; - heightDps = p.minHeightDps; - value = v; - dimens = new PointF(widthDps, heightDps); - profile = p; - } -} - public class DeviceProfile { - public static interface DeviceProfileCallbacks { - public void onAvailableSizeChanged(DeviceProfile grid); - } - String name; - float minWidthDps; - float minHeightDps; - float numRows; - float numColumns; - float numHotseatIcons; - float iconSize; - private float iconTextSize; - private int iconDrawablePaddingOriginalPx; - private float hotseatIconSize; - - int defaultLayoutId; - - boolean isLandscape; - boolean isTablet; - boolean isLargeTablet; - boolean isLayoutRtl; - boolean transposeLayoutWithOrientation; - - int desiredWorkspaceLeftRightMarginPx; - int edgeMarginPx; - Rect defaultWidgetPadding; - - int widthPx; - int heightPx; - int availableWidthPx; - int availableHeightPx; - int defaultPageSpacingPx; - - int overviewModeMinIconZoneHeightPx; - int overviewModeMaxIconZoneHeightPx; - int overviewModeBarItemWidthPx; - int overviewModeBarSpacerWidthPx; - float overviewModeIconZoneRatio; - float overviewModeScaleFactor; - - int iconSizePx; - int iconTextSizePx; - int iconDrawablePaddingPx; - int cellWidthPx; - int cellHeightPx; - int allAppsIconSizePx; - int allAppsIconTextSizePx; - int allAppsCellWidthPx; - int allAppsCellHeightPx; - int allAppsCellPaddingPx; - int folderBackgroundOffset; - int folderIconSizePx; - int folderCellWidthPx; - int folderCellHeightPx; - int hotseatCellWidthPx; - int hotseatCellHeightPx; - int hotseatIconSizePx; - int hotseatBarHeightPx; - int hotseatAllAppsRank; - int allAppsNumRows; - int allAppsNumCols; - int searchBarSpaceWidthPx; - int searchBarSpaceHeightPx; - int pageIndicatorHeightPx; - int allAppsButtonVisualSize; - - float dragViewScale; - - int allAppsShortEdgeCount = -1; - int allAppsLongEdgeCount = -1; - - private ArrayList<DeviceProfileCallbacks> mCallbacks = new ArrayList<DeviceProfileCallbacks>(); - - DeviceProfile(String n, float w, float h, float r, float c, - float is, float its, float hs, float his, int dlId) { - // Ensure that we have an odd number of hotseat items (since we need to place all apps) - if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) { - throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); - } + public final InvariantDeviceProfile inv; + + // Device properties + public final boolean isTablet; + public final boolean isLargeTablet; + public final boolean isPhone; + public final boolean transposeLayoutWithOrientation; + + // Device properties in current orientation + public final boolean isLandscape; + public final int widthPx; + public final int heightPx; + public final int availableWidthPx; + public final int availableHeightPx; + + // Overview mode + private final int overviewModeMinIconZoneHeightPx; + private final int overviewModeMaxIconZoneHeightPx; + private final int overviewModeBarItemWidthPx; + private final int overviewModeBarSpacerWidthPx; + private final float overviewModeIconZoneRatio; + private final float overviewModeScaleFactor; + + // Workspace + private int desiredWorkspaceLeftRightMarginPx; + public final int edgeMarginPx; + public final Rect defaultWidgetPadding; + private final int pageIndicatorHeightPx; + private final int defaultPageSpacingPx; + private float dragViewScale; + + // Workspace icons + public int iconSizePx; + public int iconTextSizePx; + public int iconDrawablePaddingPx; + public int iconDrawablePaddingOriginalPx; + + public int cellWidthPx; + public int cellHeightPx; + + // Folder + public int folderBackgroundOffset; + public int folderIconSizePx; + public int folderCellWidthPx; + public int folderCellHeightPx; + + // Hotseat + public int hotseatCellWidthPx; + public int hotseatCellHeightPx; + public int hotseatIconSizePx; + private int hotseatBarHeightPx; + + // All apps + public int allAppsNumCols; + public int allAppsNumPredictiveCols; + public int allAppsButtonVisualSize; + public final int allAppsIconSizePx; + public final int allAppsIconTextSizePx; + + // QSB + private int searchBarSpaceWidthPx; + private int searchBarSpaceHeightPx; + + public DeviceProfile(Context context, InvariantDeviceProfile inv, + Point minSize, Point maxSize, + int width, int height, boolean isLandscape) { + + this.inv = inv; + this.isLandscape = isLandscape; - name = n; - minWidthDps = w; - minHeightDps = h; - numRows = r; - numColumns = c; - iconSize = is; - iconTextSize = its; - numHotseatIcons = hs; - hotseatIconSize = his; - defaultLayoutId = dlId; - } + Resources res = context.getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); - DeviceProfile() { - } + // Constants from resources + isTablet = res.getBoolean(R.bool.is_tablet); + isLargeTablet = res.getBoolean(R.bool.is_large_tablet); + isPhone = !isTablet && !isLargeTablet; - DeviceProfile(Context context, - ArrayList<DeviceProfile> profiles, - float minWidth, float minHeight, - int wPx, int hPx, - int awPx, int ahPx, - Resources res) { - DisplayMetrics dm = res.getDisplayMetrics(); - ArrayList<DeviceProfileQuery> points = - new ArrayList<DeviceProfileQuery>(); + // Some more constants transposeLayoutWithOrientation = res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); - minWidthDps = minWidth; - minHeightDps = minHeight; ComponentName cn = new ComponentName(context.getPackageName(), this.getClass().getName()); @@ -178,8 +126,6 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); defaultPageSpacingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing); - allAppsCellPaddingPx = - res.getDimensionPixelSize(R.dimen.dynamic_grid_all_apps_cell_padding); overviewModeMinIconZoneHeightPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height); overviewModeMaxIconZoneHeightPx = @@ -192,188 +138,71 @@ public class DeviceProfile { res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f; overviewModeScaleFactor = res.getInteger(R.integer.config_dynamic_grid_overview_scale_percentage) / 100f; - - // Find the closes profile given the width/height - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, 0f)); - } - DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points); - - // Snap to the closest row count - numRows = closestProfile.numRows; - - // Snap to the closest column count - numColumns = closestProfile.numColumns; - - // Snap to the closest hotseat size - numHotseatIcons = closestProfile.numHotseatIcons; - hotseatAllAppsRank = (int) (numHotseatIcons / 2); - - // Snap to the closest default layout id - defaultLayoutId = closestProfile.defaultLayoutId; - - // Interpolate the icon size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.iconSize)); - } - iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); - - // AllApps uses the original non-scaled icon size - allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); - - // Interpolate the icon text size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.iconTextSize)); - } - iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); iconDrawablePaddingOriginalPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); + // AllApps uses the original non-scaled icon text size - allAppsIconTextSizePx = DynamicGrid.pxFromDp(iconTextSize, dm); + allAppsIconTextSizePx = Utilities.pxFromDp(inv.iconTextSize, dm); - // Interpolate the hotseat icon size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.hotseatIconSize)); + // AllApps uses the original non-scaled icon size + allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm); + + // Determine sizes. + widthPx = width; + heightPx = height; + if (isLandscape) { + availableWidthPx = maxSize.x; + availableHeightPx = minSize.y; + } else { + availableWidthPx = minSize.x; + availableHeightPx = maxSize.y; } - // Hotseat - hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); - - // If the partner customization apk contains any grid overrides, apply them - applyPartnerDeviceProfileOverrides(context, dm); // Calculate the remaining vars - updateFromConfiguration(context, res, wPx, hPx, awPx, ahPx); - updateAvailableDimensions(context); + updateAvailableDimensions(dm, res); computeAllAppsButtonSize(context); } /** - * Apply any Partner customization grid overrides. - * - * Currently we support: all apps row / column count. - */ - private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) { - Partner p = Partner.get(ctx.getPackageManager()); - if (p != null) { - DeviceProfile partnerDp = p.getDeviceProfileOverride(dm); - if (partnerDp != null) { - if (partnerDp.numRows > 0 && partnerDp.numColumns > 0) { - numRows = partnerDp.numRows; - numColumns = partnerDp.numColumns; - } - if (partnerDp.allAppsShortEdgeCount > 0 && partnerDp.allAppsLongEdgeCount > 0) { - allAppsShortEdgeCount = partnerDp.allAppsShortEdgeCount; - allAppsLongEdgeCount = partnerDp.allAppsLongEdgeCount; - } - if (partnerDp.iconSize > 0) { - iconSize = partnerDp.iconSize; - // AllApps uses the original non-scaled icon size - allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); - } - } - } - } - - /** * Determine the exact visual footprint of the all apps button, taking into account scaling * and internal padding of the drawable. */ private void computeAllAppsButtonSize(Context context) { Resources res = context.getResources(); float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f; - LauncherAppState app = LauncherAppState.getInstance(); allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)); } - void addCallback(DeviceProfileCallbacks cb) { - mCallbacks.add(cb); - cb.onAvailableSizeChanged(this); - } - void removeCallback(DeviceProfileCallbacks cb) { - mCallbacks.remove(cb); - } - - private int getDeviceOrientation(Context context) { - WindowManager windowManager = (WindowManager) - context.getSystemService(Context.WINDOW_SERVICE); - Resources resources = context.getResources(); - DisplayMetrics dm = resources.getDisplayMetrics(); - Configuration config = resources.getConfiguration(); - int rotation = windowManager.getDefaultDisplay().getRotation(); - - boolean isLandscape = (config.orientation == Configuration.ORIENTATION_LANDSCAPE) && - (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180); - boolean isRotatedPortrait = (config.orientation == Configuration.ORIENTATION_PORTRAIT) && - (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270); - if (isLandscape || isRotatedPortrait) { - return CellLayout.LANDSCAPE; - } else { - return CellLayout.PORTRAIT; - } - } - - private void updateAvailableDimensions(Context context) { - WindowManager windowManager = (WindowManager) - context.getSystemService(Context.WINDOW_SERVICE); - Display display = windowManager.getDefaultDisplay(); - Resources resources = context.getResources(); - DisplayMetrics dm = resources.getDisplayMetrics(); - Configuration config = resources.getConfiguration(); - - // There are three possible configurations that the dynamic grid accounts for, portrait, - // landscape with the nav bar at the bottom, and landscape with the nav bar at the side. - // To prevent waiting for fitSystemWindows(), we make the observation that in landscape, - // the height is the smallest height (either with the nav bar at the bottom or to the - // side) and otherwise, the height is simply the largest possible height for a portrait - // device. - Point size = new Point(); - Point smallestSize = new Point(); - Point largestSize = new Point(); - display.getSize(size); - display.getCurrentSizeRange(smallestSize, largestSize); - availableWidthPx = size.x; - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - availableHeightPx = smallestSize.y; - } else { - availableHeightPx = largestSize.y; - } - + private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { // Check to see if the icons fit in the new available height. If not, then we need to // shrink the icon size. float scale = 1f; int drawablePadding = iconDrawablePaddingOriginalPx; - updateIconSize(1f, drawablePadding, resources, dm); - float usedHeight = (cellHeightPx * numRows); + updateIconSize(1f, drawablePadding, res, dm); + float usedHeight = (cellHeightPx * inv.numRows); - Rect workspacePadding = getWorkspacePadding(); + // We only care about the top and bottom workspace padding, which is not affected by RTL. + Rect workspacePadding = getWorkspacePadding(false /* isLayoutRtl */); int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom); if (usedHeight > maxHeight) { scale = maxHeight / usedHeight; drawablePadding = 0; } - updateIconSize(scale, drawablePadding, resources, dm); - - // Make the callbacks - for (DeviceProfileCallbacks cb : mCallbacks) { - cb.onAvailableSizeChanged(this); - } + updateIconSize(scale, drawablePadding, res, dm); } - private void updateIconSize(float scale, int drawablePadding, Resources resources, + private void updateIconSize(float scale, int drawablePadding, Resources res, DisplayMetrics dm) { - iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale); - iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale); + iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale); + iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale); iconDrawablePaddingPx = drawablePadding; - hotseatIconSizePx = (int) (DynamicGrid.pxFromDp(hotseatIconSize, dm) * scale); + hotseatIconSizePx = (int) (Utilities.pxFromDp(inv.hotseatIconSize, dm) * scale); // Search Bar searchBarSpaceWidthPx = Math.min(widthPx, - resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); searchBarSpaceHeightPx = getSearchBarTopOffset() - + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); // Calculate the actual text height Paint textPaint = new Paint(); @@ -381,7 +210,7 @@ public class DeviceProfile { FontMetrics fm = textPaint.getFontMetrics(); cellWidthPx = iconSizePx; cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top); - final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); dragViewScale = (iconSizePx + scaleDps) / iconSizePx; // Hotseat @@ -394,123 +223,27 @@ public class DeviceProfile { folderCellHeightPx = cellHeightPx + edgeMarginPx; folderBackgroundOffset = -edgeMarginPx; folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; - - // All Apps - allAppsCellWidthPx = allAppsIconSizePx; - allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx; - int maxLongEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); - int maxShortEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); - int minEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); - int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount); - int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount); - - if (allAppsShortEdgeCount > 0 && allAppsLongEdgeCount > 0) { - allAppsNumRows = isLandscape ? allAppsShortEdgeCount : allAppsLongEdgeCount; - allAppsNumCols = isLandscape ? allAppsLongEdgeCount : allAppsShortEdgeCount; - } else { - allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / - (allAppsCellHeightPx + allAppsCellPaddingPx); - allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); - allAppsNumCols = (availableWidthPx) / - (allAppsCellWidthPx + allAppsCellPaddingPx); - allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); - } - } - - void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, - int awPx, int ahPx) { - Configuration configuration = resources.getConfiguration(); - isLandscape = (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE); - isTablet = resources.getBoolean(R.bool.is_tablet); - isLargeTablet = resources.getBoolean(R.bool.is_large_tablet); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - isLayoutRtl = (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - } else { - isLayoutRtl = false; - } - widthPx = wPx; - heightPx = hPx; - availableWidthPx = awPx; - availableHeightPx = ahPx; - - updateAvailableDimensions(context); } - private float dist(PointF p0, PointF p1) { - return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + - (p1.y-p0.y)*(p1.y-p0.y)); - } - - private float weight(PointF a, PointF b, - float pow) { - float d = dist(a, b); - if (d == 0f) { - return Float.POSITIVE_INFINITY; - } - return (float) (1f / Math.pow(d, pow)); - } - - /** Returns the closest device profile given the width and height and a list of profiles */ - private DeviceProfile findClosestDeviceProfile(float width, float height, - ArrayList<DeviceProfileQuery> points) { - return findClosestDeviceProfiles(width, height, points).get(0).profile; - } - - /** Returns the closest device profiles ordered by closeness to the specified width and height */ - private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height, - ArrayList<DeviceProfileQuery> points) { - final PointF xy = new PointF(width, height); - - // Sort the profiles by their closeness to the dimensions - ArrayList<DeviceProfileQuery> pointsByNearness = points; - Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() { - public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { - return (int) (dist(xy, a.dimens) - dist(xy, b.dimens)); - } - }); - - return pointsByNearness; - } - - private float invDistWeightedInterpolate(float width, float height, - ArrayList<DeviceProfileQuery> points) { - float sum = 0; - float weights = 0; - float pow = 5; - float kNearestNeighbors = 3; - final PointF xy = new PointF(width, height); - - ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height, - points); - - for (int i = 0; i < pointsByNearness.size(); ++i) { - DeviceProfileQuery p = pointsByNearness.get(i); - if (i < kNearestNeighbors) { - float w = weight(xy, p.dimens, pow); - if (w == Float.POSITIVE_INFINITY) { - return p.value; - } - weights += w; - } - } - - for (int i = 0; i < pointsByNearness.size(); ++i) { - DeviceProfileQuery p = pointsByNearness.get(i); - if (i < kNearestNeighbors) { - float w = weight(xy, p.dimens, pow); - sum += w * p.value / weights; - } - } - - return sum; + /** + * @param recyclerViewWidth the available width of the AllAppsRecyclerView + */ + public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) { + int appsViewLeftMarginPx = + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + int allAppsCellWidthGap = + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); + int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; + int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + (allAppsIconSizePx + allAppsCellWidthGap); + int numPredictiveAppCols = Math.max(inv.minAllAppsPredictionColumns, numAppsCols); + allAppsNumCols = numAppsCols; + allAppsNumPredictiveCols = numPredictiveAppCols; } /** Returns the search bar top offset */ - int getSearchBarTopOffset() { - if (isTablet() && !isVerticalBarLayout()) { + private int getSearchBarTopOffset() { + if (isTablet && !isVerticalBarLayout()) { return 4 * edgeMarginPx; } else { return 2 * edgeMarginPx; @@ -518,14 +251,9 @@ public class DeviceProfile { } /** Returns the search bar bounds in the current orientation */ - Rect getSearchBarBounds() { - return getSearchBarBounds(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT); - } - /** Returns the search bar bounds in the specified orientation */ - Rect getSearchBarBounds(int orientation) { + public Rect getSearchBarBounds(boolean isLayoutRtl) { Rect bounds = new Rect(); - if (orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) { + if (isLandscape && transposeLayoutWithOrientation) { if (isLayoutRtl) { bounds.set(availableWidthPx - searchBarSpaceHeightPx, edgeMarginPx, availableWidthPx, availableHeightPx - edgeMarginPx); @@ -534,16 +262,14 @@ public class DeviceProfile { availableHeightPx - edgeMarginPx); } } else { - if (isTablet()) { + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons - int width = (orientation == CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); + int width = getCurrentWidth(); // XXX: If the icon size changes across orientations, we will have to take // that into account here too. int gap = (int) ((width - 2 * edgeMarginPx - - (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); + (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1))); bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(), availableWidthPx - (edgeMarginPx + gap), searchBarSpaceHeightPx); @@ -557,36 +283,11 @@ public class DeviceProfile { return bounds; } - /** Returns the bounds of the workspace page indicators. */ - Rect getWorkspacePageIndicatorBounds(Rect insets) { - Rect workspacePadding = getWorkspacePadding(); - if (isLandscape && transposeLayoutWithOrientation) { - if (isLayoutRtl) { - return new Rect(workspacePadding.left, workspacePadding.top, - workspacePadding.left + pageIndicatorHeightPx, - heightPx - workspacePadding.bottom - insets.bottom); - } else { - int pageIndicatorLeft = widthPx - workspacePadding.right; - return new Rect(pageIndicatorLeft, workspacePadding.top, - pageIndicatorLeft + pageIndicatorHeightPx, - heightPx - workspacePadding.bottom - insets.bottom); - } - } else { - int pageIndicatorTop = heightPx - insets.bottom - workspacePadding.bottom; - return new Rect(workspacePadding.left, pageIndicatorTop, - widthPx - workspacePadding.right, pageIndicatorTop + pageIndicatorHeightPx); - } - } - /** Returns the workspace padding in the specified orientation */ - Rect getWorkspacePadding() { - return getWorkspacePadding(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT); - } - Rect getWorkspacePadding(int orientation) { - Rect searchBarBounds = getSearchBarBounds(orientation); + Rect getWorkspacePadding(boolean isLayoutRtl) { + Rect searchBarBounds = getSearchBarBounds(isLayoutRtl); Rect padding = new Rect(); - if (orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) { + if (isLandscape && transposeLayoutWithOrientation) { // Pad the left and right of the workspace with search/hotseat bar sizes if (isLayoutRtl) { padding.set(hotseatBarHeightPx, edgeMarginPx, @@ -596,22 +297,18 @@ public class DeviceProfile { hotseatBarHeightPx, edgeMarginPx); } } else { - if (isTablet()) { + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons float gapScale = 1f + (dragViewScale - 1f) / 2f; - int width = (orientation == CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); - int height = (orientation != CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); + int width = getCurrentWidth(); + int height = getCurrentHeight(); int paddingTop = searchBarBounds.bottom; int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx; - int availableWidth = Math.max(0, width - (int) ((numColumns * cellWidthPx) + - (numColumns * gapScale * cellWidthPx))); + int availableWidth = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) + + (inv.numColumns * gapScale * cellWidthPx))); int availableHeight = Math.max(0, height - paddingTop - paddingBottom - - (int) (2 * numRows * cellHeightPx)); + - (int) (2 * inv.numRows * cellHeightPx)); padding.set(availableWidth / 2, paddingTop + availableHeight / 2, availableWidth / 2, paddingBottom + availableHeight / 2); } else { @@ -625,16 +322,15 @@ public class DeviceProfile { return padding; } - int getWorkspacePageSpacing(int orientation) { - if ((orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) || isLargeTablet()) { + private int getWorkspacePageSpacing(boolean isLayoutRtl) { + if ((isLandscape && transposeLayoutWithOrientation) || isLargeTablet) { // In landscape mode the page spacing is set to the default. return defaultPageSpacingPx; } else { // In portrait, we want the pages spaced such that there is no // overhang of the previous / next page into the current page viewport. // We assume symmetrical padding in portrait mode. - return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding().left); + return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding(isLayoutRtl).left); } } @@ -645,8 +341,8 @@ public class DeviceProfile { return new Rect(0, availableHeightPx - zoneHeight, 0, availableHeightPx); } - float getOverviewModeScale() { - Rect workspacePadding = getWorkspacePadding(); + public float getOverviewModeScale(boolean isLayoutRtl) { + Rect workspacePadding = getWorkspacePadding(isLayoutRtl); Rect overviewBar = getOverviewModeButtonBarRect(); int pageSpace = availableHeightPx - workspacePadding.top - workspacePadding.bottom; return (overviewModeScaleFactor * (pageSpace - overviewBar.height())) / pageSpace; @@ -663,32 +359,26 @@ public class DeviceProfile { } } - int calculateCellWidth(int width, int countX) { + public static int calculateCellWidth(int width, int countX) { return width / countX; } - int calculateCellHeight(int height, int countY) { + public static int calculateCellHeight(int height, int countY) { return height / countY; } - boolean isPhone() { - return !isTablet && !isLargeTablet; - } - boolean isTablet() { - return isTablet; - } - boolean isLargeTablet() { - return isLargeTablet; - } - + /** + * When {@code true}, hotseat is on the bottom row when in landscape mode. + * If {@code false}, hotseat is on the right column when in landscape mode. + */ boolean isVerticalBarLayout() { return isLandscape && transposeLayoutWithOrientation; } boolean shouldFadeAdjacentWorkspaceScreens() { - return isVerticalBarLayout() || isLargeTablet(); + return isVerticalBarLayout() || isLargeTablet; } - int getVisibleChildCount(ViewGroup parent) { + private int getVisibleChildCount(ViewGroup parent) { int visibleChildren = 0; for (int i = 0; i < parent.getChildCount(); i++) { if (parent.getChildAt(i).getVisibility() != View.GONE) { @@ -700,25 +390,31 @@ public class DeviceProfile { public void layout(Launcher launcher) { FrameLayout.LayoutParams lp; - Resources res = launcher.getResources(); boolean hasVerticalBarLayout = isVerticalBarLayout(); + final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); // Layout the search bar space View searchBar = launcher.getSearchBar(); lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); if (hasVerticalBarLayout) { - // Vertical search bar space - lp.gravity = Gravity.TOP | Gravity.LEFT; + // Vertical search bar space -- The search bar is fixed in the layout to be on the left + // of the screen regardless of RTL + lp.gravity = Gravity.LEFT; lp.width = searchBarSpaceHeightPx; - lp.height = LayoutParams.WRAP_CONTENT; LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); targets.setOrientation(LinearLayout.VERTICAL); + FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams(); + targetsLp.gravity = Gravity.TOP; + targetsLp.height = LayoutParams.WRAP_CONTENT; + } else { // Horizontal search bar space - lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - lp.width = searchBarSpaceWidthPx; + lp.gravity = Gravity.TOP; lp.height = searchBarSpaceHeightPx; + + LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); + targets.getLayoutParams().width = searchBarSpaceWidthPx; } searchBar.setLayoutParams(lp); @@ -726,22 +422,22 @@ public class DeviceProfile { PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace); lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); lp.gravity = Gravity.CENTER; - int orientation = isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT; - Rect padding = getWorkspacePadding(orientation); + Rect padding = getWorkspacePadding(isLayoutRtl); workspace.setLayoutParams(lp); workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom); - workspace.setPageSpacing(getWorkspacePageSpacing(orientation)); + workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl)); // Layout the hotseat View hotseat = launcher.findViewById(R.id.hotseat); lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); if (hasVerticalBarLayout) { - // Vertical hotseat - lp.gravity = Gravity.END; + // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the + // screen regardless of RTL + lp.gravity = Gravity.RIGHT; lp.width = hotseatBarHeightPx; lp.height = LayoutParams.MATCH_PARENT; hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx); - } else if (isTablet()) { + } else if (isTablet) { // Pad the hotseat with the workspace padding calculated above lp.gravity = Gravity.BOTTOM; lp.width = LayoutParams.MATCH_PARENT; @@ -777,64 +473,6 @@ public class DeviceProfile { } } - // Layout AllApps - AppsCustomizeTabHost host = (AppsCustomizeTabHost) - launcher.findViewById(R.id.apps_customize_pane); - if (host != null) { - // Center the all apps page indicator - int pageIndicatorHeight = (int) (pageIndicatorHeightPx * Math.min(1f, - (allAppsIconSizePx / DynamicGrid.DEFAULT_ICON_SIZE_PX))); - pageIndicator = host.findViewById(R.id.apps_customize_page_indicator); - if (pageIndicator != null) { - LinearLayout.LayoutParams lllp = (LinearLayout.LayoutParams) pageIndicator.getLayoutParams(); - lllp.width = LayoutParams.WRAP_CONTENT; - lllp.height = pageIndicatorHeight; - pageIndicator.setLayoutParams(lllp); - } - - AppsCustomizePagedView pagedView = (AppsCustomizePagedView) - host.findViewById(R.id.apps_customize_pane_content); - - FrameLayout fakePageContainer = (FrameLayout) - host.findViewById(R.id.fake_page_container); - FrameLayout fakePage = (FrameLayout) host.findViewById(R.id.fake_page); - - padding = new Rect(); - if (pagedView != null) { - // Constrain the dimensions of all apps so that it does not span the full width - int paddingLR = (availableWidthPx - (allAppsCellWidthPx * allAppsNumCols)) / - (2 * (allAppsNumCols + 1)); - int paddingTB = (availableHeightPx - (allAppsCellHeightPx * allAppsNumRows)) / - (2 * (allAppsNumRows + 1)); - paddingLR = Math.min(paddingLR, (int)((paddingLR + paddingTB) * 0.75f)); - paddingTB = Math.min(paddingTB, (int)((paddingLR + paddingTB) * 0.75f)); - int maxAllAppsWidth = (allAppsNumCols * (allAppsCellWidthPx + 2 * paddingLR)); - int gridPaddingLR = (availableWidthPx - maxAllAppsWidth) / 2; - // Only adjust the side paddings on landscape phones, or tablets - if ((isTablet() || isLandscape) && gridPaddingLR > (allAppsCellWidthPx / 4)) { - padding.left = padding.right = gridPaddingLR; - } - - // The icons are centered, so we can't just offset by the page indicator height - // because the empty space will actually be pageIndicatorHeight + paddingTB - padding.bottom = Math.max(0, pageIndicatorHeight - paddingTB); - - pagedView.setWidgetsPageIndicatorPadding(pageIndicatorHeight); - fakePage.setBackground(res.getDrawable(R.drawable.quantum_panel)); - - // Horizontal padding for the whole paged view - int pagedFixedViewPadding = - res.getDimensionPixelSize(R.dimen.apps_customize_horizontal_padding); - - padding.left += pagedFixedViewPadding; - padding.right += pagedFixedViewPadding; - - pagedView.setPadding(padding.left, padding.top, padding.right, padding.bottom); - fakePageContainer.setPadding(padding.left, padding.top, padding.right, padding.bottom); - - } - } - // Layout the Overview Mode ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { @@ -875,4 +513,16 @@ public class DeviceProfile { } } } + + private int getCurrentWidth() { + return isLandscape + ? Math.max(widthPx, heightPx) + : Math.min(widthPx, heightPx); + } + + private int getCurrentHeight() { + return isLandscape + ? Math.min(widthPx, heightPx) + : Math.max(widthPx, heightPx); + } } diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 480dce999..2191455d5 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -34,6 +34,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.InputMethodManager; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.HashSet; @@ -49,8 +51,8 @@ public class DragController { /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; - private static final int SCROLL_DELAY = 500; - private static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; + public static final int SCROLL_DELAY = 500; + public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; @@ -63,16 +65,20 @@ public class DragController { private static final float MAX_FLING_DEGREES = 35f; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private Handler mHandler; // temporaries to avoid gc thrash private Rect mRectTemp = new Rect(); private final int[] mCoordinatesTemp = new int[2]; + private final boolean mIsRtl; /** Whether or not we're dragging. */ private boolean mDragging; + /** Whether or not this is an accessible drag operation */ + private boolean mIsAccessibleDrag; + /** X coordinate of the down event. */ private int mMotionDownX; @@ -99,17 +105,17 @@ public class DragController { private View mMoveTarget; - private DragScroller mDragScroller; - private int mScrollState = SCROLL_OUTSIDE_ZONE; + @Thunk DragScroller mDragScroller; + @Thunk int mScrollState = SCROLL_OUTSIDE_ZONE; private ScrollRunnable mScrollRunnable = new ScrollRunnable(); private DropTarget mLastDropTarget; private InputMethodManager mInputMethodManager; - private int mLastTouch[] = new int[2]; - private long mLastTouchUpTime = -1; - private int mDistanceSinceScroll = 0; + @Thunk int mLastTouch[] = new int[2]; + @Thunk long mLastTouchUpTime = -1; + @Thunk int mDistanceSinceScroll = 0; private int mTmpPoint[] = new int[2]; private Rect mDragLayerRect = new Rect(); @@ -120,7 +126,7 @@ public class DragController { /** * Interface to receive notifications when a drag starts or stops */ - interface DragListener { + public interface DragListener { /** * A drag has begun * @@ -152,6 +158,7 @@ public class DragController { float density = r.getDisplayMetrics().density; mFlingToDeleteThresholdVelocity = (int) (r.getInteger(R.integer.config_flingToDeleteMinVelocity) * density); + mIsRtl = Utilities.isRtl(r); } public boolean dragging() { @@ -167,22 +174,21 @@ public class DragController { * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} + * @param viewImageBounds the position of the image inside the view * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border */ - public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, - Point extraPadding, float initialDragViewScale) { + public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, + Rect viewImageBounds, int dragAction, float initialDragViewScale) { int[] loc = mCoordinatesTemp; mLauncher.getDragLayer().getLocationInDragLayer(v, loc); - int viewExtraPaddingLeft = extraPadding != null ? extraPadding.x : 0; - int viewExtraPaddingTop = extraPadding != null ? extraPadding.y : 0; - int dragLayerX = loc[0] + v.getPaddingLeft() + viewExtraPaddingLeft + - (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); - int dragLayerY = loc[1] + v.getPaddingTop() + viewExtraPaddingTop + - (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); + int dragLayerX = loc[0] + viewImageBounds.left + + (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); + int dragLayerY = loc[1] + viewImageBounds.top + + (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, - null, initialDragViewScale); + null, initialDragViewScale, false); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); @@ -202,10 +208,11 @@ public class DragController { * {@link #DRAG_ACTION_COPY} * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border + * @param accessible whether this drag should occur in accessibility mode */ public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, - float initialDragViewScale) { + float initialDragViewScale, boolean accessible) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } @@ -228,12 +235,21 @@ public class DragController { final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mDragging = true; + mIsAccessibleDrag = accessible; mDragObject = new DropTarget.DragObject(); mDragObject.dragComplete = false; - mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); - mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + if (mIsAccessibleDrag) { + // For an accessible drag, we assume the view is being dragged from the center. + mDragObject.xOffset = b.getWidth() / 2; + mDragObject.yOffset = b.getHeight() / 2; + mDragObject.accessibleDrag = true; + } else { + mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + } + mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; @@ -349,6 +365,7 @@ public class DragController { private void endDrag() { if (mDragging) { mDragging = false; + mIsAccessibleDrag = false; clearScrollRunnable(); boolean isDeferred = false; if (mDragObject.dragView != null) { @@ -361,7 +378,7 @@ public class DragController { // Only end the drag if we are not deferred if (!isDeferred) { - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } @@ -378,13 +395,13 @@ public class DragController { if (mDragObject.deferDragViewCleanupPostAnimation) { // If we skipped calling onDragEnd() before, do it now - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } } - void onDeferredEndFling(DropTarget.DragObject d) { + public void onDeferredEndFling(DropTarget.DragObject d) { d.dragSource.onFlingToDeleteCompleted(); } @@ -421,6 +438,10 @@ public class DragController { + mDragging); } + if (mIsAccessibleDrag) { + return false; + } + // Update the velocity tracker acquireVelocityTrackerAndAddMovement(ev); @@ -442,7 +463,7 @@ public class DragController { mLastTouchUpTime = System.currentTimeMillis(); if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -493,8 +514,7 @@ public class DragController { checkTouchMove(dropTarget); // Check if we are hovering over the scroll areas - mDistanceSinceScroll += - Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); + mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); mLastTouch[0] = x; mLastTouch[1] = y; checkScrollState(x, y); @@ -525,13 +545,12 @@ public class DragController { mLastDropTarget = dropTarget; } - private void checkScrollState(int x, int y) { + @Thunk void checkScrollState(int x, int y) { final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY; final DragLayer dragLayer = mLauncher.getDragLayer(); - final boolean isRtl = (dragLayer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - final int forwardDirection = isRtl ? SCROLL_RIGHT : SCROLL_LEFT; - final int backwardsDirection = isRtl ? SCROLL_LEFT : SCROLL_RIGHT; + final int forwardDirection = mIsRtl ? SCROLL_RIGHT : SCROLL_LEFT; + final int backwardsDirection = mIsRtl ? SCROLL_LEFT : SCROLL_RIGHT; if (x < mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { @@ -560,7 +579,7 @@ public class DragController { * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { - if (!mDragging) { + if (!mDragging || mIsAccessibleDrag) { return false; } @@ -596,7 +615,7 @@ public class DragController { if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -617,6 +636,35 @@ public class DragController { } /** + * Since accessible drag and drop won't cause the same sequence of touch events, we manually + * inject the appropriate state. + */ + public void prepareAccessibleDrag(int x, int y) { + mMotionDownX = x; + mMotionDownY = y; + mLastDropTarget = null; + } + + /** + * As above, since accessible drag and drop won't cause the same sequence of touch events, + * we manually ensure appropriate drag and drop events get emulated for accessible drag. + */ + public void completeAccessibleDrag(int[] location) { + final int[] coordinates = mCoordinatesTemp; + + // We make sure that we prime the target for drop. + DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + checkTouchMove(dropTarget); + + dropTarget.prepareAccessibilityDrop(); + // Perform the drop + drop(location[0], location[1]); + endDrag(); + } + + /** * Determines whether the user flung the current item to delete it. * * @return the vector at which the item was flung, or null if no fling was detected. @@ -662,8 +710,7 @@ public class DragController { mDragObject.dragComplete = true; mFlingToDeleteDropTarget.onDragExit(mDragObject); if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) { - mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y, - vel); + mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, vel); accepted = true; } mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true, diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index a352b7914..aaa14e6a6 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -24,6 +24,7 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; @@ -38,7 +39,8 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; -import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -46,30 +48,34 @@ import java.util.ArrayList; * A ViewGroup that coordinates dragging across its descendants */ public class DragLayer extends InsettableFrameLayout { - private DragController mDragController; - private int[] mTmpXY = new int[2]; + + public static final int ANIMATION_END_DISAPPEAR = 0; + public static final int ANIMATION_END_REMAIN_VISIBLE = 2; + + // Scrim color without any alpha component. + private static final int SCRIM_COLOR = Color.BLACK & 0x00FFFFFF; + + private final int[] mTmpXY = new int[2]; + + @Thunk DragController mDragController; private int mXDown, mYDown; private Launcher mLauncher; // Variables relating to resizing widgets - private final ArrayList<AppWidgetResizeFrame> mResizeFrames = - new ArrayList<AppWidgetResizeFrame>(); + private final ArrayList<AppWidgetResizeFrame> mResizeFrames = new ArrayList<>(); + private final boolean mIsRtl; private AppWidgetResizeFrame mCurrentResizeFrame; // Variables relating to animation of views after drop private ValueAnimator mDropAnim = null; - private ValueAnimator mFadeOutAnim = null; - private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); - private DragView mDropView = null; - private int mAnchorViewInitialScrollX = 0; - private View mAnchorView = null; + private final TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); + @Thunk DragView mDropView = null; + @Thunk int mAnchorViewInitialScrollX = 0; + @Thunk View mAnchorView = null; private boolean mHoverPointClosesFolder = false; - private Rect mHitRect = new Rect(); - public static final int ANIMATION_END_DISAPPEAR = 0; - public static final int ANIMATION_END_FADE_OUT = 1; - public static final int ANIMATION_END_REMAIN_VISIBLE = 2; + private final Rect mHitRect = new Rect(); private TouchCompleteListener mTouchCompleteListener; @@ -78,10 +84,10 @@ public class DragLayer extends InsettableFrameLayout { private int mChildCountOnLastUpdate = -1; // Darkening scrim - private Drawable mBackground; private float mBackgroundAlpha = 0; // Related to adjacent page hints + private final Rect mScrollChildPosition = new Rect(); private boolean mInScrollArea; private boolean mShowPageHints; private Drawable mLeftHoverDrawable; @@ -109,7 +115,7 @@ public class DragLayer extends InsettableFrameLayout { mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right); mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active); mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active); - mBackground = res.getDrawable(R.drawable.apps_customize_bg); + mIsRtl = Utilities.isRtl(res); } public void setup(Launcher launcher, DragController controller) { @@ -152,6 +158,14 @@ public class DragLayer extends InsettableFrameLayout { return false; } + private boolean isEventOverDropTargetBar(MotionEvent ev) { + getDescendantRectRelativeToSelf(mLauncher.getSearchBar(), mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + public void setBlockTouch(boolean block) { mBlockTouches = block; } @@ -187,10 +201,16 @@ public class DragLayer extends InsettableFrameLayout { } } - getDescendantRectRelativeToSelf(currentFolder, hitRect); if (!isEventOverFolder(currentFolder, ev)) { - mLauncher.closeFolder(); - return true; + if (isInAccessibleDrag()) { + // Do not close the folder if in drag and drop. + if (!isEventOverDropTargetBar(ev)) { + return true; + } + } else { + mLauncher.closeFolder(); + return true; + } } } return false; @@ -227,11 +247,12 @@ public class DragLayer extends InsettableFrameLayout { getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isTouchExplorationEnabled()) { final int action = ev.getAction(); - boolean isOverFolder; + boolean isOverFolderOrSearchBar; switch (action) { case MotionEvent.ACTION_HOVER_ENTER: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; @@ -239,12 +260,13 @@ public class DragLayer extends InsettableFrameLayout { mHoverPointClosesFolder = false; break; case MotionEvent.ACTION_HOVER_MOVE: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder && !mHoverPointClosesFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; - } else if (!isOverFolder) { + } else if (!isOverFolderOrSearchBar) { return true; } mHoverPointClosesFolder = false; @@ -267,6 +289,12 @@ public class DragLayer extends InsettableFrameLayout { } } + private boolean isInAccessibleDrag() { + LauncherAccessibilityDelegate delegate = LauncherAppState + .getInstance().getAccessibilityDelegate(); + return delegate != null && delegate.isInAccessibleDrag(); + } + @Override public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); @@ -274,6 +302,10 @@ public class DragLayer extends InsettableFrameLayout { if (child == currentFolder) { return super.onRequestSendAccessibilityEvent(child, event); } + + if (isInAccessibleDrag() && child instanceof SearchDropTargetBar) { + return super.onRequestSendAccessibilityEvent(child, event); + } // Skip propagating onRequestSendAccessibilityEvent all for other children // when a folder is open return false; @@ -287,6 +319,10 @@ public class DragLayer extends InsettableFrameLayout { if (currentFolder != null) { // Only add the folder as a child for accessibility when it is open childrenForAccessibility.add(currentFolder); + + if (isInAccessibleDrag()) { + childrenForAccessibility.add(mLauncher.getSearchBar()); + } } else { super.addChildrenForAccessibility(childrenForAccessibility); } @@ -656,8 +692,7 @@ public class DragLayer extends InsettableFrameLayout { final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { // Calculate the duration of the animation based on the object's distance - final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + - Math.pow(to.top - from.top, 2)); + final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top); final Resources res = getResources(); final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); @@ -725,7 +760,6 @@ public class DragLayer extends InsettableFrameLayout { final int animationEndStyle, View anchorView) { // Clean up the previous animations if (mDropAnim != null) mDropAnim.cancel(); - if (mFadeOutAnim != null) mFadeOutAnim.cancel(); // Show the drop view if it was previously hidden mDropView = view; @@ -753,9 +787,6 @@ public class DragLayer extends InsettableFrameLayout { case ANIMATION_END_DISAPPEAR: clearAnimatedView(); break; - case ANIMATION_END_FADE_OUT: - fadeOutDragView(); - break; case ANIMATION_END_REMAIN_VISIBLE: break; } @@ -779,31 +810,6 @@ public class DragLayer extends InsettableFrameLayout { return mDropView; } - private void fadeOutDragView() { - mFadeOutAnim = new ValueAnimator(); - mFadeOutAnim.setDuration(150); - mFadeOutAnim.setFloatValues(0f, 1f); - mFadeOutAnim.removeAllUpdateListeners(); - mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - final float percent = (Float) animation.getAnimatedValue(); - - float alpha = 1 - percent; - mDropView.setAlpha(alpha); - } - }); - mFadeOutAnim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - if (mDropView != null) { - mDragController.onDeferredEndDrag(mDropView); - } - mDropView = null; - invalidate(); - } - }); - mFadeOutAnim.start(); - } - @Override public void onChildViewAdded(View parent, View child) { super.onChildViewAdded(parent, child); @@ -880,6 +886,9 @@ public class DragLayer extends InsettableFrameLayout { void showPageHints() { mShowPageHints = true; + Workspace workspace = mLauncher.getWorkspace(); + getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.numCustomPages()), + mScrollChildPosition); invalidate(); } @@ -888,21 +897,12 @@ public class DragLayer extends InsettableFrameLayout { invalidate(); } - /** - * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. - */ - private boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); - } - @Override protected void dispatchDraw(Canvas canvas) { - // Draw the background gradient below children. - if (mBackground != null && mBackgroundAlpha > 0.0f) { + // Draw the background below children. + if (mBackgroundAlpha > 0.0f) { int alpha = (int) (mBackgroundAlpha * 255); - mBackground.setAlpha(alpha); - mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); - mBackground.draw(canvas); + canvas.drawColor((alpha << 24) | SCRIM_COLOR); } super.dispatchDraw(canvas); @@ -912,27 +912,22 @@ public class DragLayer extends InsettableFrameLayout { if (mShowPageHints) { Workspace workspace = mLauncher.getWorkspace(); int width = getMeasuredWidth(); - Rect childRect = new Rect(); - getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.getChildCount() - 1), - childRect); - int page = workspace.getNextPage(); - final boolean isRtl = isLayoutRtl(); - CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1); - CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1); + CellLayout leftPage = (CellLayout) workspace.getChildAt(mIsRtl ? page + 1 : page - 1); + CellLayout rightPage = (CellLayout) workspace.getChildAt(mIsRtl ? page - 1 : page + 1); if (leftPage != null && leftPage.isDragTarget()) { Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ? mLeftHoverDrawableActive : mLeftHoverDrawable; - left.setBounds(0, childRect.top, - left.getIntrinsicWidth(), childRect.bottom); + left.setBounds(0, mScrollChildPosition.top, + left.getIntrinsicWidth(), mScrollChildPosition.bottom); left.draw(canvas); } if (rightPage != null && rightPage.isDragTarget()) { Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ? mRightHoverDrawableActive : mRightHoverDrawable; right.setBounds(width - right.getIntrinsicWidth(), - childRect.top, width, childRect.bottom); + mScrollChildPosition.top, width, mScrollChildPosition.bottom); right.draw(canvas); } } diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java index 7369eeac2..2a1346ef5 100644 --- a/src/com/android/launcher3/DragSource.java +++ b/src/com/android/launcher3/DragSource.java @@ -22,9 +22,9 @@ import com.android.launcher3.DropTarget.DragObject; /** * Interface defining an object that can originate a drag. - * */ public interface DragSource { + /** * @return whether items dragged from this source supports */ diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index ea34e46f9..dfa8202a7 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -16,25 +16,35 @@ package com.android.launcher3; +import android.animation.FloatArrayEvaluator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Point; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; +import android.os.Build; import android.view.View; import android.view.animation.DecelerateInterpolator; +import com.android.launcher3.util.Thunk; + +import java.util.Arrays; + public class DragView extends View { - private static float sDragAlpha = 1f; + public static int COLOR_CHANGE_DURATION = 120; + + @Thunk static float sDragAlpha = 1f; private Bitmap mBitmap; private Bitmap mCrossFadeBitmap; - private Paint mPaint; + @Thunk Paint mPaint; private int mRegistrationX; private int mRegistrationY; @@ -42,16 +52,19 @@ public class DragView extends View { private Rect mDragRegion = null; private DragLayer mDragLayer = null; private boolean mHasDrawn = false; - private float mCrossFadeProgress = 0f; + @Thunk float mCrossFadeProgress = 0f; ValueAnimator mAnim; - private float mOffsetX = 0.0f; - private float mOffsetY = 0.0f; + @Thunk float mOffsetX = 0.0f; + @Thunk float mOffsetY = 0.0f; private float mInitialScale = 1f; // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace // size. This is ignored for non-icons. private float mIntrinsicIconScale = 1f; + @Thunk float[] mCurrentFilter; + private ValueAnimator mFilterAnimator; + /** * Construct the drag view. * <p> @@ -63,6 +76,7 @@ public class DragView extends View { * @param registrationX The x coordinate of the registration point. * @param registrationY The y coordinate of the registration point. */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, int left, int top, int width, int height, final float initialScale) { super(launcher); @@ -70,8 +84,6 @@ public class DragView extends View { mInitialScale = initialScale; final Resources res = getResources(); - final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); - final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); final float scale = (width + scaleDps) / width; @@ -87,8 +99,8 @@ public class DragView extends View { public void onAnimationUpdate(ValueAnimator animation) { final float value = (Float) animation.getAnimatedValue(); - final int deltaX = (int) ((value * offsetX) - mOffsetX); - final int deltaY = (int) ((value * offsetY) - mOffsetY); + final int deltaX = (int) (-mOffsetX); + final int deltaY = (int) (-mOffsetY); mOffsetX += deltaX; mOffsetY += deltaY; @@ -118,6 +130,10 @@ public class DragView extends View { int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); measure(ms, ms); mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + if (Utilities.isLmpOrAbove()) { + setElevation(getResources().getDimension(R.dimen.drag_elevation)); + } } /** Sets the scale of the view over the normal workspace icon size. */ @@ -229,11 +245,49 @@ public class DragView extends View { mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); } if (color != 0) { - mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + ColorMatrix m1 = new ColorMatrix(); + m1.setSaturation(0); + + ColorMatrix m2 = new ColorMatrix(); + setColorScale(color, m2); + m1.postConcat(m2); + + if (Utilities.isLmpOrAbove()) { + animateFilterTo(m1.getArray()); + } else { + mPaint.setColorFilter(new ColorMatrixColorFilter(m1)); + invalidate(); + } } else { - mPaint.setColorFilter(null); + if (!Utilities.isLmpOrAbove() || mCurrentFilter == null) { + mPaint.setColorFilter(null); + invalidate(); + } else { + animateFilterTo(new ColorMatrix().getArray()); + } } - invalidate(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void animateFilterTo(float[] targetFilter) { + float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter; + mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length); + + if (mFilterAnimator != null) { + mFilterAnimator.cancel(); + } + mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter), + oldFilter, targetFilter); + mFilterAnimator.setDuration(COLOR_CHANGE_DURATION); + mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mPaint.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + invalidate(); + } + }); + mFilterAnimator.start(); } public boolean hasDrawn() { @@ -300,5 +354,9 @@ public class DragView extends View { mDragLayer.removeView(DragView.this); } } -} + public static void setColorScale(int color, ColorMatrix target) { + target.setScale(Color.red(color) / 255f, Color.green(color) / 255f, + Color.blue(color) / 255f, Color.alpha(color) / 255f); + } +} diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java index 64f0ac867..c8fac5466 100644 --- a/src/com/android/launcher3/DropTarget.java +++ b/src/com/android/launcher3/DropTarget.java @@ -16,10 +16,8 @@ package com.android.launcher3; -import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; -import android.util.Log; /** * Interface defining an object that can receive a drag. @@ -29,7 +27,7 @@ public interface DropTarget { public static final String TAG = "DropTarget"; - class DragObject { + public static class DragObject { public int x = -1; public int y = -1; @@ -54,6 +52,9 @@ public interface DropTarget { /** Where the drag originated */ public DragSource dragSource = null; + /** The object is part of an accessible drag operation */ + public boolean accessibleDrag; + /** Post drag animation runnable */ public Runnable postAnimationRunnable = null; @@ -65,42 +66,28 @@ public interface DropTarget { public DragObject() { } - } - - public static class DragEnforcer implements DragController.DragListener { - int dragParity = 0; - - public DragEnforcer(Context context) { - Launcher launcher = (Launcher) context; - launcher.getDragController().addDragListener(this); - } - void onDragEnter() { - dragParity++; - if (dragParity != 1) { - Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); - } - } + /** + * This is used to compute the visual center of the dragView. This point is then + * used to visualize drop locations and determine where to drop an item. The idea is that + * the visual center represents the user's interpretation of where the item is, and hence + * is the appropriate point to use when determining drop location. + */ + public final float[] getVisualCenter(float[] recycle) { + final float res[] = (recycle == null) ? new float[2] : recycle; - void onDragExit() { - dragParity--; - if (dragParity != 0) { - Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); - } - } + // These represent the visual top and left of drag view if a dragRect was provided. + // If a dragRect was not provided, then they correspond to the actual view left and + // top, as the dragRect is in that case taken to be the entire dragView. + // R.dimen.dragViewOffsetY. + int left = x - xOffset; + int top = y - yOffset; - @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - if (dragParity != 0) { - Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); - } - } + // In order to find the visual center, we shift by half the dragRect + res[0] = left + dragView.getDragRegion().width() / 2; + res[1] = top + dragView.getDragRegion().height() / 2; - @Override - public void onDragEnd() { - if (dragParity != 0) { - Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); - } + return res; } } @@ -113,7 +100,7 @@ public interface DropTarget { /** * Handle an object being dropped on the DropTarget - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location @@ -138,12 +125,12 @@ public interface DropTarget { * of onDrop(). (This is only called on objects that are set as the DragController's * fling-to-delete target. */ - void onFlingToDelete(DragObject dragObject, int x, int y, PointF vec); + void onFlingToDelete(DragObject dragObject, PointF vec); /** * Check if a drop action can occur at, or near, the requested location. * This will be called just before onDrop. - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location @@ -157,6 +144,8 @@ public interface DropTarget { */ boolean acceptDrop(DragObject dragObject); + void prepareAccessibilityDrop(); + // These methods are implemented in Views void getHitRectRelativeToDragLayer(Rect outRect); void getLocationInDragLayer(int[] loc); diff --git a/src/com/android/launcher3/DummyWidget.java b/src/com/android/launcher3/DummyWidget.java new file mode 100644 index 000000000..59cd80501 --- /dev/null +++ b/src/com/android/launcher3/DummyWidget.java @@ -0,0 +1,50 @@ +package com.android.launcher3; + +import android.appwidget.AppWidgetProviderInfo; + +public class DummyWidget implements CustomAppWidget { + @Override + public String getLabel() { + return "Dumb Launcher Widget"; + } + + @Override + public int getPreviewImage() { + return 0; + } + + @Override + public int getIcon() { + return 0; + } + + @Override + public int getWidgetLayout() { + return R.layout.dummy_widget; + } + + @Override + public int getSpanX() { + return 2; + } + + @Override + public int getSpanY() { + return 2; + } + + @Override + public int getMinSpanX() { + return 1; + } + + @Override + public int getMinSpanY() { + return 1; + } + + @Override + public int getResizeMode() { + return AppWidgetProviderInfo.RESIZE_BOTH; + } +} diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java deleted file mode 100644 index aa08148d2..000000000 --- a/src/com/android/launcher3/DynamicGrid.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2008 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.content.Context; -import android.content.res.Resources; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import java.util.ArrayList; - - -public class DynamicGrid { - @SuppressWarnings("unused") - private static final String TAG = "DynamicGrid"; - - private DeviceProfile mProfile; - private float mMinWidth; - private float mMinHeight; - - // This is a static that we use for the default icon size on a 4/5-inch phone - static float DEFAULT_ICON_SIZE_DP = 60; - static float DEFAULT_ICON_SIZE_PX = 0; - - public static float dpiFromPx(int size, DisplayMetrics metrics){ - float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; - return (size / densityRatio); - } - public static int pxFromDp(float size, DisplayMetrics metrics) { - return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - size, metrics)); - } - public static int pxFromSp(float size, DisplayMetrics metrics) { - return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, - size, metrics)); - } - - public DynamicGrid(Context context, Resources resources, - int minWidthPx, int minHeightPx, - int widthPx, int heightPx, - int awPx, int ahPx) { - DisplayMetrics dm = resources.getDisplayMetrics(); - ArrayList<DeviceProfile> deviceProfiles = - new ArrayList<DeviceProfile>(); - boolean hasAA = !LauncherAppState.isDisableAllApps(); - DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm); - // Our phone profiles include the bar sizes in each orientation - deviceProfiles.add(new DeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Short Stubby", - 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Stubby", - 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus 4", - 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus 5", - 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Large Phone", - 406, 694, 5, 5, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); - // The tablet profile is odd in that the landscape orientation - // also includes the nav bar on the side - deviceProfiles.add(new DeviceProfile("Nexus 7", - 575, 904, 5, 6, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); - // Larger tablet profiles always have system bars on the top & bottom - deviceProfiles.add(new DeviceProfile("Nexus 10", - 727, 1207, 5, 6, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); - deviceProfiles.add(new DeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 100, 20, 7, 72, R.xml.default_workspace_4x4)); - mMinWidth = dpiFromPx(minWidthPx, dm); - mMinHeight = dpiFromPx(minHeightPx, dm); - mProfile = new DeviceProfile(context, deviceProfiles, - mMinWidth, mMinHeight, - widthPx, heightPx, - awPx, ahPx, - resources); - } - - public DeviceProfile getDeviceProfile() { - return mProfile; - } - - public String toString() { - return "-------- DYNAMIC GRID ------- \n" + - "Wd: " + mProfile.minWidthDps + ", Hd: " + mProfile.minHeightDps + - ", W: " + mProfile.widthPx + ", H: " + mProfile.heightPx + - " [r: " + mProfile.numRows + ", c: " + mProfile.numColumns + - ", is: " + mProfile.iconSizePx + ", its: " + mProfile.iconTextSizePx + - ", cw: " + mProfile.cellWidthPx + ", ch: " + mProfile.cellHeightPx + - ", hc: " + mProfile.numHotseatIcons + ", his: " + mProfile.hotseatIconSizePx + "]"; - } -} diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index ff02bbbc3..28e923e67 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -32,7 +32,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.SparseArray; -class FastBitmapDrawable extends Drawable { +public class FastBitmapDrawable extends Drawable { static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @@ -72,7 +72,7 @@ class FastBitmapDrawable extends Drawable { private boolean mPressed = false; private ObjectAnimator mPressedAnimator; - FastBitmapDrawable(Bitmap b) { + public FastBitmapDrawable(Bitmap b) { mAlpha = 255; mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); diff --git a/src/com/android/launcher3/FastBitmapView.java b/src/com/android/launcher3/FastBitmapView.java deleted file mode 100644 index 0937eb75e..000000000 --- a/src/com/android/launcher3/FastBitmapView.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2014 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.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.view.View; - -public class FastBitmapView extends View { - - private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); - private Bitmap mBitmap; - - public FastBitmapView(Context context) { - super(context); - } - - /** - * Applies the new bitmap. - * @return true if the view was invalidated. - */ - public boolean setBitmap(Bitmap b) { - if (b != mBitmap){ - if (mBitmap != null) { - invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); - } - mBitmap = b; - if (mBitmap != null) { - invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); - } - return true; - } - return false; - } - - @Override - protected void onDraw(Canvas canvas) { - if (mBitmap != null) { - canvas.drawBitmap(mBitmap, 0, 0, mPaint); - } - } -} diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java index 095c5631d..a51ddd4b8 100644 --- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java +++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java @@ -24,6 +24,8 @@ import android.view.View; import android.view.ViewPropertyAnimator; import android.view.ViewTreeObserver; +import com.android.launcher3.util.Thunk; + /* * This is a helper class that listens to updates from the corresponding animation. * For the first two frames, it adjusts the current play time of the animation to @@ -41,7 +43,7 @@ public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter private boolean mAdjustedSecondFrameTime; private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; - private static long sGlobalFrameCounter; + @Thunk static long sGlobalFrameCounter; private static boolean sVisible; public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index e60704718..57aec3280 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * 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. @@ -16,658 +16,445 @@ package com.android.launcher3; -import android.content.res.Configuration; +import android.util.Log; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; -import android.widget.ScrollView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import com.android.launcher3.util.FocusLogic; +import com.android.launcher3.util.Thunk; /** * A keyboard listener we set on all the workspace icons. */ class IconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleIconKeyEvent(v, keyCode, event); } } /** - * A keyboard listener we set on all the workspace icons. - */ -class FolderKeyEventListener implements View.OnKeyListener { - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleFolderKeyEvent(v, keyCode, event); - } -} - -/** * A keyboard listener we set on all the hotseat buttons. */ class HotseatIconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - final Configuration configuration = v.getResources().getConfiguration(); - return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); + return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event); } } public class FocusHelper { - /** - * Returns the Viewgroup containing page contents for the page at the index specified. - */ - private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { - ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); - if (page instanceof CellLayout) { - // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren - page = ((CellLayout) page).getShortcutsAndWidgets(); - } - return page; - } + private static final String TAG = "FocusHelper"; + private static final boolean DEBUG = false; /** - * Handles key events in a PageViewCellLayout containing PagedViewIcons. + * Handles key events in paged folder. */ - static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { - ViewGroup parentLayout; - ViewGroup itemContainer; - int countX; - int countY; - if (v.getParent() instanceof ShortcutAndWidgetContainer) { - itemContainer = (ViewGroup) v.getParent(); - parentLayout = (ViewGroup) itemContainer.getParent(); - countX = ((CellLayout) parentLayout).getCountX(); - countY = ((CellLayout) parentLayout).getCountY(); - } else { - itemContainer = parentLayout = (ViewGroup) v.getParent(); - countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); - countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + public static class PagedFolderKeyEventListener implements View.OnKeyListener { + + private final Folder mFolder; + + public PagedFolderKeyEventListener(Folder folder) { + mFolder = folder; } - // Note we have an extra parent because of the - // PagedViewCellLayout/PagedViewCellLayoutChildren relationship - final PagedView container = (PagedView) parentLayout.getParent(); - final int iconIndex = itemContainer.indexOfChild(v); - final int itemCount = itemContainer.getChildCount(); - final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout)); - final int pageCount = container.getChildCount(); - - final int x = iconIndex % countX; - final int y = iconIndex / countX; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - ViewGroup newParent = null; - // Side pages do not always load synchronously, so check before focusing child siblings - // willy-nilly - View child = null; - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page - if (iconIndex > 0) { - itemContainer.getChildAt(iconIndex - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } else { - if (pageIndex > 0) { - newParent = getAppsCustomizePage(container, pageIndex - 1); - if (newParent != null) { - container.snapToPage(pageIndex - 1); - child = newParent.getChildAt(newParent.getChildCount() - 1); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - } - } + @Override + public boolean onKey(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP) { + return consume; + } + if (DEBUG) { + Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].", + KeyEvent.keyCodeToString(keyCode))); + } + + + if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { + if (LauncherAppState.isDogfoodBuild()) { + throw new IllegalStateException("Parent of the focused item is not supported."); + } else { + return false; } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page - if (iconIndex < (itemCount - 1)) { - itemContainer.getChildAt(iconIndex + 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } else { - if (pageIndex < (pageCount - 1)) { - newParent = getAppsCustomizePage(container, pageIndex + 1); - if (newParent != null) { - container.snapToPage(pageIndex + 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - } + } + + // Initialize variables. + final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout cellLayout = (CellLayout) itemContainer.getParent(); + final int countX = cellLayout.getCountX(); + final int countY = cellLayout.getCountY(); + + final int iconIndex = itemContainer.indexOfChild(v); + final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent(); + + final int pageIndex = pagedView.indexOfChild(cellLayout); + final int pageCount = pagedView.getPageCount(); + final boolean isLayoutRtl = Utilities.isRtl(v.getResources()); + + int[][] matrix = FocusLogic.createSparseMatrix(cellLayout); + // Process focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl); + if (newIconIndex == FocusLogic.NOOP) { + handleNoopKey(keyCode, v); + return consume; + } + ShortcutAndWidgetContainer newParent = null; + View child = null; + + switch (newIconIndex) { + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt( + ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) + ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous row, otherwise select the tab bar - if (y > 0) { - int newiconIndex = ((y - 1) * countX) + x; - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + break; + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(0, 0); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next row, otherwise do nothing - if (y < (countY - 1)) { - int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); - int newIconY = newiconIndex / countX; - if (newIconY != y) { - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } + break; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(countX - 1, countY - 1); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page, or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - newParent = getAppsCustomizePage(container, pageIndex - 1); - if (newParent != null) { - container.snapToPage(pageIndex - 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } else { - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + break; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0, 0); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page, or the last icon on this page - // if there is no next page - if (pageIndex < (pageCount - 1)) { - newParent = getAppsCustomizePage(container, pageIndex + 1); - if (newParent != null) { - container.snapToPage(pageIndex + 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } else { - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); + break; + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; - break; - default: break; + break; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + child = cellLayout.getChildAt(0, 0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + child = pagedView.getLastItem(); + break; + default: // Go to some item on the current page. + child = itemContainer.getChildAt(newIconIndex); + break; + } + if (child != null) { + child.requestFocus(); + playSoundEffect(keyCode, v); + } else { + handleNoopKey(keyCode, v); + } + return consume; } - return wasHandled; - } - /** - * Handles key events in the workspace hotseat (bottom of the screen). - */ - static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - - // NOTE: currently we don't special case for the phone UI in different - // orientations, even though the hotseat is on the side in landscape mode. This - // is to ensure that accessibility consistency is maintained across rotations. - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - int myIndex = views.indexOf(v); - // Select the previous button, otherwise do nothing - if (myIndex > 0) { - views.get(myIndex - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - int myIndex = views.indexOf(v); - // Select the next button, otherwise do nothing - if (myIndex < views.size() - 1) { - views.get(myIndex + 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - final Workspace workspace = (Workspace) - v.getRootView().findViewById(R.id.workspace); - if (workspace != null) { - int pageIndex = workspace.getCurrentPage(); - CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex); - ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets(); - final View newIcon = getIconInDirection(layout, children, -1, 1); - // Select the first bubble text view in the current page of the workspace - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } else { - workspace.requestFocus(); - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - // Do nothing - wasHandled = true; - break; - default: break; + public void handleNoopKey(int keyCode, View v) { + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mFolder.mFolderName.requestFocus(); + playSoundEffect(keyCode, v); + } } - return wasHandled; } /** - * Private helper method to get the CellLayoutChildren given a CellLayout index. + * Handles key events in the workspace hot seat (bottom of the screen). + * <p>Currently we don't special case for the phone UI in different orientations, even though + * the hotseat is on the side in landscape mode. This is to ensure that accessibility + * consistency is maintained across rotations. */ - private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( - ViewGroup container, int i) { - CellLayout parent = (CellLayout) container.getChildAt(i); - return parent.getShortcutsAndWidgets(); - } + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } - /** - * Private helper method to sort all the CellLayout children in order of their (x,y) spatially - * from top left to bottom right. - */ - private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout, - ViewGroup parent) { - // First we order each the CellLayout children by their x,y coordinates - final int cellCountX = layout.getCountX(); - final int count = parent.getChildCount(); - ArrayList<View> views = new ArrayList<View>(); - for (int j = 0; j < count; ++j) { - views.add(parent.getChildAt(j)); + DeviceProfile profile = ((Launcher) v.getContext()).getDeviceProfile(); + + if (DEBUG) { + Log.v(TAG, String.format( + "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); } - Collections.sort(views, new Comparator<View>() { - @Override - public int compare(View lhs, View rhs) { - CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); - CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); - int lvIndex = (llp.cellY * cellCountX) + llp.cellX; - int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; - return lvIndex - rvIndex; - } - }); - return views; - } - /** - * Private helper method to find the index of the next BubbleTextView or FolderIcon in the - * direction delta. - * - * @param delta either -1 or 1 depending on the direction we want to search - */ - private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) { - // Then we find the next BubbleTextView offset by delta from i - final int count = views.size(); - int newI = i + delta; - while (0 <= newI && newI < count) { - View newV = views.get(newI); - if (newV instanceof BubbleTextView || newV instanceof FolderIcon) { - return newV; - } - newI += delta; + + // Initialize the variables. + final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); + Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); + + Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + int pageIndex = workspace.getNextPage(); + int pageCount = workspace.getChildCount(); + int countX = -1; + int countY = -1; + int iconIndex = hotseatParent.indexOfChild(v); + int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() + .getChildAt(iconIndex).getLayoutParams()).cellX; + + final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); + if (iconLayout == null) { + // This check is to guard against cases where key strokes rushes in when workspace + // child creation/deletion is still in flux. (e.g., during drop or fling + // animation.) + return consume; } - return null; - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, i, delta); - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, views.indexOf(v), delta); - } - /** - * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction - * delta on the next line. - * - * @param delta either -1 or 1 depending on the line and direction we want to search - */ - private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, - int lineDelta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - final int cellCountY = layout.getCountY(); - final int row = lp.cellY; - final int newRow = row + lineDelta; - if (0 <= newRow && newRow < cellCountY) { - float closestDistance = Float.MAX_VALUE; - int closestIndex = -1; - int index = views.indexOf(v); - int endIndex = (lineDelta < 0) ? -1 : views.size(); - while (index != endIndex) { - View newV = views.get(index); - CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); - boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); - if (satisfiesRow && - (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { - float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + - Math.pow(tmpLp.cellY - lp.cellY, 2)); - if (tmpDistance < closestDistance) { - closestIndex = index; - closestDistance = tmpDistance; - } - } - if (index <= endIndex) { - ++index; - } else { - --index; - } + final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + ViewGroup parent = null; + int[][] matrix = null; + + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && + !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, + iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX(); + countY = iconLayout.getCountY() + hotseatLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, + iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX() + hotseatLayout.getCountX(); + countY = iconLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + keyCode = KeyEvent.KEYCODE_PAGE_DOWN; + }else { + // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the + // matrix extended with hotseat. + matrix = FocusLogic.createSparseMatrix(hotseatLayout); + countX = hotseatLayout.getCountX(); + countY = hotseatLayout.getCountY(); + parent = hotseatParent; + } + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + + View newIcon = null; + if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex + 1); + } + if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { + newIconIndex -= iconParent.getChildCount(); + } + if (parent != null) { + if (newIcon == null && newIconIndex >=0) { + newIcon = parent.getChildAt(newIconIndex); } - if (closestIndex > -1) { - return views.get(closestIndex); + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); } } - return null; + return consume; } /** - * Handles key events in a Workspace containing. + * Handles key events in a workspace containing icons. */ static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } + + Launcher launcher = (Launcher) v.getContext(); + DeviceProfile profile = launcher.getDeviceProfile(); + + if (DEBUG) { + Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); + } + + // Initialize the variables. ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final Workspace workspace = (Workspace) layout.getParent(); - final ViewGroup launcher = (ViewGroup) workspace.getParent(); - final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); - final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat); - int pageIndex = workspace.indexOfChild(layout); - int pageCount = workspace.getChildCount(); + CellLayout iconLayout = (CellLayout) parent.getParent(); + final Workspace workspace = (Workspace) iconLayout.getParent(); + final ViewGroup dragLayer = (ViewGroup) workspace.getParent(); + final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar); + final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat); - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page if possible - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } else { - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } + final int iconIndex = parent.indexOfChild(v); + final int pageIndex = workspace.indexOfChild(iconLayout); + final int pageCount = workspace.getChildCount(); + int countX = iconLayout.getCountX(); + int countY = iconLayout.getCountY(); + + CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); + ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + int[][] matrix; + + // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed + // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended + // with the hotseat. + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */, + profile.inv.hotseatAllAppsRank, + !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); + countY = countY + 1; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */, + profile.inv.hotseatAllAppsRank, + !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); + countX = countX + 1; + } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { + workspace.removeWorkspaceItem(v); + return consume; + } else { + matrix = FocusLogic.createSparseMatrix(iconLayout); + } + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + View newIcon = null; + switch (newIconIndex) { + case FocusLogic.NOOP: + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + newIcon = tabs; } - wasHandled = true; break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page if possible - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } else { - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + int newPageIndex = pageIndex - 1; + if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) { + newPageIndex = pageIndex + 1; } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line, otherwise select the tab bar - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - wasHandled = true; - } else { - tabs.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + workspace.snapToPage(newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, + iconLayout.getCountX(), row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, + matrix, FocusLogic.PIVOT, newPageIndex, pageCount, + Utilities.isRtl(v.getResources())); + newIcon = parent.getChildAt(newIconIndex); } break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line, otherwise select the button bar - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - wasHandled = true; - } else if (hotseat != null) { - hotseat.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex - 1); break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } else { - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } - wasHandled = true; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(parent.getChildCount() - 1); + workspace.snapToPage(pageIndex - 1); break; - case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page or the last icon on this page - // if there is no previous page - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } else { - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } - wasHandled = true; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex + 1); break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newPageIndex = pageIndex + 1; + if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) { + newPageIndex = pageIndex - 1; + } + workspace.snapToPage(newPageIndex); + row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, + matrix, FocusLogic.PIVOT, newPageIndex, pageCount, + Utilities.isRtl(v.getResources())); + newIcon = parent.getChildAt(newIconIndex); } - wasHandled = true; break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + newIcon = parent.getChildAt(0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + newIcon = parent.getChildAt(parent.getChildCount() - 1); + break; + default: + // current page, some item. + if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { + newIcon = parent.getChildAt(newIconIndex); + } else if (parent.getChildCount() <= newIconIndex && + newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { + newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); } - wasHandled = true; break; - default: break; } - return wasHandled; + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); + } + return consume; } + // + // Helper methods. + // + /** - * Handles key events for items in a Folder. + * Private helper method to get the CellLayoutChildren given a CellLayout index. */ - static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final ScrollView scrollView = (ScrollView) layout.getParent(); - final Folder folder = (Folder) scrollView.getParent(); - View title = folder.mFolderName; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; + @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { + CellLayout parent = (CellLayout) container.getChildAt(i); + return parent.getShortcutsAndWidgets(); + } + + /** + * Helper method to be used for playing sound effects. + */ + @Thunk static void playSoundEffect(int keyCode, View v) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); break; case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); break; case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; + case KeyEvent.KEYCODE_PAGE_DOWN: + case KeyEvent.KEYCODE_MOVE_END: + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - wasHandled = true; + default: break; - default: break; } - return wasHandled; } } diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java index 7d4664abb..ecf93e4b3 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -23,7 +23,8 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Pair; import android.view.View; -import android.view.ViewParent; + +import com.android.launcher3.util.Thunk; public class FocusIndicatorView extends View implements View.OnFocusChangeListener { @@ -32,9 +33,6 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen private static final float MIN_VISIBLE_ALPHA = 0.2f; private static final long ANIM_DURATION = 150; - private static final int[] sTempPos = new int[2]; - private static final int[] sTempShift = new int[2]; - private final int[] mIndicatorPos = new int[2]; private final int[] mTargetViewPos = new int[2]; @@ -80,7 +78,8 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } if (!mInitiated) { - getLocationRelativeToParentPagedView(this, mIndicatorPos); + // The parent view should always the a parent of the target view. + computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos); mInitiated = true; } @@ -93,7 +92,7 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth; nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight; - getLocationRelativeToParentPagedView(v, mTargetViewPos); + computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos); nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2; nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2; @@ -150,31 +149,36 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } /** - * Gets the location of a view relative in the window, off-setting any shift due to - * page view scroll + * Computes the location of a view relative to {@param parent}, off-setting + * any shift due to page view scroll. + * @param pos an array of two integers in which to hold the coordinates */ - private static void getLocationRelativeToParentPagedView(View v, int[] pos) { - getPagedViewScrollShift(v, sTempShift); - v.getLocationInWindow(sTempPos); - pos[0] = sTempPos[0] + sTempShift[0]; - pos[1] = sTempPos[1] + sTempShift[1]; + private static void computeLocationRelativeToParent(View v, View parent, int[] pos) { + pos[0] = pos[1] = 0; + computeLocationRelativeToParentHelper(v, parent, pos); + + // If a view is scaled, its position will also shift accordingly. For optimization, only + // consider this for the last node. + pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2; + pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2; } - private static void getPagedViewScrollShift(View child, int[] shift) { - ViewParent parent = child.getParent(); + private static void computeLocationRelativeToParentHelper(View child, + View commonParent, int[] shift) { + View parent = (View) child.getParent(); + shift[0] += child.getLeft(); + shift[1] += child.getTop(); if (parent instanceof PagedView) { - View parentView = (View) parent; - child.getLocationInWindow(sTempPos); - shift[0] = parentView.getPaddingLeft() - sTempPos[0]; - shift[1] = -(int) child.getTranslationY(); - } else if (parent instanceof View) { - getPagedViewScrollShift((View) parent, shift); - } else { - shift[0] = shift[1] = 0; + PagedView page = (PagedView) parent; + shift[0] -= page.getScrollForPage(page.indexOfChild(child)); + } + + if (parent != commonParent) { + computeLocationRelativeToParentHelper(parent, commonParent, shift); } } - private static final class ViewAnimState { + @Thunk static final class ViewAnimState { float x, y, scaleX, scaleY; } } diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java deleted file mode 100644 index 08fc311bc..000000000 --- a/src/com/android/launcher3/FocusOnlyTabWidget.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2011 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.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TabWidget; - -public class FocusOnlyTabWidget extends TabWidget { - public FocusOnlyTabWidget(Context context) { - super(context); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public View getSelectedTab() { - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.isSelected()) { - return v; - } - } - return null; - } - - public int getChildTabIndex(View v) { - final int tabCount = getTabCount(); - for (int i = 0; i < tabCount; ++i) { - if (getChildTabViewAt(i) == v) { - return i; - } - } - return -1; - } - - public void setCurrentTabToFocusedTab() { - View tab = null; - int index = -1; - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.hasFocus()) { - tab = v; - index = i; - break; - } - } - if (index > -1) { - super.setCurrentTab(index); - super.onFocusChange(tab, true); - } - } - public void superOnFocusChange(View v, boolean hasFocus) { - super.onFocusChange(v, hasFocus); - } - - @Override - public void onFocusChange(android.view.View v, boolean hasFocus) { - if (v == this && hasFocus && getTabCount() > 0) { - getSelectedTab().requestFocus(); - return; - } - } -} diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 1890af47d..2e19f6eba 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -21,12 +21,15 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.os.SystemClock; -import android.support.v4.widget.AutoScrollHelper; +import android.os.Build; +import android.os.Bundle; import android.text.InputType; import android.text.Selection; import android.text.Spannable; @@ -34,21 +37,28 @@ import android.util.AttributeSet; import android.util.Log; import android.view.ActionMode; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +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; -import android.widget.ScrollView; import android.widget.TextView; +import com.android.launcher3.CellLayout.CellInfo; +import com.android.launcher3.DragController.DragListener; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.UninstallDropTarget.UninstallSource; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.UiThreadCircularReveal; import java.util.ArrayList; import java.util.Collections; @@ -59,106 +69,109 @@ import java.util.Comparator; */ public class Folder extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, - View.OnFocusChangeListener { + View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource, + Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Folder"; - protected DragController mDragController; - protected Launcher mLauncher; - protected FolderInfo mInfo; + /** + * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this + * results in CellLayout being measured as UNSPECIFIED, which it does not support. + */ + private static final int MIN_CONTENT_DIMEN = 5; static final int STATE_NONE = -1; static final int STATE_SMALL = 0; static final int STATE_ANIMATING = 1; static final int STATE_OPEN = 2; - private static final int CLOSE_FOLDER_DELAY_MS = 150; - - private int mExpandDuration; - private int mMaterialExpandDuration; - private int mMaterialExpandStagger; - protected CellLayout mContent; - private ScrollView mScrollView; - private final LayoutInflater mInflater; - private final IconCache mIconCache; - private int mState = STATE_NONE; - private static final int REORDER_ANIMATION_DURATION = 230; + /** + * Time for which the scroll hint is shown before automatically changing page. + */ + public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + + /** + * Fraction of icon width which behave as scroll region. + */ + private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; + + 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; + private static final Rect sTempRect = new Rect(); + + private static String sDefaultFolderName; + private static String sHintText; + + private final Alarm mReorderAlarm = new Alarm(); + private final Alarm mOnExitAlarm = new Alarm(); + private final Alarm mOnScrollHintAlarm = new Alarm(); + @Thunk final Alarm mScrollPauseAlarm = new Alarm(); + + @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + + private final int mExpandDuration; + private final int mMaterialExpandDuration; + private final int mMaterialExpandStagger; + + private final InputMethodManager mInputMethodManager; + + protected final Launcher mLauncher; + protected DragController mDragController; + protected FolderInfo mInfo; + + @Thunk FolderIcon mFolderIcon; + + @Thunk FolderPagedView mContent; + @Thunk View mContentWrapper; + FolderEditText mFolderName; + + private View mFooter; + private int mFooterHeight; + + // Cell ranks used for drag and drop + @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; + + @Thunk int mState = STATE_NONE; private boolean mRearrangeOnClose = false; - private FolderIcon mFolderIcon; - private int mMaxCountX; - private int mMaxCountY; - private int mMaxNumItems; - private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); boolean mItemsInvalidated = false; private ShortcutInfo mCurrentDragInfo; private View mCurrentDragView; private boolean mIsExternalDrag; boolean mSuppressOnAdd = false; - private int[] mTargetCell = new int[2]; - private int[] mPreviousTargetCell = new int[2]; - private int[] mEmptyCell = new int[2]; - private Alarm mReorderAlarm = new Alarm(); - private Alarm mOnExitAlarm = new Alarm(); - private int mFolderNameHeight; - private Rect mTempRect = new Rect(); private boolean mDragInProgress = false; private boolean mDeleteFolderOnDropCompleted = false; private boolean mSuppressFolderDeletion = false; private boolean mItemAddedBackToSelfViaIcon = false; - FolderEditText mFolderName; - private float mFolderIconPivotX; - private float mFolderIconPivotY; - + @Thunk float mFolderIconPivotX; + @Thunk float mFolderIconPivotY; private boolean mIsEditingName = false; - private InputMethodManager mInputMethodManager; - - private static String sDefaultFolderName; - private static String sHintText; - - private FocusIndicatorView mFocusIndicatorHandler; - - // We avoid measuring the scroll view with a 0 width or height, as this - // results in CellLayout being measured as UNSPECIFIED, which it does - // not support. - private static final int MIN_CONTENT_DIMEN = 5; private boolean mDestroyed; - private AutoScrollHelper mAutoScrollHelper; - - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; + // Folder scrolling + private int mScrollAreaOffset; + + @Thunk int mScrollHintDir = DragController.SCROLL_NONE; + @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE; + /** * Used to inflate the Workspace from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. */ public Folder(Context context, AttributeSet attrs) { super(context, attrs); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); setAlwaysDrawnWithCacheEnabled(false); - mInflater = LayoutInflater.from(context); - mIconCache = app.getIconCache(); - - Resources res = getResources(); - mMaxCountX = (int) grid.numColumns; - // Allow scrolling folders when DISABLE_ALL_APPS is true. - if (LauncherAppState.isDisableAllApps()) { - mMaxCountY = mMaxNumItems = Integer.MAX_VALUE; - } else { - mMaxCountY = (int) grid.numRows; - mMaxNumItems = mMaxCountX * mMaxCountY; - } - mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + Resources res = getResources(); mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger); @@ -172,45 +185,35 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mLauncher = (Launcher) context; // We need this view to be focusable in touch mode so that when text editing of the folder // name is complete, we have something to focus on, thus hiding the cursor and giving - // reliable behvior when clicking the text field (since it will always gain focus on click). + // reliable behavior when clicking the text field (since it will always gain focus on click). setFocusableInTouchMode(true); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mScrollView = (ScrollView) findViewById(R.id.scroll_view); - mContent = (CellLayout) findViewById(R.id.folder_content); - - mFocusIndicatorHandler = new FocusIndicatorView(getContext()); - mContent.addView(mFocusIndicatorHandler, 0); - mFocusIndicatorHandler.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - mFocusIndicatorHandler.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + mContentWrapper = findViewById(R.id.folder_content_wrapper); + mContent = (FolderPagedView) findViewById(R.id.folder_content); + mContent.setFolder(this); - mContent.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); - mContent.setGridSize(0, 0); - mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); - mContent.setInvertIfRtl(true); mFolderName = (FolderEditText) findViewById(R.id.folder_name); mFolderName.setFolder(this); mFolderName.setOnFocusChangeListener(this); - // We find out how tall the text view wants to be (it is set to wrap_content), so that - // we can allocate the appropriate amount of space for it. - int measureSpec = MeasureSpec.UNSPECIFIED; - mFolderName.measure(measureSpec, measureSpec); - mFolderNameHeight = mFolderName.getMeasuredHeight(); - // We disable action mode for now since it messes up the view on phones mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); mFolderName.setOnEditorActionListener(this); mFolderName.setSelectAllOnFocus(true); mFolderName.setInputType(mFolderName.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); - mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView); + + mFooter = findViewById(R.id.folder_footer); + + // We find out how tall footer wants to be (it is set to wrap_content), so that + // we can allocate the appropriate amount of space for it. + int measureSpec = MeasureSpec.UNSPECIFIED; + mFooter.measure(measureSpec, measureSpec); + mFooterHeight = mFooter.getMeasuredHeight(); } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -240,7 +243,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public boolean onLongClick(View v) { // Return if global dragging is not enabled if (!mLauncher.isDraggingEnabled()) return true; + return beginDrag(v, false); + } + private boolean beginDrag(View v, boolean accessible) { Object tag = v.getTag(); if (tag instanceof ShortcutInfo) { ShortcutInfo item = (ShortcutInfo) tag; @@ -248,14 +254,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return false; } - mLauncher.getWorkspace().beginDragShared(v, this); + mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible); mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = item.rank; mCurrentDragView = v; - mContent.removeView(mCurrentDragView); + mContent.removeItem(mCurrentDragView); mInfo.remove(mCurrentDragInfo); mDragInProgress = true; mItemAddedBackToSelfViaIcon = false; @@ -263,6 +268,23 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } + @Override + public void startDrag(CellInfo cellInfo, boolean accessible) { + beginDrag(cellInfo.cell, accessible); + } + + @Override + public void enableAccessibleDrag(boolean enable) { + mLauncher.getSearchBar().enableAccessibleDrag(enable); + for (int i = 0; i < mContent.getChildCount(); i++) { + mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG); + } + + mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : + IMPORTANT_FOR_ACCESSIBILITY_AUTO); + mLauncher.getWorkspace().setAddNewPageOnDrag(!enable); + } + public boolean isEditingName() { return mIsEditingName; } @@ -309,13 +331,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderName; } - public CellLayout getContent() { - return mContent; - } - /** * We need to handle touch events to prevent them from falling through to the workspace below. */ + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent ev) { return true; @@ -325,7 +344,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mDragController = dragController; } - void setFolderIcon(FolderIcon icon) { + public void setFolderIcon(FolderIcon icon) { mFolderIcon = icon; } @@ -338,64 +357,16 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList /** * @return the FolderInfo object associated with this folder */ - FolderInfo getInfo() { + public FolderInfo getInfo() { return mInfo; } - private class GridComparator implements Comparator<ShortcutInfo> { - int mNumCols; - public GridComparator(int numCols) { - mNumCols = numCols; - } - - @Override - public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { - int lhIndex = lhs.cellY * mNumCols + lhs.cellX; - int rhIndex = rhs.cellY * mNumCols + rhs.cellX; - return (lhIndex - rhIndex); - } - } - - private void placeInReadingOrder(ArrayList<ShortcutInfo> items) { - int maxX = 0; - int count = items.size(); - for (int i = 0; i < count; i++) { - ShortcutInfo item = items.get(i); - if (item.cellX > maxX) { - maxX = item.cellX; - } - } - - GridComparator gridComparator = new GridComparator(maxX + 1); - Collections.sort(items, gridComparator); - final int countX = mContent.getCountX(); - for (int i = 0; i < count; i++) { - int x = i % countX; - int y = i / countX; - ShortcutInfo item = items.get(i); - item.cellX = x; - item.cellY = y; - } - } - void bind(FolderInfo info) { mInfo = info; ArrayList<ShortcutInfo> children = info.contents; - ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); - setupContentForNumItems(children.size()); - placeInReadingOrder(children); - int count = 0; - for (int i = 0; i < children.size(); i++) { - ShortcutInfo child = (ShortcutInfo) children.get(i); - if (createAndAddShortcut(child) == null) { - overflow.add(child); - } else { - count++; - } - } + Collections.sort(children, ITEM_POS_COMPARATOR); - // We rearrange the items in case there are any empty gaps - setupContentForNumItems(count); + ArrayList<ShortcutInfo> overflow = mContent.bindItems(children); // If our folder has too many items we prune them from the list. This is an issue // when upgrading from the old Folders implementation which could contain an unlimited @@ -405,6 +376,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList LauncherModel.deleteItemFromDatabase(mLauncher, item); } + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + if (lp == null) { + lp = new DragLayer.LayoutParams(0, 0); + lp.customPosition = true; + setLayoutParams(lp); + } + centerAboutIcon(); + mItemsInvalidated = true; updateTextViewFocus(); mInfo.addListener(this); @@ -414,7 +393,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } else { mFolderName.setText(""); } - updateItemLocationsInDatabase(); // In case any children didn't come across during loading, clean up the folder accordingly mFolderIcon.post(new Runnable() { @@ -433,8 +411,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList * * @return A new UserFolder. */ - static Folder fromXml(Context context) { - return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); + @SuppressLint("InflateParams") + static Folder fromXml(Launcher launcher) { + return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, null); } /** @@ -459,6 +438,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateOpen() { if (!(getParent() instanceof DragLayer)) return; + mContent.completePendingPageChanges(); + if (!mDragInProgress) { + // Open on the first page. + mContent.snapToPageImmediately(0); + } + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.isLmpOrAbove()) { @@ -484,6 +469,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList prepareReveal(); centerAboutIcon(); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getFolderHeight(); @@ -494,32 +480,32 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0); PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0); + Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty); + drift.setDuration(mMaterialExpandDuration); + drift.setStartDelay(mMaterialExpandStagger); + drift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); - float radius = (float) Math.sqrt(rx * rx + ry * ry); - AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); - Animator reveal = LauncherAnimUtils.createCircularReveal(this, (int) getPivotX(), + float radius = (float) Math.hypot(rx, ry); + + Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(), (int) getPivotY(), 0, radius); reveal.setDuration(mMaterialExpandDuration); reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - mContent.setAlpha(0f); - Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContent, "alpha", 0f, 1f); + mContentWrapper.setAlpha(0f); + Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f); iconsAlpha.setDuration(mMaterialExpandDuration); iconsAlpha.setStartDelay(mMaterialExpandStagger); iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - mFolderName.setAlpha(0f); - Animator textAlpha = LauncherAnimUtils.ofFloat(mFolderName, "alpha", 0f, 1f); + mFooter.setAlpha(0f); + Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f); textAlpha.setDuration(mMaterialExpandDuration); textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty); - drift.setDuration(mMaterialExpandDuration); - drift.setStartDelay(mMaterialExpandStagger); - drift.setInterpolator(new LogDecelerateInterpolator(60, 0)); - anim.play(drift); anim.play(iconsAlpha); anim.play(textAlpha); @@ -527,11 +513,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList openFolderAnim = anim; - mContent.setLayerType(LAYER_TYPE_HARDWARE, null); + mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null); + mFooter.setLayerType(LAYER_TYPE_HARDWARE, null); onCompleteRunnable = new Runnable() { @Override public void run() { - mContent.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); } }; } @@ -539,8 +527,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, - String.format(getContext().getString(R.string.folder_opened), - mContent.getCountX(), mContent.getCountY())); + mContent.getAccessibilityDescription()); mState = STATE_ANIMATING; } @Override @@ -551,30 +538,79 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList onCompleteRunnable.run(); } - setFocusOnFirstChild(); + mContent.setFocusOnFirstChild(); } }); + + // Footer animation + if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) { + int footerWidth = mContent.getDesiredWidth() + - mFooter.getPaddingLeft() - mFooter.getPaddingRight(); + + float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString()); + 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 + // actually drop the icon. + final boolean updateAnimationFlag = !mDragInProgress; + openFolderAnim.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + 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) { + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); + } + } + }); + } else { + mFolderName.setTranslationX(0); + mContent.setMarkerScale(1); + } + openFolderAnim.start(); // Make sure the folder picks up the last drag move even if the finger doesn't move. if (mDragController.isDragging()) { mDragController.forceTouchMove(); } + + FolderPagedView pages = (FolderPagedView) mContent; + pages.verifyVisibleHighResIcons(pages.getNextPage()); } public void beginExternalDrag(ShortcutInfo item) { - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = mContent.allocateRankForNewItem(item); mIsExternalDrag = true; - mDragInProgress = true; + + // Since this folder opened by another controller, it might not get onDrop or + // onDropComplete. Perform cleanup once drag-n-drop ends. + mDragController.addDragListener(this); + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + if (mIsExternalDrag && mDragInProgress) { + completeDragExit(); + } + mDragController.removeDragListener(this); } - private void sendCustomAccessibilityEvent(int type, String text) { + @Thunk void sendCustomAccessibilityEvent(int type, String text) { AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { @@ -585,13 +621,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - private void setFocusOnFirstChild() { - View firstChild = mContent.getChildAt(0, 0); - if (firstChild != null) { - firstChild.requestFocus(); - } - } - public void animateClosed() { if (!(getParent() instanceof DragLayer)) return; PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); @@ -627,174 +656,90 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList !isFull()); } - protected boolean findAndSetEmptyCells(ShortcutInfo item) { - int[] emptyCell = new int[2]; - if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { - item.cellX = emptyCell[0]; - item.cellY = emptyCell[1]; - return true; - } else { - return false; - } - } - - protected View createAndAddShortcut(ShortcutInfo item) { - final BubbleTextView textView = - (BubbleTextView) mInflater.inflate(R.layout.folder_application, this, false); - textView.applyFromShortcutInfo(item, mIconCache, false); - - textView.setOnClickListener(this); - textView.setOnLongClickListener(this); - textView.setOnFocusChangeListener(mFocusIndicatorHandler); - - // We need to check here to verify that the given item's location isn't already occupied - // by another item. - if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 - || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { - // This shouldn't happen, log it. - Log.e(TAG, "Folder order not properly persisted during bind"); - if (!findAndSetEmptyCells(item)) { - return null; - } - } - - CellLayout.LayoutParams lp = - new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); - boolean insert = false; - textView.setOnKeyListener(new FolderKeyEventListener()); - mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); - return textView; - } - public void onDragEnter(DragObject d) { - mPreviousTargetCell[0] = -1; - mPreviousTargetCell[1] = -1; + mPrevTargetRank = -1; mOnExitAlarm.cancelAlarm(); + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; } OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { public void onAlarm(Alarm alarm) { - realTimeReorder(mEmptyCell, mTargetCell); + mContent.realTimeReorder(mEmptyCellRank, mTargetRank); + mEmptyCellRank = mTargetRank; } }; - boolean readingOrderGreaterThan(int[] v1, int[] v2) { - if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { - return true; - } else { - return false; - } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } - private void realTimeReorder(int[] empty, int[] target) { - boolean wrap; - int startX; - int endX; - int startY; - int delay = 0; - float delayAmount = 30; - if (readingOrderGreaterThan(target, empty)) { - wrap = empty[0] >= mContent.getCountX() - 1; - startY = wrap ? empty[1] + 1 : empty[1]; - for (int y = startY; y <= target[1]; y++) { - startX = y == empty[1] ? empty[0] + 1 : 0; - endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; - for (int x = startX; x <= endX; x++) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } else { - wrap = empty[0] == 0; - startY = wrap ? empty[1] - 1 : empty[1]; - for (int y = startY; y >= target[1]; y--) { - startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; - endX = y > target[1] ? 0 : target[0]; - for (int x = startX; x >= endX; x--) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } + @Override + public void onDragOver(DragObject d) { + onDragOver(d, REORDER_DELAY); } - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + private int getTargetRank(DragObject d, float[] recycle) { + recycle = d.getVisualCenter(recycle); + return mContent.findNearestArea( + (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); } - public void onDragOver(DragObject d) { - final DragView dragView = d.dragView; - final int scrollOffset = mScrollView.getScrollY(); - final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, dragView, null); - r[0] -= getPaddingLeft(); - r[1] -= getPaddingTop(); - - final long downTime = SystemClock.uptimeMillis(); - final MotionEvent translatedEv = MotionEvent.obtain( - downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0); - - if (!mAutoScrollHelper.isEnabled()) { - mAutoScrollHelper.setEnabled(true); + @Thunk void onDragOver(DragObject d, int reorderDelay) { + if (mScrollPauseAlarm.alarmPending()) { + return; } + final float[] r = new float[2]; + mTargetRank = getTargetRank(d, r); - final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv); - translatedEv.recycle(); - - if (handled) { + if (mTargetRank != mPrevTargetRank) { mReorderAlarm.cancelAlarm(); + mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); + mReorderAlarm.setAlarm(REORDER_DELAY); + mPrevTargetRank = mTargetRank; + } + + float x = r[0]; + int currentPage = mContent.getNextPage(); + + float cellOverlap = mContent.getCurrentCellLayout().getCellWidth() + * ICON_OVERSCROLL_WIDTH_FACTOR; + boolean isOutsideLeftEdge = x < cellOverlap; + boolean isOutsideRightEdge = x > (getWidth() - cellOverlap); + + if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) { + showScrollHint(DragController.SCROLL_LEFT, d); + } else if (currentPage < (mContent.getPageCount() - 1) + && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) { + showScrollHint(DragController.SCROLL_RIGHT, d); } else { - mTargetCell = mContent.findNearestArea( - (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell); - if (isLayoutRtl()) { - mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; - } - if (mTargetCell[0] != mPreviousTargetCell[0] - || mTargetCell[1] != mPreviousTargetCell[1]) { - mReorderAlarm.cancelAlarm(); - mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); - mReorderAlarm.setAlarm(REORDER_DELAY); - mPreviousTargetCell[0] = mTargetCell[0]; - mPreviousTargetCell[1] = mTargetCell[1]; + mOnScrollHintAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; } } } - // This is used to compute the visual center of the dragView. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; + private void showScrollHint(int direction, DragObject d) { + // Show scroll hint on the right + if (mScrollHintDir != direction) { + mContent.showScrollHint(direction); + mScrollHintDir = direction; } - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) { + mCurrentScrollDir = direction; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; - - return res; + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } } OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { @@ -804,17 +749,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; public void completeDragExit() { - mLauncher.closeFolder(); + if (mInfo.opened) { + mLauncher.closeFolder(); + mRearrangeOnClose = true; + } else if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + rearrangeChildren(); + clearDragInfo(); + } + } + + private void clearDragInfo() { mCurrentDragInfo = null; mCurrentDragView = null; mSuppressOnAdd = false; - mRearrangeOnClose = true; mIsExternalDrag = false; } public void onDragExit(DragObject d) { - // Exiting folder; stop the auto scroller. - mAutoScrollHelper.setEnabled(false); // We only close the folder if this is a true drag exit, ie. not because // a drop has occurred above the folder. if (!d.dragComplete) { @@ -822,6 +775,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } + + /** + * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we + * need to complete all transient states based on timers. + */ + @Override + public void prepareAccessibilityDrop() { + if (mReorderAlarm.alarmPending()) { + mReorderAlarm.cancelAlarm(); + mReorderAlarmListener.onAlarm(mReorderAlarm); + } } public void onDropCompleted(final View target, final DragObject d, @@ -846,9 +818,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList replaceFolderWithFinalItem(); } } else { - setupContentForNumItems(getItemCount()); // The drag failed, we need to return the item to the folder + ShortcutInfo info = (ShortcutInfo) d.dragInfo; + View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) + ? mCurrentDragView : mContent.createNewView(info); + ArrayList<View> views = getItemsInReadingOrder(); + views.add(info.rank, icon); + mContent.arrangeChildren(views, views.size()); + mItemsInvalidated = true; + + mSuppressOnAdd = true; mFolderIcon.onDrop(d); + mSuppressOnAdd = false; } if (target != this) { @@ -857,6 +838,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (!successfulDrop) { mSuppressFolderDeletion = true; } + mScrollPauseAlarm.cancelAlarm(); completeDragExit(); } } @@ -871,12 +853,22 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Reordering may have occured, and we need to save the new item locations. We do this once // at the end to prevent unnecessary database operations. updateItemLocationsInDatabaseBatch(); + + // Use the item count to check for multi-page as the folder UI may not have + // been refreshed yet. + if (getItemCount() <= mContent.itemsPerPage()) { + // Show the animation, next time something is added to the folder. + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher); + } + } + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -905,7 +897,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + @Override + public void onFlingToDelete(DragObject d, PointF vec) { // Do nothing } @@ -914,22 +907,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Do nothing } - private void updateItemLocationsInDatabase() { - ArrayList<View> list = getItemsInReadingOrder(); - for (int i = 0; i < list.size(); i++) { - View v = list.get(i); - ItemInfo info = (ItemInfo) v.getTag(); - LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); - } - } - private void updateItemLocationsInDatabaseBatch() { ArrayList<View> list = getItemsInReadingOrder(); ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); for (int i = 0; i < list.size(); i++) { View v = list.get(i); ItemInfo info = (ItemInfo) v.getTag(); + info.rank = i; items.add(info); } @@ -942,7 +926,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, false); + info.cellX, info.cellY); } } @@ -956,37 +940,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } - private void setupContentDimensions(int count) { - ArrayList<View> list = getItemsInReadingOrder(); - - int countX = mContent.getCountX(); - int countY = mContent.getCountY(); - boolean done = false; - - while (!done) { - int oldCountX = countX; - int oldCountY = countY; - if (countX * countY < count) { - // Current grid is too small, expand it - if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { - countX++; - } else if (countY < mMaxCountY) { - countY++; - } - if (countY == 0) countY++; - } else if ((countY - 1) * countX >= count && countY >= countX) { - countY = Math.max(0, countY - 1); - } else if ((countX - 1) * countY >= count) { - countX = Math.max(0, countX - 1); - } - done = countX == oldCountX && countY == oldCountY; - } - mContent.setGridSize(countX, countY); - arrangeChildren(list); - } - public boolean isFull() { - return getItemCount() >= mMaxNumItems; + return mContent.isFull(); } private void centerAboutIcon() { @@ -996,41 +951,30 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getFolderHeight(); - float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); - int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); - int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2); + int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2); int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; - int currentPage = mLauncher.getWorkspace().getNextPage(); - // In case the workspace is scrolling, we need to use the final scroll to compute - // the folders bounds. - mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); - // We first fetch the currently visible CellLayoutChildren - CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); - ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); - Rect bounds = new Rect(); - parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); - // We reset the workspaces scroll - mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); - - // We need to bound the folder to the currently visible CellLayoutChildren - int left = Math.min(Math.max(bounds.left, centeredLeft), - bounds.left + bounds.width() - width); - int top = Math.min(Math.max(bounds.top, centeredTop), - bounds.top + bounds.height() - height); - if (grid.isPhone() && (grid.availableWidthPx - width) < grid.iconSizePx) { + + // We need to bound the folder to the currently visible workspace area + mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect); + int left = Math.min(Math.max(sTempRect.left, centeredLeft), + sTempRect.left + sTempRect.width() - width); + int top = Math.min(Math.max(sTempRect.top, centeredTop), + sTempRect.top + sTempRect.height() - height); + if (grid.isPhone && (grid.availableWidthPx - width) < grid.iconSizePx) { // Center the folder if it is full (on phones only) left = (grid.availableWidthPx - width) / 2; - } else if (width >= bounds.width()) { + } else if (width >= sTempRect.width()) { // If the folder doesn't fit within the bounds, center it about the desired bounds - left = bounds.left + (bounds.width() - width) / 2; + left = sTempRect.left + (sTempRect.width() - width) / 2; } - if (height >= bounds.height()) { - top = bounds.top + (bounds.height() - height) / 2; + if (height >= sTempRect.height()) { + top = sTempRect.top + (sTempRect.height() - height) / 2; } int folderPivotX = width / 2 + (centeredLeft - left); @@ -1055,26 +999,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderIconPivotY; } - private void setupContentForNumItems(int count) { - setupContentDimensions(count); - - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - if (lp == null) { - lp = new DragLayer.LayoutParams(0, 0); - lp.customPosition = true; - setLayoutParams(lp); - } - centerAboutIcon(); - } - private int getContentAreaHeight() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect workspacePadding = grid.getWorkspacePadding(grid.isLandscape ? - CellLayout.LANDSCAPE : CellLayout.PORTRAIT); + DeviceProfile grid = mLauncher.getDeviceProfile(); + Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl); int maxContentAreaHeight = grid.availableHeightPx - workspacePadding.top - workspacePadding.bottom - - mFolderNameHeight; + mFooterHeight; int height = Math.min(maxContentAreaHeight, mContent.getDesiredHeight()); return Math.max(height, MIN_CONTENT_DIMEN); @@ -1085,67 +1015,67 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } private int getFolderHeight() { - int height = getPaddingTop() + getPaddingBottom() - + getContentAreaHeight() + mFolderNameHeight; - return height; + return getFolderHeight(getContentAreaHeight()); + } + + private int getFolderHeight(int contentAreaHeight) { + return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); - int height = getFolderHeight(); - int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(getContentAreaWidth(), - MeasureSpec.EXACTLY); - int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(getContentAreaHeight(), - MeasureSpec.EXACTLY); - - if (LauncherAppState.isDisableAllApps()) { - // Don't cap the height of the content to allow scrolling. - mContent.setFixedSize(getContentAreaWidth(), mContent.getDesiredHeight()); - } else { - mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight()); + int contentWidth = getContentAreaWidth(); + int contentHeight = getContentAreaHeight(); + + int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); + int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); + + 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)); - mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); - mFolderName.measure(contentAreaWidthSpec, - MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); - setMeasuredDimension(width, height); + int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; + int folderHeight = getFolderHeight(contentHeight); + setMeasuredDimension(folderWidth, folderHeight); } - private void arrangeChildren(ArrayList<View> list) { - int[] vacant = new int[2]; - if (list == null) { - list = getItemsInReadingOrder(); - } - mContent.removeAllViews(); + /** + * Rearranges the children based on their rank. + */ + public void rearrangeChildren() { + rearrangeChildren(-1); + } - for (int i = 0; i < list.size(); i++) { - View v = list.get(i); - mContent.getVacantCell(vacant, 1, 1); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - lp.cellX = vacant[0]; - lp.cellY = vacant[1]; - ItemInfo info = (ItemInfo) v.getTag(); - if (info.cellX != vacant[0] || info.cellY != vacant[1]) { - info.cellX = vacant[0]; - info.cellY = vacant[1]; - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); - } - boolean insert = false; - mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); - } + /** + * Rearranges the children based on their rank. + * @param itemCount if greater than the total children count, empty spaces are left at the end, + * otherwise it is ignored. + */ + public void rearrangeChildren(int itemCount) { + ArrayList<View> views = getItemsInReadingOrder(); + mContent.arrangeChildren(views, Math.max(itemCount, views.size())); mItemsInvalidated = true; } - public int getItemCount() { - return mContent.getShortcutsAndWidgets().getChildCount(); + // TODO remove this once GSA code fix is submitted + public ViewGroup getContent() { + return (ViewGroup) mContent; } - public View getItemAt(int index) { - return mContent.getShortcutsAndWidgets().getChildAt(index); + public int getItemCount() { + return mContent.getItemCount(); } - private void onCloseComplete() { + @Thunk void onCloseComplete() { DragLayer parent = (DragLayer) getParent(); if (parent != null) { parent.removeView(this); @@ -1155,7 +1085,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderIcon.requestFocus(); if (mRearrangeOnClose) { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); mRearrangeOnClose = false; } if (getItemCount() <= 1) { @@ -1166,9 +1096,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } mSuppressFolderDeletion = false; + clearDragInfo(); } - private void replaceFolderWithFinalItem() { + @Thunk void replaceFolderWithFinalItem() { // Add the last remaining child to the workspace in place of the folder Runnable onCompleteRunnable = new Runnable() { @Override @@ -1179,8 +1110,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Move the item from the folder to the workspace, in the position of the folder if (getItemCount() == 1) { ShortcutInfo finalItem = mInfo.contents.get(0); - child = mLauncher.createShortcut(R.layout.application, cellLayout, - finalItem); + child = mLauncher.createShortcut(cellLayout, finalItem); LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY); } @@ -1205,7 +1135,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } }; - View finalChild = getItemAt(0); + View finalChild = mContent.getLastItem(); if (finalChild != null) { mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); } else { @@ -1220,9 +1150,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // This method keeps track of the last item in the folder for the purposes // of keyboard focus - private void updateTextViewFocus() { - View lastChild = getItemAt(getItemCount() - 1); - getItemAt(getItemCount() - 1); + public void updateTextViewFocus() { + View lastChild = mContent.getLastItem(); if (lastChild != null) { mFolderName.setNextFocusDownId(lastChild.getId()); mFolderName.setNextFocusRightId(lastChild.getId()); @@ -1247,12 +1176,24 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; } + // If the icon was dropped while the page was being scrolled, we need to compute + // the target location again such that the icon is placed of the final page. + if (!mContent.rankOnCurrentPage(mEmptyCellRank)) { + // Reorder again. + mTargetRank = getTargetRank(d, null); + + // Rearrange items immediately. + mReorderAlarmListener.onAlarm(mReorderAlarm); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + } + mContent.completePendingPageChanges(); + View currentDragView; ShortcutInfo si = mCurrentDragInfo; if (mIsExternalDrag) { - si.cellX = mEmptyCell[0]; - si.cellY = mEmptyCell[1]; - + currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); // Actually move the item in the database if it was an external drag. Call this // before creating the view, so that ShortcutInfo is updated appropriately. LauncherModel.addOrMoveItemInDatabase( @@ -1263,14 +1204,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList updateItemLocationsInDatabaseBatch(); } mIsExternalDrag = false; - - currentDragView = createAndAddShortcut(si); } else { currentDragView = mCurrentDragView; - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) currentDragView.getLayoutParams(); - si.cellX = lp.cellX = mEmptyCell[0]; - si.cellX = lp.cellY = mEmptyCell[1]; - mContent.addViewToCellLayout(currentDragView, -1, (int) si.id, lp, true); + mContent.addViewForRank(currentDragView, si, mEmptyCellRank); } if (d.dragView.hasDrawn()) { @@ -1289,7 +1225,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList currentDragView.setVisibility(VISIBLE); } mItemsInvalidated = true; - setupContentDimensions(getItemCount()); + rearrangeChildren(); // Temporarily suppress the listener, as we did all the work already here. mSuppressOnAdd = true; @@ -1297,6 +1233,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mSuppressOnAdd = false; // Clear the drag info, as it is no longer being dragged. mCurrentDragInfo = null; + mDragInProgress = false; + + if (mContent.getPageCount() > 1) { + // The animation has already been shown while opening the folder. + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); + } } // This is used so the item doesn't immediately appear in the folder when added. In one case @@ -1311,17 +1253,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList v.setVisibility(VISIBLE); } + @Override public void onAdd(ShortcutInfo item) { - mItemsInvalidated = true; // If the item was dropped onto this open folder, we have done the work associated // with adding the item to the folder, as indicated by mSuppressOnAdd being set if (mSuppressOnAdd) return; - if (!findAndSetEmptyCells(item)) { - // The current layout is full, can we expand it? - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - } - createAndAddShortcut(item); + mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item)); + mItemsInvalidated = true; LauncherModel.addOrMoveItemInDatabase( mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); } @@ -1332,27 +1270,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // the work associated with removing the item, so we don't have to do anything here. if (item == mCurrentDragInfo) return; View v = getViewForInfo(item); - mContent.removeView(v); + mContent.removeItem(v); if (mState == STATE_ANIMATING) { mRearrangeOnClose = true; } else { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); } if (getItemCount() <= 1) { replaceFolderWithFinalItem(); } } - private View getViewForInfo(ShortcutInfo item) { - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v.getTag() == item) { - return v; - } + private View getViewForInfo(final ShortcutInfo item) { + return mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + return info == item; } - } - return null; + }); } public void onItemsChanged() { @@ -1365,14 +1301,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public ArrayList<View> getItemsInReadingOrder() { if (mItemsInvalidated) { mItemsInReadingOrder.clear(); - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v != null) { - mItemsInReadingOrder.add(v); - } + mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + mItemsInReadingOrder.add(view); + return false; } - } + }); mItemsInvalidated = false; } return mItemsInReadingOrder; @@ -1391,5 +1327,79 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void getHitRectRelativeToDragLayer(Rect outRect) { getHitRect(outRect); + outRect.left -= mScrollAreaOffset; + 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; + + OnScrollHintListener(DragObject object) { + mDragObject = object; + } + + /** + * Scroll hint has been shown long enough. Now scroll to appropriate page. + */ + @Override + public void onAlarm(Alarm alarm) { + if (mCurrentScrollDir == DragController.SCROLL_LEFT) { + mContent.scrollLeft(); + mScrollHintDir = DragController.SCROLL_NONE; + } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { + mContent.scrollRight(); + mScrollHintDir = DragController.SCROLL_NONE; + } else { + // This should not happen + return; + } + mCurrentScrollDir = DragController.SCROLL_NONE; + + // Pause drag event until the scrolling is finished + mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); + mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY); + } + } + + private class OnScrollFinishedListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollFinishedListener(DragObject object) { + mDragObject = object; + } + + /** + * Page scroll is complete. + */ + @Override + public void onAlarm(Alarm alarm) { + // Reorder immediately on page change. + onDragOver(mDragObject, 1); + } + } + + // Compares item position based on rank and position giving priority to the rank. + private static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() { + + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + if (lhs.rank != rhs.rank) { + return lhs.rank - rhs.rank; + } else if (lhs.cellY != rhs.cellY) { + return lhs.cellY - rhs.cellY; + } else { + return lhs.cellX - rhs.cellX; + } + } + }; } diff --git a/src/com/android/launcher3/FolderAutoScrollHelper.java b/src/com/android/launcher3/FolderAutoScrollHelper.java deleted file mode 100644 index 40e888464..000000000 --- a/src/com/android/launcher3/FolderAutoScrollHelper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 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.support.v4.widget.AutoScrollHelper; -import android.widget.ScrollView; - -/** - * An implementation of {@link AutoScrollHelper} that knows how to scroll - * through a {@link Folder}. - */ -public class FolderAutoScrollHelper extends AutoScrollHelper { - private static final float MAX_SCROLL_VELOCITY = 1500f; - - private final ScrollView mTarget; - - public FolderAutoScrollHelper(ScrollView target) { - super(target); - - mTarget = target; - - setActivationDelay(0); - setEdgeType(EDGE_TYPE_INSIDE_EXTEND); - setExclusive(true); - setMaximumVelocity(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY); - setRampDownDuration(0); - setRampUpDuration(0); - } - - @Override - public void scrollTargetBy(int deltaX, int deltaY) { - mTarget.scrollBy(deltaX, deltaY); - } - - @Override - public boolean canTargetScrollHorizontally(int direction) { - // List do not scroll horizontally. - return false; - } - - @Override - public boolean canTargetScrollVertically(int direction) { - return mTarget.canScrollVertically(direction); - } -}
\ No newline at end of file diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index a359f1180..8d534d2fe 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -43,6 +43,7 @@ import android.widget.TextView; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -50,15 +51,16 @@ import java.util.ArrayList; * An icon that can appear on in the workspace representing an {@link UserFolder}. */ public class FolderIcon extends FrameLayout implements FolderListener { - private Launcher mLauncher; - private Folder mFolder; + @Thunk Launcher mLauncher; + @Thunk Folder mFolder; private FolderInfo mInfo; - private static boolean sStaticValuesDirty = true; + @Thunk static boolean sStaticValuesDirty = true; private CheckLongPressHelper mLongPressHelper; + private StylusEventHelper mStylusEventHelper; // The number of icons to display in the - private static final int NUM_ITEMS_IN_PREVIEW = 3; + public static final int NUM_ITEMS_IN_PREVIEW = 3; private static final int CONSUMPTION_ANIMATION_DURATION = 100; private static final int DROP_IN_ANIMATION_DURATION = 400; private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; @@ -88,8 +90,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static Drawable sSharedFolderLeaveBehind = null; - private ImageView mPreviewBackground; - private BubbleTextView mFolderName; + @Thunk ImageView mPreviewBackground; + @Thunk BubbleTextView mFolderName; FolderRingAnimator mFolderRingAnimator = null; @@ -109,11 +111,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { private float mSlop; private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0); - private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); - private ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>(); + @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); + @Thunk ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>(); private Alarm mOpenAlarm = new Alarm(); - private ItemInfo mDragInfo; + @Thunk ItemInfo mDragInfo; public FolderIcon(Context context, AttributeSet attrs) { super(context, attrs); @@ -127,6 +129,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { private void init() { mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } public boolean isDropEnabled() { @@ -145,8 +149,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + "is dependent on this"); } - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + + DeviceProfile grid = launcher.getDeviceProfile(); FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); icon.setClipToPadding(false); @@ -191,7 +195,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static class FolderRingAnimator { public int mCellX; public int mCellY; - private CellLayout mCellLayout; + @Thunk CellLayout mCellLayout; public float mOuterRingSize; public float mInnerRingSize; public FolderIcon mFolderIcon = null; @@ -215,12 +219,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { + Thread.currentThread()); } - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = launcher.getDeviceProfile(); sPreviewSize = grid.folderIconSizePx; sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); - sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); - sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip_holo); + sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer); + sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip); sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); sStaticValuesDirty = false; } @@ -488,8 +491,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { private void computePreviewDrawingParams(int drawableSize, int totalSize) { if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); mIntrinsicIconSize = drawableSize; mTotalWidth = totalSize; @@ -643,9 +645,10 @@ public class FolderIcon extends FrameLayout implements FolderListener { final Runnable onCompleteRunnable) { final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); - final float scale0 = 1.0f; - final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2; - final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2 + getPaddingTop(); + float iconSize = mLauncher.getDeviceProfile().iconSizePx; + final float scale0 = iconSize / d.getIntrinsicWidth() ; + final float transX0 = (mAvailableSpaceInPreview - iconSize) / 2; + final float transY0 = (mAvailableSpaceInPreview - iconSize) / 2 + getPaddingTop(); mAnimParams.drawable = d; ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f); @@ -708,7 +711,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } public void onTitleChanged(CharSequence title) { - mFolderName.setText(title.toString()); + mFolderName.setText(title); setContentDescription(String.format(getContext().getString(R.string.folder_name_format), title)); } @@ -719,6 +722,12 @@ public class FolderIcon extends FrameLayout implements FolderListener { // isPressed() on an ACTION_UP boolean result = super.onTouchEvent(event); + // Check for a stylus button press, if it occurs cancel any long press checks. + if (mStylusEventHelper.checkAndPerformStylusEvent(event)) { + mLongPressHelper.cancelLongPress(); + return true; + } + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLongPressHelper.postCheckForLongPress(); diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index 85a792f4b..aea21c95b 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -29,19 +29,38 @@ import java.util.Arrays; */ public class FolderInfo extends ItemInfo { + public static final int NO_FLAGS = 0x00000000; + + /** + * The folder is locked in sorted mode + */ + public static final int FLAG_ITEMS_SORTED = 0x00000001; + + /** + * It is a work folder + */ + public static final int FLAG_WORK_FOLDER = 0x00000002; + + /** + * The multi-page animation has run for this folder + */ + public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004; + /** * Whether this folder has been opened */ boolean opened; + public int options; + /** * The apps and shortcuts */ - ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); + public ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); ArrayList<FolderListener> listeners = new ArrayList<FolderListener>(); - FolderInfo() { + public FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; user = UserHandleCompat.myUserHandle(); } @@ -83,6 +102,8 @@ public class FolderInfo extends ItemInfo { void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.TITLE, title.toString()); + values.put(LauncherSettings.Favorites.OPTIONS, options); + } void addListener(FolderListener listener) { @@ -121,4 +142,25 @@ public class FolderInfo extends ItemInfo { + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")"; } + + public boolean hasOption(int optionFlag) { + return (options & optionFlag) != 0; + } + + /** + * @param option flag to set or clear + * @param isEnabled whether to set or clear the flag + * @param context if not null, save changes to the db. + */ + public void setOption(int option, boolean isEnabled, Context context) { + int oldOptions = options; + if (isEnabled) { + options |= option; + } else { + options &= ~option; + } + if (context != null && oldOptions != options) { + LauncherModel.updateItemInDatabase(context, this); + } + } } diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java new file mode 100644 index 000000000..f2ec1b68c --- /dev/null +++ b/src/com/android/launcher3/FolderPagedView.java @@ -0,0 +1,678 @@ +/** + * 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.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +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; +import com.android.launcher3.PageIndicator.PageMarkerResources; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class FolderPagedView extends PagedView { + + private static final String TAG = "FolderPagedView"; + + private static final boolean ALLOW_FOLDER_SCROLL = true; + + private static final int REORDER_ANIMATION_DURATION = 230; + 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_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. + */ + private static final float SCROLL_HINT_FRACTION = 0.07f; + + private static final int[] sTempPosArray = new int[2]; + + public final boolean mIsRtl; + + private final LayoutInflater mInflater; + private final IconCache mIconCache; + + @Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>(); + + private final int mMaxCountX; + private final int mMaxCountY; + private final int mMaxItemsPerPage; + + private int mAllocatedContentSize; + private int mGridCountX; + private int mGridCountY; + + private Folder mFolder; + private FocusIndicatorView mFocusIndicatorView; + private PagedFolderKeyEventListener mKeyListener; + + private PageIndicator mPageIndicator; + + public FolderPagedView(Context context, AttributeSet attrs) { + super(context, attrs); + LauncherAppState app = LauncherAppState.getInstance(); + + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + mMaxCountX = profile.numFolderColumns; + mMaxCountY = profile.numFolderRows; + + mMaxItemsPerPage = mMaxCountX * mMaxCountY; + + mInflater = LayoutInflater.from(context); + mIconCache = app.getIconCache(); + + mIsRtl = Utilities.isRtl(getResources()); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + + setEdgeGlowColor(getResources().getColor(R.color.folder_edge_effect_color)); + } + + public void setFolder(Folder folder) { + mFolder = folder; + mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); + mKeyListener = new PagedFolderKeyEventListener(folder); + mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator); + } + + /** + * Sets up the grid size such that {@param count} items can fit in the grid. + * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while + * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. + */ + private void setupContentDimensions(int count) { + mAllocatedContentSize = count; + boolean done; + if (count >= mMaxItemsPerPage) { + mGridCountX = mMaxCountX; + mGridCountY = mMaxCountY; + done = true; + } else { + done = false; + } + + while (!done) { + int oldCountX = mGridCountX; + int oldCountY = mGridCountY; + if (mGridCountX * mGridCountY < count) { + // Current grid is too small, expand it + if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) { + mGridCountX++; + } else if (mGridCountY < mMaxCountY) { + mGridCountY++; + } + if (mGridCountY == 0) mGridCountY++; + } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) { + mGridCountY = Math.max(0, mGridCountY - 1); + } else if ((mGridCountX - 1) * mGridCountY >= count) { + mGridCountX = Math.max(0, mGridCountX - 1); + } + done = mGridCountX == oldCountX && mGridCountY == oldCountY; + } + + // Update grid size + for (int i = getPageCount() - 1; i >= 0; i--) { + getPageAt(i).setGridSize(mGridCountX, mGridCountY); + } + } + + /** + * Binds items to the layout. + * @return list of items that could not be bound, probably because we hit the max size limit. + */ + public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) { + ArrayList<View> icons = new ArrayList<View>(); + ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>(); + + for (ShortcutInfo item : items) { + if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) { + extra.add(item); + } else { + icons.add(createNewView(item)); + } + } + arrangeChildren(icons, icons.size(), false); + return extra; + } + + /** + * Create space for a new item at the end, and returns the rank for that item. + * Also sets the current page to the last page. + */ + public int allocateRankForNewItem(ShortcutInfo info) { + int rank = getItemCount(); + ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder()); + views.add(rank, null); + arrangeChildren(views, views.size(), false); + setCurrentPage(rank / mMaxItemsPerPage); + return rank; + } + + public View createAndAddViewForRank(ShortcutInfo item, int rank) { + View icon = createNewView(item); + addViewForRank(icon, item, rank); + return icon; + } + + /** + * Adds the {@param view} to the layout based on {@param rank} and updated the position + * related attributes. It assumes that {@param item} is already attached to the view. + */ + public void addViewForRank(View view, ShortcutInfo item, int rank) { + int pagePos = rank % mMaxItemsPerPage; + int pageNo = rank / mMaxItemsPerPage; + + item.rank = rank; + item.cellX = pagePos % mGridCountX; + item.cellY = pagePos / mGridCountX; + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + lp.cellX = item.cellX; + lp.cellY = item.cellY; + getPageAt(pageNo).addViewToCellLayout( + view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); + } + + @SuppressLint("InflateParams") + public View createNewView(ShortcutInfo item) { + final BubbleTextView textView = (BubbleTextView) mInflater.inflate( + R.layout.folder_application, null, false); + textView.applyFromShortcutInfo(item, mIconCache); + textView.setOnClickListener(mFolder); + textView.setOnLongClickListener(mFolder); + textView.setOnFocusChangeListener(mFocusIndicatorView); + textView.setOnKeyListener(mKeyListener); + + textView.setLayoutParams(new CellLayout.LayoutParams( + item.cellX, item.cellY, item.spanX, item.spanY)); + return textView; + } + + @Override + public CellLayout getPageAt(int index) { + return (CellLayout) getChildAt(index); + } + + public void removeCellLayoutView(View view) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(view); + } + } + + public CellLayout getCurrentCellLayout() { + return getPageAt(getNextPage()); + } + + private CellLayout createAndAddNewPage() { + DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile(); + CellLayout page = new CellLayout(getContext()); + page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); + page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + page.setInvertIfRtl(true); + page.setGridSize(mGridCountX, mGridCountY); + + addView(page, -1, generateDefaultLayoutParams()); + return page; + } + + @Override + protected int getChildGap() { + return getPaddingLeft() + getPaddingRight(); + } + + public void setFixedSize(int width, int height) { + width -= (getPaddingLeft() + getPaddingRight()); + height -= (getPaddingTop() + getPaddingBottom()); + for (int i = getChildCount() - 1; i >= 0; i --) { + ((CellLayout) getChildAt(i)).setFixedSize(width, height); + } + } + + public void removeItem(View v) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(v); + } + } + + /** + * Updates position and rank of all the children in the view. + * It essentially removes all views from all the pages and then adds them again in appropriate + * page. + * + * @param list the ordered list of children. + * @param itemCount if greater than the total children count, empty spaces are left + * at the end, otherwise it is ignored. + * + */ + public void arrangeChildren(ArrayList<View> list, int itemCount) { + 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++) { + CellLayout page = (CellLayout) getChildAt(i); + page.removeAllViews(); + pages.add(page); + } + setupContentDimensions(itemCount); + + Iterator<CellLayout> pageItr = pages.iterator(); + CellLayout currentPage = null; + + int position = 0; + int newX, newY, rank; + + rank = 0; + for (int i = 0; i < itemCount; i++) { + View v = list.size() > i ? list.get(i) : null; + if (currentPage == null || position >= mMaxItemsPerPage) { + // Next page + if (pageItr.hasNext()) { + currentPage = pageItr.next(); + } else { + currentPage = createAndAddNewPage(); + } + position = 0; + } + + if (v != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + newX = position % mGridCountX; + newY = position / mGridCountX; + ItemInfo info = (ItemInfo) v.getTag(); + if (info.cellX != newX || info.cellY != newY || info.rank != rank) { + info.cellX = newX; + info.cellY = newY; + info.rank = rank; + if (saveChanges) { + LauncherModel.addOrMoveItemInDatabase(getContext(), info, + mFolder.mInfo.id, 0, info.cellX, info.cellY); + } + } + lp.cellX = info.cellX; + lp.cellY = info.cellY; + currentPage.addViewToCellLayout( + v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); + } + + rank ++; + position++; + } + + // Remove extra views. + boolean removed = false; + while (pageItr.hasNext()) { + removeView(pageItr.next()); + removed = true; + } + if (removed) { + setCurrentPage(0); + } + + setEnableOverscroll(getPageCount() > 1); + + // Update footer + mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); + // 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() { + return getPageCount() > 0 ? + (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0; + } + + public int getDesiredHeight() { + return getPageCount() > 0 ? + (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0; + } + + public int getItemCount() { + int lastPageIndex = getChildCount() - 1; + if (lastPageIndex < 0) { + // If there are no pages, nothing has yet been added to the folder. + return 0; + } + return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount() + + lastPageIndex * mMaxItemsPerPage; + } + + /** + * @return the rank of the cell nearest to the provided pixel position. + */ + public int findNearestArea(int pixelX, int pixelY) { + int pageIndex = getNextPage(); + CellLayout page = getPageAt(pageIndex); + page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray); + if (mFolder.isLayoutRtl()) { + sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1; + } + return Math.min(mAllocatedContentSize - 1, + pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]); + } + + @Override + protected PageMarkerResources getPageIndicatorMarker(int pageIndex) { + return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder, + R.drawable.ic_pageindicator_default_folder); + } + + public boolean isFull() { + return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage; + } + + public View getLastItem() { + if (getChildCount() < 1) { + return null; + } + ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets(); + int lastRank = lastContainer.getChildCount() - 1; + if (mGridCountX > 0) { + return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); + } else { + return lastContainer.getChildAt(lastRank); + } + } + + /** + * Iterates over all its items in a reading order. + * @return the view for which the operator returned true. + */ + public View iterateOverItems(ItemOperator op) { + for (int k = 0 ; k < getChildCount(); k++) { + CellLayout page = getPageAt(k); + for (int j = 0; j < page.getCountY(); j++) { + for (int i = 0; i < page.getCountX(); i++) { + View v = page.getChildAt(i, j); + if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) { + return v; + } + } + } + } + return null; + } + + public String getAccessibilityDescription() { + return String.format(getContext().getString(R.string.folder_opened), + mGridCountX, mGridCountY); + } + + /** + * Sets the focus on the first visible child. + */ + public void setFocusOnFirstChild() { + View firstChild = getCurrentCellLayout().getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + if (mFolder != null) { + mFolder.updateTextViewFocus(); + } + } + + /** + * Scrolls the current view by a fraction + */ + public void showScrollHint(int direction) { + float fraction = (direction == DragController.SCROLL_LEFT) ^ mIsRtl + ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION; + int hint = (int) (fraction * getWidth()); + int scroll = getScrollForPage(getNextPage()) + hint; + int delta = scroll - getScrollX(); + if (delta != 0) { + mScroller.setInterpolator(new DecelerateInterpolator()); + mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION); + invalidate(); + } + } + + public void clearScrollHint() { + if (getScrollX() != getScrollForPage(getNextPage())) { + snapToPage(getNextPage()); + } + } + + /** + * Finish animation all the views which are animating across pages + */ + public void completePendingPageChanges() { + if (!mPendingAnimations.isEmpty()) { + HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations); + for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) { + e.getKey().animate().cancel(); + e.getValue().run(); + } + } + } + + public boolean rankOnCurrentPage(int rank) { + int p = rank / mMaxItemsPerPage; + return p == getNextPage(); + } + + @Override + protected void onPageBeginMoving() { + super.onPageBeginMoving(); + getVisiblePages(sTempPosArray); + for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) { + verifyVisibleHighResIcons(i); + } + } + + /** + * Ensures that all the icons on the given page are of high-res + */ + public void verifyVisibleHighResIcons(int pageNo) { + CellLayout page = getPageAt(pageNo); + if (page != null) { + ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); + for (int i = parent.getChildCount() - 1; i >= 0; i--) { + ((BubbleTextView) parent.getChildAt(i)).verifyHighRes(); + } + } + } + + public int getAllocatedContentSize() { + return mAllocatedContentSize; + } + + /** + * Reorders the items such that the {@param empty} spot moves to {@param target} + */ + public void realTimeReorder(int empty, int target) { + completePendingPageChanges(); + int delay = 0; + float delayAmount = START_VIEW_REORDER_DELAY; + + // Animation only happens on the current page. + int pageToAnimate = getNextPage(); + + int pageT = target / mMaxItemsPerPage; + int pagePosT = target % mMaxItemsPerPage; + + if (pageT != pageToAnimate) { + Log.e(TAG, "Cannot animate when the target cell is invisible"); + } + int pagePosE = empty % mMaxItemsPerPage; + int pageE = empty / mMaxItemsPerPage; + + int startPos, endPos; + int moveStart, moveEnd; + int direction; + + if (target == empty) { + // No animation + return; + } else if (target > empty) { + // Items will move backwards to make room for the empty cell. + direction = 1; + + // If empty cell is in a different page, move them instantly. + if (pageE < pageToAnimate) { + moveStart = empty; + // Instantly move the first item in the current page. + moveEnd = pageToAnimate * mMaxItemsPerPage; + // Animate the 2nd item in the current page, as the first item was already moved to + // the last page. + startPos = 0; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } else { + // The items will move forward. + direction = -1; + + if (pageE > pageToAnimate) { + // Move the items immediately. + moveStart = empty; + // Instantly move the last item in the current page. + moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1; + + // Animations start with the second last item in the page + startPos = mMaxItemsPerPage - 1; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } + + // Instant moving views. + while (moveStart != moveEnd) { + int rankToMove = moveStart + direction; + int p = rankToMove / mMaxItemsPerPage; + int pagePos = rankToMove % mMaxItemsPerPage; + int x = pagePos % mGridCountX; + int y = pagePos / mGridCountX; + + final CellLayout page = getPageAt(p); + final View v = page.getChildAt(x, y); + if (v != null) { + if (pageToAnimate != p) { + page.removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart); + } else { + // Do a fake animation before removing it. + final int newRank = moveStart; + final float oldTranslateX = v.getTranslationX(); + + Runnable endAction = new Runnable() { + + @Override + public void run() { + mPendingAnimations.remove(v); + v.setTranslationX(oldTranslateX); + ((CellLayout) v.getParent().getParent()).removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), newRank); + } + }; + v.animate() + .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth()) + .setDuration(REORDER_ANIMATION_DURATION) + .setStartDelay(0) + .withEndAction(endAction); + mPendingAnimations.put(v, endAction); + } + } + moveStart = rankToMove; + } + + if ((endPos - startPos) * direction <= 0) { + // No animation + return; + } + + CellLayout page = getPageAt(pageToAnimate); + for (int i = startPos; i != endPos; i += direction) { + int nextPos = i + direction; + View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX); + if (v != null) { + ((ItemInfo) v.getTag()).rank -= direction; + } + if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, + REORDER_ANIMATION_DURATION, delay, true, true)) { + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + + public void setMarkerScale(float scale) { + int count = mPageIndicator.getChildCount(); + for (int i = 0; i < count; i++) { + View marker = mPageIndicator.getChildAt(i); + marker.animate().cancel(); + marker.setScaleX(scale); + marker.setScaleY(scale); + } + } + + public void animateMarkers() { + int count = mPageIndicator.getChildCount(); + 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(PAGE_INDICATOR_ANIMATION_DURATION) + .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i + + PAGE_INDICATOR_ANIMATION_START_DELAY); + } + } + + public int itemsPerPage() { + return mMaxItemsPerPage; + } + + @Override + protected void getEdgeVerticalPostion(int[] pos) { + pos[0] = 0; + pos[1] = getViewportHeight(); + } +} diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java index b1e0e68a4..5ff85d664 100644 --- a/src/com/android/launcher3/HolographicOutlineHelper.java +++ b/src/com/android/launcher3/HolographicOutlineHelper.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; @@ -25,11 +26,16 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.util.SparseArray; +/** + * Utility class to generate shadow and outline effect, which are used for click feedback + * and drag-n-drop respectively. + */ public class HolographicOutlineHelper { - private static final Rect sTempRect = new Rect(); + private static HolographicOutlineHelper sInstance; private final Canvas mCanvas = new Canvas(); private final Paint mDrawPaint = new Paint(); @@ -40,26 +46,23 @@ public class HolographicOutlineHelper { private final BlurMaskFilter mThinOuterBlurMaskFilter; private final BlurMaskFilter mMediumInnerBlurMaskFilter; - private final BlurMaskFilter mShaowBlurMaskFilter; - private final int mShadowOffset; - - /** - * Padding used when creating shadow bitmap; - */ - final int shadowBitmapPadding; + private final BlurMaskFilter mShadowBlurMaskFilter; - static HolographicOutlineHelper INSTANCE; + // We have 4 different icon sizes: homescreen, hotseat, folder & all-apps + private final SparseArray<Bitmap> mBitmapCache = new SparseArray<>(4); private HolographicOutlineHelper(Context context) { - final float scale = LauncherAppState.getInstance().getScreenDensity(); + Resources res = context.getResources(); + + float mediumBlur = res.getDimension(R.dimen.blur_size_medium_outline); + mMediumOuterBlurMaskFilter = new BlurMaskFilter(mediumBlur, BlurMaskFilter.Blur.OUTER); + mMediumInnerBlurMaskFilter = new BlurMaskFilter(mediumBlur, BlurMaskFilter.Blur.NORMAL); - mMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); - mThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); - mMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL); + mThinOuterBlurMaskFilter = new BlurMaskFilter( + res.getDimension(R.dimen.blur_size_thin_outline), BlurMaskFilter.Blur.OUTER); - mShaowBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); - mShadowOffset = (int) (scale * 2.0f); - shadowBitmapPadding = (int) (scale * 4.0f); + mShadowBlurMaskFilter = new BlurMaskFilter( + res.getDimension(R.dimen.blur_size_click_shadow), BlurMaskFilter.Blur.NORMAL); mDrawPaint.setFilterBitmap(true); mDrawPaint.setAntiAlias(true); @@ -71,10 +74,10 @@ public class HolographicOutlineHelper { } public static HolographicOutlineHelper obtain(Context context) { - if (INSTANCE == null) { - INSTANCE = new HolographicOutlineHelper(context); + if (sInstance == null) { + sInstance = new HolographicOutlineHelper(context); } - return INSTANCE; + return sInstance; } /** @@ -153,51 +156,34 @@ public class HolographicOutlineHelper { } Bitmap createMediumDropShadow(BubbleTextView view) { - final Bitmap result = Bitmap.createBitmap( - view.getWidth() + shadowBitmapPadding + shadowBitmapPadding, - view.getHeight() + shadowBitmapPadding + shadowBitmapPadding + mShadowOffset, - Bitmap.Config.ARGB_8888); - - mCanvas.setBitmap(result); - - final Rect clipRect = sTempRect; - view.getDrawingRect(sTempRect); - // adjust the clip rect so that we don't include the text label - clipRect.bottom = view.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V - + view.getLayout().getLineTop(0); - - // Draw the View into the bitmap. - // The translate of scrollX and scrollY is necessary when drawing TextViews, because - // they set scrollX and scrollY to large values to achieve centered text - mCanvas.save(); - mCanvas.scale(view.getScaleX(), view.getScaleY(), - view.getWidth() / 2 + shadowBitmapPadding, - view.getHeight() / 2 + shadowBitmapPadding); - mCanvas.translate(-view.getScrollX() + shadowBitmapPadding, - -view.getScrollY() + shadowBitmapPadding); - mCanvas.clipRect(clipRect, Op.REPLACE); - view.draw(mCanvas); - mCanvas.restore(); - - int[] blurOffst = new int[2]; - mBlurPaint.setMaskFilter(mShaowBlurMaskFilter); - Bitmap blurBitmap = result.extractAlpha(mBlurPaint, blurOffst); - - mCanvas.save(); - mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - mCanvas.translate(blurOffst[0], blurOffst[1]); - - mDrawPaint.setColor(Color.BLACK); - mDrawPaint.setAlpha(30); - mCanvas.drawBitmap(blurBitmap, 0, 0, mDrawPaint); + Drawable icon = view.getIcon(); + if (icon == null) { + return null; + } + Rect rect = icon.getBounds(); + + int bitmapWidth = (int) (rect.width() * view.getScaleX()); + int bitmapHeight = (int) (rect.height() * view.getScaleY()); + + int key = (bitmapWidth << 16) | bitmapHeight; + Bitmap cache = mBitmapCache.get(key); + if (cache == null) { + cache = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + mCanvas.setBitmap(cache); + mBitmapCache.put(key, cache); + } else { + mCanvas.setBitmap(cache); + mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } - mDrawPaint.setAlpha(60); - mCanvas.drawBitmap(blurBitmap, 0, mShadowOffset, mDrawPaint); + mCanvas.save(Canvas.MATRIX_SAVE_FLAG); + mCanvas.scale(view.getScaleX(), view.getScaleY()); + mCanvas.translate(-rect.left, -rect.top); + icon.draw(mCanvas); mCanvas.restore(); - mCanvas.setBitmap(null); - blurBitmap.recycle(); - return result; + mBlurPaint.setMaskFilter(mShadowBlurMaskFilter); + return cache.extractAlpha(mBlurPaint, null); } } diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index b08272f36..17fdeb1dc 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -16,24 +16,19 @@ package com.android.launcher3; -import android.content.ComponentName; 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.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; -import java.util.ArrayList; - -public class Hotseat extends FrameLayout { - private static final String TAG = "Hotseat"; +public class Hotseat extends FrameLayout + implements Stats.LaunchSourceProvider{ private CellLayout mContent; @@ -41,8 +36,7 @@ public class Hotseat extends FrameLayout { private int mAllAppsButtonRank; - private boolean mTransposeLayoutWithOrientation; - private boolean mIsLandscape; + private final boolean mHasVerticalHotseat; public Hotseat(Context context) { this(context, null); @@ -54,16 +48,8 @@ public class Hotseat extends FrameLayout { public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - - Resources r = context.getResources(); - mTransposeLayoutWithOrientation = - r.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); - mIsLandscape = context.getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - } - - public void setup(Launcher launcher) { - mLauncher = launcher; + mLauncher = (Launcher) context; + mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout(); } CellLayout getLayout() { @@ -71,6 +57,13 @@ public class Hotseat extends FrameLayout { } /** + * Returns whether there are other icons than the all apps button in the hotseat. + */ + public boolean hasIcons() { + return mContent.getShortcutsAndWidgets().getChildCount() > 1; + } + + /** * Registers the specified listener on the cell layout of the hotseat. */ @Override @@ -78,60 +71,35 @@ public class Hotseat extends FrameLayout { mContent.setOnLongClickListener(l); } - private boolean hasVerticalHotseat() { - return (mIsLandscape && mTransposeLayoutWithOrientation); - } - /* Get the orientation invariant order of the item in the hotseat for persistence. */ int getOrderInHotseat(int x, int y) { - return hasVerticalHotseat() ? (mContent.getCountY() - y - 1) : x; + return mHasVerticalHotseat ? (mContent.getCountY() - y - 1) : x; } + /* Get the orientation specific coordinates given an invariant order in the hotseat. */ int getCellXFromOrder(int rank) { - return hasVerticalHotseat() ? 0 : rank; + return mHasVerticalHotseat ? 0 : rank; } + int getCellYFromOrder(int rank) { - return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0; - } - public boolean isAllAppsButtonRank(int rank) { - if (LauncherAppState.isDisableAllApps()) { - return false; - } else { - return rank == mAllAppsButtonRank; - } + return mHasVerticalHotseat ? (mContent.getCountY() - (rank + 1)) : 0; } - /** This returns the coordinates of an app in a given cell, relative to the DragLayer */ - Rect getCellCoordinates(int cellX, int cellY) { - Rect coords = new Rect(); - mContent.cellToRect(cellX, cellY, 1, 1, coords); - int[] hotseatInParent = new int[2]; - Utilities.getDescendantCoordRelativeToParent(this, mLauncher.getDragLayer(), - hotseatInParent, false); - coords.offset(hotseatInParent[0], hotseatInParent[1]); - - // Center the icon - int cWidth = mContent.getShortcutsAndWidgets().getCellContentWidth(); - int cHeight = mContent.getShortcutsAndWidgets().getCellContentHeight(); - int cellPaddingX = (int) Math.max(0, ((coords.width() - cWidth) / 2f)); - int cellPaddingY = (int) Math.max(0, ((coords.height() - cHeight) / 2f)); - coords.offset(cellPaddingX, cellPaddingY); - - return coords; + public boolean isAllAppsButtonRank(int rank) { + return rank == mAllAppsButtonRank; } @Override protected void onFinishInflate() { super.onFinishInflate(); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); - mAllAppsButtonRank = grid.hotseatAllAppsRank; + mAllAppsButtonRank = grid.inv.hotseatAllAppsRank; mContent = (CellLayout) findViewById(R.id.layout); - if (grid.isLandscape && !grid.isLargeTablet()) { - mContent.setGridSize(1, (int) grid.numHotseatIcons); + if (grid.isLandscape && !grid.isLargeTablet) { + mContent.setGridSize(1, (int) grid.inv.numHotseatIcons); } else { - mContent.setGridSize((int) grid.numHotseatIcons, 1); + mContent.setGridSize((int) grid.inv.numHotseatIcons, 1); } mContent.setIsHotseat(true); @@ -141,35 +109,34 @@ public class Hotseat extends FrameLayout { void resetLayout() { mContent.removeAllViewsInLayout(); - if (!LauncherAppState.isDisableAllApps()) { - // Add the Apps button - Context context = getContext(); - - LayoutInflater inflater = LayoutInflater.from(context); - TextView allAppsButton = (TextView) - inflater.inflate(R.layout.all_apps_button, mContent, false); - Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); - - Utilities.resizeIconDrawable(d); - allAppsButton.setCompoundDrawables(null, d, null, null); - - allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); - allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener()); - if (mLauncher != null) { - allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener()); - mLauncher.setAllAppsButton(allAppsButton); - allAppsButton.setOnClickListener(mLauncher); - allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler); - } - - // Note: We do this to ensure that the hotseat is always laid out in the orientation of - // the hotseat in order regardless of which orientation they were added - int x = getCellXFromOrder(mAllAppsButtonRank); - int y = getCellYFromOrder(mAllAppsButtonRank); - CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); - lp.canReorder = false; - mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); + // Add the Apps button + Context context = getContext(); + + LayoutInflater inflater = LayoutInflater.from(context); + TextView allAppsButton = (TextView) + inflater.inflate(R.layout.all_apps_button, mContent, false); + Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); + + mLauncher.resizeIconDrawable(d); + allAppsButton.setCompoundDrawables(null, d, null, null); + + allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); + allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener()); + if (mLauncher != null) { + mLauncher.setAllAppsButton(allAppsButton); + allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener()); + allAppsButton.setOnClickListener(mLauncher); + allAppsButton.setOnLongClickListener(mLauncher); + allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler); } + + // Note: We do this to ensure that the hotseat is always laid out in the orientation of + // the hotseat in order regardless of which orientation they were added + int x = getCellXFromOrder(mAllAppsButtonRank); + int y = getCellYFromOrder(mAllAppsButtonRank); + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); + lp.canReorder = false; + mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); } @Override @@ -182,54 +149,8 @@ public class Hotseat extends FrameLayout { return false; } - void addAllAppsFolder(IconCache iconCache, - ArrayList<AppInfo> allApps, ArrayList<ComponentName> onWorkspace, - Launcher launcher, Workspace workspace) { - if (LauncherAppState.isDisableAllApps()) { - FolderInfo fi = new FolderInfo(); - - fi.cellX = getCellXFromOrder(mAllAppsButtonRank); - fi.cellY = getCellYFromOrder(mAllAppsButtonRank); - fi.spanX = 1; - fi.spanY = 1; - fi.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT; - fi.screenId = mAllAppsButtonRank; - fi.itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; - fi.title = "More Apps"; - LauncherModel.addItemToDatabase(launcher, fi, fi.container, fi.screenId, fi.cellX, - fi.cellY, false); - FolderIcon folder = FolderIcon.fromXml(R.layout.folder_icon, launcher, - getLayout(), fi, iconCache); - workspace.addInScreen(folder, fi.container, fi.screenId, fi.cellX, fi.cellY, - fi.spanX, fi.spanY); - - for (AppInfo info: allApps) { - ComponentName cn = info.intent.getComponent(); - if (!onWorkspace.contains(cn)) { - Log.d(TAG, "Adding to 'more apps': " + info.intent); - ShortcutInfo si = info.makeShortcut(); - fi.add(si); - } - } - } - } - - void addAppsToAllAppsFolder(ArrayList<AppInfo> apps) { - if (LauncherAppState.isDisableAllApps()) { - View v = mContent.getChildAt(getCellXFromOrder(mAllAppsButtonRank), getCellYFromOrder(mAllAppsButtonRank)); - FolderIcon fi = null; - - if (v instanceof FolderIcon) { - fi = (FolderIcon) v; - } else { - return; - } - - FolderInfo info = fi.getFolderInfo(); - for (AppInfo a: apps) { - ShortcutInfo si = a.makeShortcut(); - info.add(si); - } - } + @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 5a0875b30..916418f18 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -16,19 +16,28 @@ package com.android.launcher3; -import android.app.ActivityManager; import android.content.ComponentName; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; @@ -36,17 +45,17 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Stack; /** * Cache of application icons. Icons can be made from any thread. @@ -56,66 +65,71 @@ public class IconCache { private static final String TAG = "Launcher.IconCache"; private static final int INITIAL_ICON_CACHE_CAPACITY = 50; - private static final String RESOURCE_FILE_PREFIX = "icon_"; // Empty class name is used for storing package default entry. private static final String EMPTY_CLASS_NAME = "."; private static final boolean DEBUG = false; - private static class CacheEntry { - public Bitmap icon; - public CharSequence title; - public CharSequence contentDescription; - } - - private static class CacheKey { - public ComponentName componentName; - public UserHandleCompat user; + private static final int LOW_RES_SCALE_FACTOR = 5; - CacheKey(ComponentName componentName, UserHandleCompat user) { - this.componentName = componentName; - this.user = user; - } + @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); - @Override - public int hashCode() { - return componentName.hashCode() + user.hashCode(); - } - - @Override - public boolean equals(Object o) { - CacheKey other = (CacheKey) o; - return other.componentName.equals(componentName) && other.user.equals(user); - } + @Thunk static class CacheEntry { + public Bitmap icon; + public CharSequence title = ""; + public CharSequence contentDescription = ""; + public boolean isLowResIcon; } - private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = - new HashMap<UserHandleCompat, Bitmap>(); + private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>(); + @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<CacheKey, CacheEntry> mCache = - new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); - private int mIconDpi; - - public IconCache(Context context) { - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - + private final HashMap<ComponentKey, CacheEntry> mCache = + new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); + private final int mIconDpi; + @Thunk final IconDB mIconDb; + + @Thunk final Handler mWorkerHandler; + + // The background color used for activity icons. Since these icons are displayed in all-apps + // and folders, this would be same as the light quantum panel background. This color + // is used to convert icons to RGB_565. + private final int mActivityBgColor; + // The background color used for package icons. These are displayed in widget tray, which + // has a dark quantum panel background. + private final int mPackageBgColor; + private final BitmapFactory.Options mLowResOptions; + + private String mSystemState; + private Bitmap mLowResBitmap; + private Canvas mLowResCanvas; + private Paint mLowResPaint; + + public IconCache(Context context, InvariantDeviceProfile inv) { mContext = context; mPackageManager = context.getPackageManager(); mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); - mIconDpi = activityManager.getLauncherLargeIconDensity(); - - // need to set mIconDpi before getting default icon - UserHandleCompat myUser = UserHandleCompat.myUserHandle(); - mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); + mIconDpi = inv.fillResIconDpi; + mIconDb = new IconDB(context); + + mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); + + mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color); + mPackageBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color_dark); + mLowResOptions = new BitmapFactory.Options(); + // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will + // automatically be loaded as ALPHA_8888. + mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565; + updateSystemStateString(); } - public Drawable getFullResDefaultActivityIcon() { + private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); } @@ -145,10 +159,6 @@ public class IconCache { return getFullResDefaultActivityIcon(); } - public int getFullResIconDpi() { - return mIconDpi; - } - public Drawable getFullResIcon(ActivityInfo info) { Resources resources; try { @@ -184,59 +194,269 @@ public class IconCache { * Remove any records for the supplied ComponentName. */ public synchronized void remove(ComponentName componentName, UserHandleCompat user) { - mCache.remove(new CacheKey(componentName, user)); + mCache.remove(new ComponentKey(componentName, user)); } /** - * Remove any records for the supplied package name. + * Remove any records for the supplied package name from memory. */ - public synchronized void remove(String packageName, UserHandleCompat user) { - HashSet<CacheKey> forDeletion = new HashSet<CacheKey>(); - for (CacheKey key: mCache.keySet()) { + private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { + HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>(); + for (ComponentKey key: mCache.keySet()) { if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { forDeletion.add(key); } } - for (CacheKey condemned: forDeletion) { + for (ComponentKey condemned: forDeletion) { mCache.remove(condemned); } } /** - * Empty out the cache. + * Updates the entries related to the given package in memory and persistent DB. */ - public synchronized void flush() { - mCache.clear(); + public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { + removeIconsForPkg(packageName, user); + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + long userSerial = mUserManager.getSerialNumberForUser(user); + for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, info, userSerial); + } + } catch (NameNotFoundException e) { + Log.d(TAG, "Package not found", e); + return; + } } /** - * Empty out the cache that aren't of the correct grid size + * Removes the entries related to the given package in memory and persistent DB. */ - public synchronized void flushInvalidIcons(DeviceProfile grid) { - Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator(); - while (it.hasNext()) { - final CacheEntry e = it.next().getValue(); - if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx - || e.icon.getHeight() < grid.iconSizePx)) { - it.remove(); + public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { + removeFromMemCacheLocked(packageName, user); + long userSerial = mUserManager.getSerialNumberForUser(user); + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {packageName + "/%", Long.toString(userSerial)}); + } + + public void updateDbIcons(Set<String> ignorePackagesForMainUser) { + // Remove all active icon update tasks. + mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); + + updateSystemStateString(); + for (UserHandleCompat user : mUserManager.getUserProfiles()) { + // Query for the set of apps + final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); + // Fail if we don't have any apps + // TODO: Fix this. Only fail for the current user. + if (apps == null || apps.isEmpty()) { + return; } + + // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} + // is called by the icon cache when the job is complete. + updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user) + ? ignorePackagesForMainUser : Collections.<String>emptySet()); } } /** - * Fill in "application" with the icon and label for "info." + * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in + * the DB and are updated. + * @return The set of packages for which icons have updated. + */ + private void updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps, + Set<String> ignorePackages) { + long userSerial = mUserManager.getSerialNumberForUser(user); + PackageManager pm = mContext.getPackageManager(); + HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>(); + for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { + pkgInfoMap.put(info.packageName, info); + } + + HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>(); + for (LauncherActivityInfoCompat app : apps) { + componentMap.put(app.getComponentName(), app); + } + + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, + IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, + IconDB.COLUMN_SYSTEM_STATE}, + IconDB.COLUMN_USER + " = ? ", + new String[] {Long.toString(userSerial)}, + null, null, null); + + final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); + final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); + final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); + final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); + final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); + + HashSet<Integer> itemsToRemove = new HashSet<Integer>(); + Stack<LauncherActivityInfoCompat> appsToUpdate = new Stack<>(); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = pkgInfoMap.get(component.getPackageName()); + if (info == null) { + if (!ignorePackages.contains(component.getPackageName())) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } + continue; + } + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { + // Application is not present + continue; + } + + long updateTime = c.getLong(indexLastUpdate); + int version = c.getInt(indexVersion); + LauncherActivityInfoCompat app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime && + TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { + continue; + } + if (app == null) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } else { + appsToUpdate.add(app); + } + } + c.close(); + if (!itemsToRemove.isEmpty()) { + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), + null); + } + + // Insert remaining apps. + if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { + Stack<LauncherActivityInfoCompat> appsToAdd = new Stack<>(); + appsToAdd.addAll(componentMap.values()); + new SerializedIconUpdateTask(userSerial, pkgInfoMap, + appsToAdd, appsToUpdate).scheduleNext(); + } + } + + @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. + ContentValues values = updateCacheAndGetContentValues(app, false); + addIconToDB(values, app.getComponentName(), info, userSerial); + } + + /** + * Updates {@param values} to contain versoning information and adds it to the DB. + * @param values {@link ContentValues} containing icon & title */ - public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache, - info.getUser(), false); + private void addIconToDB(ContentValues values, ComponentName key, + PackageInfo info, long userSerial) { + values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); + values.put(IconDB.COLUMN_VERSION, info.versionCode); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, + boolean replaceExisting) { + final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); + CacheEntry entry = null; + if (!replaceExisting) { + entry = mCache.get(key); + // We can't reuse the entry if the high-res icon is not present. + if (entry == null || entry.isLowResIcon || entry.icon == null) { + entry = null; + } + } + if (entry == null) { + entry = new CacheEntry(); + entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); + } + entry.title = app.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); + mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); + + return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor); + } + + /** + * Fetches high-res icon for the provided ItemInfo and updates the caller when done. + * @return a request ID that can be used to cancel the request. + */ + public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { + Runnable request = new Runnable() { + + @Override + public void run() { + if (info instanceof AppInfo) { + getTitleAndIcon((AppInfo) info, null, false); + } else if (info instanceof ShortcutInfo) { + ShortcutInfo st = (ShortcutInfo) info; + getTitleAndIcon(st, + st.promisedIntent != null ? st.promisedIntent : st.intent, + st.user, false); + } else if (info instanceof PackageItemInfo) { + PackageItemInfo pti = (PackageItemInfo) info; + getTitleAndIconForApp(pti.packageName, pti.user, false, pti); + } + mMainThreadExecutor.execute(new Runnable() { + + @Override + public void run() { + caller.reapplyItemInfo(info); + } + }); + } + }; + mWorkerHandler.post(request); + return new IconLoadRequest(request, mWorkerHandler); + } - application.title = entry.title; - application.iconBitmap = entry.icon; + private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) { + return entry.icon == null ? getDefaultIcon(user) : entry.icon; + } + + /** + * Fill in "application" with the icon and label for "info." + */ + public synchronized void getTitleAndIcon(AppInfo application, + LauncherActivityInfoCompat info, boolean useLowResIcon) { + UserHandleCompat user = info == null ? application.user : info.getUser(); + CacheEntry entry = cacheLocked(application.componentName, info, user, + false, useLowResIcon); + application.title = Utilities.trim(entry.title); + application.iconBitmap = getNonNullIcon(entry, user); application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; } + /** + * Updates {@param application} only if a valid entry is found. + */ + public synchronized void updateTitleAndIcon(AppInfo application) { + CacheEntry entry = cacheLocked(application.componentName, null, application.user, + false, application.usingLowResIcon); + if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { + application.title = Utilities.trim(entry.title); + application.iconBitmap = entry.icon; + application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; + } + } + + /** + * Returns a high res icon for the given intent and user + */ public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then @@ -246,15 +466,16 @@ public class IconCache { } LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true); + CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */); return entry.icon; } /** - * Fill in "shortcutInfo" with the icon and label for "info." + * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the + * corresponding activity is not found, it reverts to the package icon. */ public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, - UserHandleCompat user, boolean usePkgIcon) { + UserHandleCompat user, boolean useLowResIcon) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then // we should still look in the cache for restored app icons. @@ -262,17 +483,38 @@ public class IconCache { shortcutInfo.setIcon(getDefaultIcon(user)); shortcutInfo.title = ""; shortcutInfo.usingFallbackIcon = true; + shortcutInfo.usingLowResIcon = false; } else { - LauncherActivityInfoCompat launcherActInfo = - mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); - - shortcutInfo.setIcon(entry.icon); - shortcutInfo.title = entry.title; - shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); + getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); } } + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, + UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { + CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); + shortcutInfo.setIcon(getNonNullIcon(entry, user)); + shortcutInfo.title = Utilities.trim(entry.title); + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + shortcutInfo.usingLowResIcon = entry.isLowResIcon; + } + + /** + * Fill in {@param appInfo} with the icon and label for {@param packageName} + */ + public synchronized void getTitleAndIconForApp( + String packageName, UserHandleCompat user, boolean useLowResIcon, + PackageItemInfo infoOut) { + CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); + infoOut.iconBitmap = getNonNullIcon(entry, user); + infoOut.title = Utilities.trim(entry.title); + infoOut.usingLowResIcon = entry.isLowResIcon; + infoOut.contentDescription = entry.contentDescription; + } public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { if (!mDefaultIcons.containsKey(user)) { @@ -281,16 +523,6 @@ public class IconCache { return mDefaultIcons.get(user); } - public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache) { - if (info == null || component == null) { - return null; - } - - CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false); - return entry.icon; - } - public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { return mDefaultIcons.get(user) == icon; } @@ -300,44 +532,27 @@ public class IconCache { * This method is not thread safe, it must be called from a synchronized method. */ private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) { - CacheKey cacheKey = new CacheKey(componentName, user); + UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { + ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - mCache.put(cacheKey, entry); - if (info != null) { - ComponentName labelKey = info.getComponentName(); - if (labelCache != null && labelCache.containsKey(labelKey)) { - entry.title = labelCache.get(labelKey).toString(); - } else { - entry.title = info.getLabel().toString(); - if (labelCache != null) { - labelCache.put(labelKey, entry.title); - } - } - - entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); - entry.icon = Utilities.createIconBitmap( - info.getBadgedIcon(mIconDpi), mContext); - } else { - entry.title = ""; - Bitmap preloaded = getPreloadedIcon(componentName, user); - if (preloaded != null) { - if (DEBUG) Log.d(TAG, "using preloaded icon for " + - componentName.toShortString()); - entry.icon = preloaded; + // Check the DB first. + if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { + if (info != null) { + entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { if (usePackageIcon) { - CacheEntry packageEntry = getEntryForPackage( - componentName.getPackageName(), user); + CacheEntry packageEntry = getEntryForPackageLocked( + componentName.getPackageName(), user, false); if (packageEntry != null) { if (DEBUG) Log.d(TAG, "using package default icon for " + componentName.toShortString()); entry.icon = packageEntry.icon; entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { @@ -347,6 +562,11 @@ public class IconCache { } } } + + if (TextUtils.isEmpty(entry.title) && info != null) { + entry.title = info.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + } } return entry; } @@ -357,9 +577,9 @@ public class IconCache { */ public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, Bitmap icon, CharSequence title) { - remove(packageName, user); + removeFromMemCacheLocked(packageName, user); - CacheEntry entry = getEntryForPackage(packageName, user); + CacheEntry entry = getEntryForPackageLocked(packageName, user, false); if (!TextUtils.isEmpty(title)) { entry.title = title; } @@ -371,56 +591,68 @@ public class IconCache { /** * Gets an entry for the package, which can be used as a fallback entry for various components. * This method is not thread safe, it must be called from a synchronized method. + * */ - private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { - ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);; - CacheKey cacheKey = new CacheKey(cn, user); + private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, + boolean useLowResIcon) { + ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); + ComponentKey cacheKey = new ComponentKey(cn, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - entry.title = ""; - mCache.put(cacheKey, entry); + boolean entryUpdated = true; - try { - ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0); - entry.title = info.loadLabel(mPackageManager); - entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext); - } catch (NameNotFoundException e) { - if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + // Check the DB first. + if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + ApplicationInfo appInfo = info.applicationInfo; + if (appInfo == null) { + throw new NameNotFoundException("ApplicationInfo is null"); + } + Drawable drawable = mUserManager.getBadgedDrawableForUser( + appInfo.loadIcon(mPackageManager), user); + entry.icon = Utilities.createIconBitmap(drawable, mContext); + entry.title = appInfo.loadLabel(mPackageManager); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + entry.isLowResIcon = false; + + // Add the icon in the DB here, since these do not get written during + // package updates. + ContentValues values = + newContentValues(entry.icon, entry.title.toString(), mPackageBgColor); + addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); + + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + entryUpdated = false; + } } - if (entry.icon == null) { - entry.icon = getPreloadedIcon(cn, user); + // Only add a filled-out entry to the cache + if (entryUpdated) { + mCache.put(cacheKey, entry); } } return entry; } - public synchronized HashMap<ComponentName,Bitmap> getAllIcons() { - HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>(); - for (CacheKey ck : mCache.keySet()) { - final CacheEntry e = mCache.get(ck); - set.put(ck.componentName, e.icon); - } - return set; - } - /** * Pre-load an icon into the persistent cache. * * <P>Queries for a component that does not exist in the package manager * will be answered by the persistent cache. * - * @param context application context * @param componentName the icon should be returned for this component * @param icon the icon to be persisted * @param dpi the native density of the icon */ - public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon, - int dpi) { + public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, + long userSerial) { // TODO rescale to the correct native DPI try { - PackageManager packageManager = context.getPackageManager(); + PackageManager packageManager = mContext.getPackageManager(); packageManager.getActivityIcon(componentName); // component is present on the system already, do nothing return; @@ -428,100 +660,208 @@ public class IconCache { // pass } - final String key = componentName.flattenToString(); - FileOutputStream resourceFile = null; + ContentValues values = newContentValues(icon, label, Color.TRANSPARENT); + values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, + CacheEntry entry, boolean lowRes) { + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, + IconDB.COLUMN_LABEL}, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {component.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(user))}, + null, null, null); try { - resourceFile = context.openFileOutput(getResourceFilename(componentName), - Context.MODE_PRIVATE); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) { - byte[] buffer = os.toByteArray(); - resourceFile.write(buffer, 0, buffer.length); - } else { - Log.w(TAG, "failed to encode cache for " + key); - return; - } - } catch (FileNotFoundException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } catch (IOException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } finally { - if (resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to save restored icon for: " + key, e); + if (c.moveToNext()) { + entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); + entry.isLowResIcon = lowRes; + entry.title = c.getString(1); + if (entry.title == null) { + entry.title = ""; + entry.contentDescription = ""; + } else { + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } + return true; } + } finally { + c.close(); + } + return false; + } + + public static class IconLoadRequest { + private final Runnable mRunnable; + private final Handler mHandler; + + IconLoadRequest(Runnable runnable, Handler handler) { + mRunnable = runnable; + mHandler = handler; + } + + public void cancel() { + mHandler.removeCallbacks(mRunnable); } } /** - * Read a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - * @returns a bitmap if one is cached, or null. + * A runnable that updates invalid icons and adds missing icons in the DB for the provided + * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the + * worker thread doesn't get blocked. */ - private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) { - final String key = componentName.flattenToShortString(); - - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle())) { - return null; + @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>(); + + @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, + Stack<LauncherActivityInfoCompat> appsToAdd, + Stack<LauncherActivityInfoCompat> appsToUpdate) { + mUserSerial = userSerial; + mPkgInfoMap = pkgInfoMap; + mAppsToAdd = appsToAdd; + mAppsToUpdate = appsToUpdate; } - if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); - Bitmap icon = null; - FileInputStream resourceFile = null; - try { - resourceFile = mContext.openFileInput(getResourceFilename(componentName)); - byte[] buffer = new byte[1024]; - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int bytesRead = 0; - while(bytesRead >= 0) { - bytes.write(buffer, 0, bytesRead); - bytesRead = resourceFile.read(buffer, 0, buffer.length); + @Override + public void run() { + if (!mAppsToUpdate.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToUpdate.pop(); + String cn = app.getComponentName().flattenToString(); + ContentValues values = updateCacheAndGetContentValues(app, true); + mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {cn, Long.toString(mUserSerial)}); + mUpdatedPackages.add(app.getComponentName().getPackageName()); + + if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { + // No more app to update. Notify model. + LauncherAppState.getInstance().getModel().onPackageIconsUpdated( + mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial)); + } + + // Let it run one more time. + scheduleNext(); + } else if (!mAppsToAdd.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToAdd.pop(); + PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName()); + if (info != null) { + synchronized (IconCache.this) { + addIconToDBAndMemCache(app, info, mUserSerial); + } + } + + if (!mAppsToAdd.isEmpty()) { + scheduleNext(); + } } - if (DEBUG) Log.d(TAG, "read " + bytes.size()); - icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size()); - if (icon == null) { - Log.w(TAG, "failed to decode pre-load icon for " + key); + } + + public void scheduleNext() { + mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1); + } + } + + private void updateSystemStateString() { + mSystemState = Locale.getDefault().toString(); + } + + private static final class IconDB extends SQLiteOpenHelper { + private final static int DB_VERSION = 6; + + private final static String TABLE_NAME = "icons"; + private final static String COLUMN_ROWID = "rowid"; + private final static String COLUMN_COMPONENT = "componentName"; + private final static String COLUMN_USER = "profileId"; + private final static String COLUMN_LAST_UPDATED = "lastUpdated"; + private final static String COLUMN_VERSION = "version"; + private final static String COLUMN_ICON = "icon"; + private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; + private final static String COLUMN_LABEL = "label"; + private final static String COLUMN_SYSTEM_STATE = "system_state"; + + public IconDB(Context context) { + super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_ICON + " BLOB, " + + COLUMN_ICON_LOW_RES + " BLOB, " + + COLUMN_LABEL + " TEXT, " + + COLUMN_SYSTEM_STATE + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } - } catch (FileNotFoundException e) { - if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key); - } catch (IOException e) { - Log.w(TAG, "failed to read pre-load icon for: " + key, e); - } finally { - if(resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to manage pre-load icon file: " + key, e); - } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } } - return icon; + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } } - /** - * Remove a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - */ - public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) { - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) { - return; + private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) { + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon)); + + values.put(IconDB.COLUMN_LABEL, label); + values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState); + + if (lowResBackgroundColor == Color.TRANSPARENT) { + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap( + Bitmap.createScaledBitmap(icon, + icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, true))); + } else { + synchronized (this) { + if (mLowResBitmap == null) { + mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565); + mLowResCanvas = new Canvas(mLowResBitmap); + mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + } + mLowResCanvas.drawColor(lowResBackgroundColor); + mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()), + new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()), + mLowResPaint); + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap)); + } } - remove(componentName, user); - boolean success = mContext.deleteFile(getResourceFilename(componentName)); - if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache"); + return values; } - private static String getResourceFilename(ComponentName component) { - String resourceName = component.flattenToShortString(); - String filename = resourceName.replace(File.separatorChar, '_'); - return RESOURCE_FILE_PREFIX + filename; + private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) { + byte[] data = c.getBlob(iconIndex); + try { + return BitmapFactory.decodeByteArray(data, 0, data.length, options); + } catch (Exception e) { + return null; + } } } diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java index 7e55af228..d93cdcc1b 100644 --- a/src/com/android/launcher3/InfoDropTarget.java +++ b/src/com/android/launcher3/InfoDropTarget.java @@ -18,21 +18,12 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; import com.android.launcher3.compat.UserHandleCompat; public class InfoDropTarget extends ButtonDropTarget { - private ColorStateList mOriginalTextColor; - private TransitionDrawable mDrawable; - public InfoDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -44,96 +35,44 @@ public class InfoDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.info_target_hover_tint); - mDrawable = (TransitionDrawable) getCurrentDrawable(); - - if (mDrawable == null) { - // TODO: investigate why this is ever happening. Presently only on one known device. - mDrawable = (TransitionDrawable) r.getDrawable(R.drawable.info_target_selector); - setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); - } + mHoverColor = getResources().getColor(R.color.info_target_hover_tint); - if (null != mDrawable) { - mDrawable.setCrossFadeEnabled(true); - } - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } + setDrawable(R.drawable.ic_info_launcher); } - @Override - public boolean acceptDrop(DragObject d) { - // acceptDrop is called just before onDrop. We do the work here, rather than - // in onDrop, because it allows us to reject the drop (by returning false) - // so that the object being dragged isn't removed from the drag source. + public static void startDetailsActivityForInfo(Object info, Launcher launcher) { ComponentName componentName = null; - if (d.dragInfo instanceof AppInfo) { - componentName = ((AppInfo) d.dragInfo).componentName; - } else if (d.dragInfo instanceof ShortcutInfo) { - componentName = ((ShortcutInfo) d.dragInfo).intent.getComponent(); - } else if (d.dragInfo instanceof PendingAddItemInfo) { - componentName = ((PendingAddItemInfo) d.dragInfo).componentName; + if (info instanceof AppInfo) { + componentName = ((AppInfo) info).componentName; + } else if (info instanceof ShortcutInfo) { + componentName = ((ShortcutInfo) info).intent.getComponent(); + } else if (info instanceof PendingAddItemInfo) { + componentName = ((PendingAddItemInfo) info).componentName; } final UserHandleCompat user; - if (d.dragInfo instanceof ItemInfo) { - user = ((ItemInfo) d.dragInfo).user; + if (info instanceof ItemInfo) { + user = ((ItemInfo) info).user; } else { user = UserHandleCompat.myUserHandle(); } if (componentName != null) { - mLauncher.startApplicationDetailsActivity(componentName, user); - } - - // There is no post-drop animation, so clean up the DragView now - d.deferDragViewCleanupPostAnimation = false; - return false; - } - - @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; - - // Hide this button unless we are dragging something from AllApps - if (!source.supportsAppInfoDropTarget()) { - isVisible = false; + launcher.startApplicationDetailsActivity(componentName, user); } - - mActive = isVisible; - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); } @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; + protected boolean supportsDrop(DragSource source, Object info) { + return source.supportsAppInfoDropTarget() && supportsDrop(getContext(), info); } - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - mDrawable.startTransition(mTransitionDuration); - setTextColor(mHoverColor); + public static boolean supportsDrop(Context context, Object info) { + return info instanceof AppInfo || info instanceof PendingAddItemInfo; } - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - } + @Override + void completeDrop(DragObject d) { + startDetailsActivityForInfo(d.dragInfo, mLauncher); } } diff --git a/src/com/android/launcher3/Insettable.java b/src/com/android/launcher3/Insettable.java index 1d2356c65..3b8ef2f93 100644 --- a/src/com/android/launcher3/Insettable.java +++ b/src/com/android/launcher3/Insettable.java @@ -18,6 +18,10 @@ package com.android.launcher3; import android.graphics.Rect; +/** + * Allows the implementing {@link View} to not draw underneath system bars. + * e.g., notification bar on top and home key area on the bottom. + */ public interface Insettable { void setInsets(Rect insets); diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 1ab308558..3fc8fc0a9 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.text.TextUtils; @@ -32,6 +33,7 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.Thunk; import org.json.JSONException; import org.json.JSONObject; @@ -144,12 +146,26 @@ public class InstallShortcutReceiver extends BroadcastReceiver { return; } - if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0)); PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + if (info.launchIntent == null || info.label == null) { + if (DBG) Log.e(TAG, "Invalid install shortcut intent"); + return; + } + info = convertToLauncherActivityIfPossible(info); queuePendingShortcutInfo(info, context); } + public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + if (info.launchIntent == null || info.label == null) { + if (DBG) Log.e(TAG, "Invalid install shortcut intent"); + return null; + } + info = convertToLauncherActivityIfPossible(info); + return info.getShortcutInfo(); + } + static void queueInstallShortcut(LauncherActivityInfoCompat info, Context context) { queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context); } @@ -186,11 +202,6 @@ public class InstallShortcutReceiver extends BroadcastReceiver { final PendingInstallShortcutInfo pendingInfo = iter.next(); final Intent intent = pendingInfo.launchIntent; - if (LauncherAppState.isDisableAllApps() && !isValidShortcutLaunchIntent(intent)) { - if (DBG) Log.d(TAG, "Ignoring shortcut with launchIntent:" + intent); - continue; - } - // If the intent specifies a package, make sure the package exists String packageName = pendingInfo.getTargetPackage(); if (!TextUtils.isEmpty(packageName)) { @@ -201,56 +212,28 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } } - final boolean exists = LauncherModel.shortcutExists(context, pendingInfo.label, - intent, pendingInfo.user); - if (!exists) { - // Generate a shortcut info to add into the model - addShortcuts.add(pendingInfo.getShortcutInfo()); - } + // Generate a shortcut info to add into the model + addShortcuts.add(pendingInfo.getShortcutInfo()); } // Add the new apps to the model and bind them if (!addShortcuts.isEmpty()) { LauncherAppState app = LauncherAppState.getInstance(); - app.getModel().addAndBindAddedWorkspaceApps(context, addShortcuts); + app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts); } } } /** - * Returns true if the intent is a valid launch intent for a shortcut. - * This is used to identify shortcuts which are different from the ones exposed by the - * applications' manifest file. - * - * When DISABLE_ALL_APPS is true, shortcuts exposed via the app's manifest should never be - * duplicated or removed(unless the app is un-installed). - * - * @param launchIntent The intent that will be launched when the shortcut is clicked. - */ - static boolean isValidShortcutLaunchIntent(Intent launchIntent) { - if (launchIntent != null - && Intent.ACTION_MAIN.equals(launchIntent.getAction()) - && launchIntent.getComponent() != null - && launchIntent.getCategories() != null - && launchIntent.getCategories().size() == 1 - && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) - && launchIntent.getExtras() == null - && TextUtils.isEmpty(launchIntent.getDataString())) { - return false; - } - return true; - } - - /** * Ensures that we have a valid, non-null name. If the provided name is null, we will return * the application name instead. */ - private static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { + @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { if (name == null) { try { PackageManager pm = context.getPackageManager(); ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); - name = info.loadLabel(pm).toString(); + name = info.loadLabel(pm); } catch (PackageManager.NameNotFoundException nnfe) { return ""; } @@ -335,7 +318,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) .key(NAME_KEY).value(name); if (icon != null) { - byte[] iconByteArray = ItemInfo.flattenBitmap(icon); + byte[] iconByteArray = Utilities.flattenBitmap(icon); json = json.key(ICON_KEY).value( Base64.encodeToString( iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); @@ -354,16 +337,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { public ShortcutInfo getShortcutInfo() { if (activityInfo != null) { - final ShortcutInfo info = new ShortcutInfo(); - info.user = user; - info.title = label; - info.contentDescription = label; - info.customIcon = false; - info.intent = launchIntent; - info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; - info.flags = AppInfo.initFlags(activityInfo); - info.firstInstallTime = activityInfo.getFirstInstallTime(); - return info; + return ShortcutInfo.fromActivityInfo(activityInfo, mContext); } else { return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data); } @@ -377,6 +351,10 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return packageName; } + + public boolean isLuncherActivity() { + return activityInfo != null; + } } private static PendingInstallShortcutInfo decode(String encoded, Context context) { @@ -424,4 +402,34 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return null; } + + /** + * Tries to create a new PendingInstallShortcutInfo which represents the same target, + * but is an app target and not a shortcut. + * @return the newly created info or the original one. + */ + private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( + PendingInstallShortcutInfo original) { + if (original.isLuncherActivity()) { + // Already an activity target + return original; + } + if (!Utilities.isLauncherAppTarget(original.launchIntent) + || !original.user.equals(UserHandleCompat.myUserHandle())) { + // We can only convert shortcuts which point to a main activity in the current user. + return original; + } + + PackageManager pm = original.mContext.getPackageManager(); + ResolveInfo info = pm.resolveActivity(original.launchIntent, 0); + + if (info == null) { + return original; + } + + // Ignore any conflicts in the label name, as that can change based on locale. + LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat + .fromResolveInfo(info, original.mContext); + return new PendingInstallShortcutInfo(launcherInfo, original.mContext); + } } diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java index 2898b347d..29df38bae 100644 --- a/src/com/android/launcher3/InterruptibleInOutAnimator.java +++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java @@ -21,6 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.view.View; +import com.android.launcher3.util.Thunk; + /** * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation. * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get @@ -43,7 +45,7 @@ public class InterruptibleInOutAnimator { private static final int OUT = 2; // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz - private int mDirection = STOPPED; + @Thunk int mDirection = STOPPED; public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) { mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration); diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java new file mode 100644 index 000000000..ae204c40c --- /dev/null +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -0,0 +1,303 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class InvariantDeviceProfile { + + // This is a static that we use for the default icon size on a 4/5-inch phone + private static float DEFAULT_ICON_SIZE_DP = 60; + + private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48; + + // Constants that affects the interpolation curve between statically defined device profile + // buckets. + private static float KNEARESTNEIGHBOR = 3; + private static float WEIGHT_POWER = 5; + + // used to offset float not being able to express extremely small weights in extreme cases. + private static float WEIGHT_EFFICIENT = 100000f; + + // Profile-defining invariant properties + String name; + float minWidthDps; + float minHeightDps; + + /** + * Number of icons per row and column in the workspace. + */ + public int numRows; + public int numColumns; + + /** + * The minimum number of predicted apps in all apps. + */ + int minAllAppsPredictionColumns; + + /** + * Number of icons per row and column in the folder. + */ + public int numFolderRows; + public int numFolderColumns; + float iconSize; + int iconBitmapSize; + int fillResIconDpi; + float iconTextSize; + + /** + * Number of icons inside the hotseat area. + */ + float numHotseatIcons; + float hotseatIconSize; + int defaultLayoutId; + + // Derived invariant properties + int hotseatAllAppsRank; + + DeviceProfile landscapeProfile; + DeviceProfile portraitProfile; + + InvariantDeviceProfile() { + } + + public InvariantDeviceProfile(InvariantDeviceProfile p) { + this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, + p.numFolderRows, p.numFolderColumns, p.minAllAppsPredictionColumns, + p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize, + p.defaultLayoutId); + } + + InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, + float is, float its, float hs, float his, int dlId) { + // Ensure that we have an odd number of hotseat items (since we need to place all apps) + if (hs % 2 == 0) { + throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); + } + + name = n; + minWidthDps = w; + minHeightDps = h; + numRows = r; + numColumns = c; + numFolderRows = fr; + numFolderColumns = fc; + minAllAppsPredictionColumns = maapc; + iconSize = is; + iconTextSize = its; + numHotseatIcons = hs; + hotseatIconSize = his; + defaultLayoutId = dlId; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + InvariantDeviceProfile(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + DisplayMetrics dm = new DisplayMetrics(); + display.getMetrics(dm); + + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + + // This guarantees that width < height + minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); + minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); + + ArrayList<InvariantDeviceProfile> closestProfiles = + findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); + InvariantDeviceProfile interpolatedDeviceProfileOut = + invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); + + InvariantDeviceProfile closestProfile = closestProfiles.get(0); + numRows = closestProfile.numRows; + numColumns = closestProfile.numColumns; + numHotseatIcons = closestProfile.numHotseatIcons; + hotseatAllAppsRank = (int) (numHotseatIcons / 2); + defaultLayoutId = closestProfile.defaultLayoutId; + numFolderRows = closestProfile.numFolderRows; + numFolderColumns = closestProfile.numFolderColumns; + minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns; + + iconSize = interpolatedDeviceProfileOut.iconSize; + iconBitmapSize = Utilities.pxFromDp(iconSize, dm); + iconTextSize = interpolatedDeviceProfileOut.iconTextSize; + hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize; + fillResIconDpi = getLauncherIconDensity(iconBitmapSize); + + // If the partner customization apk contains any grid overrides, apply them + // Supported overrides: numRows, numColumns, iconSize + applyPartnerDeviceProfileOverrides(context, dm); + + Point realSize = new Point(); + display.getRealSize(realSize); + // The real size never changes. smallSide and largeSide will remain the + // same in any orientation. + int smallSide = Math.min(realSize.x, realSize.y); + int largeSide = Math.max(realSize.x, realSize.y); + + landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize, + largeSide, smallSide, true /* isLandscape */); + portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize, + smallSide, largeSide, false /* isLandscape */); + } + + ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() { + ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>(); + // width, height, #rows, #columns, #folder rows, #folder columns, + // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", + 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", + 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", + 275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", + 255, 450, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", + 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", + 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", + 359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", + 406, 694, 5, 5, 4, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); + // The tablet profile is odd in that the landscape orientation + // also includes the nav bar on the side + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", + 575, 904, 5, 6, 4, 5, 4, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); + // Larger tablet profiles always have system bars on the top & bottom + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", + 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", + 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + return predefinedDeviceProfiles; + } + + private int getLauncherIconDensity(int requiredSize) { + // Densities typically defined by an app. + int[] densityBuckets = new int[] { + DisplayMetrics.DENSITY_LOW, + DisplayMetrics.DENSITY_MEDIUM, + DisplayMetrics.DENSITY_TV, + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XHIGH, + DisplayMetrics.DENSITY_XXHIGH, + DisplayMetrics.DENSITY_XXXHIGH + }; + + int density = DisplayMetrics.DENSITY_XXXHIGH; + for (int i = densityBuckets.length - 1; i >= 0; i--) { + float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] + / DisplayMetrics.DENSITY_DEFAULT; + if (expectedSize >= requiredSize) { + density = densityBuckets[i]; + } + } + + return density; + } + + /** + * Apply any Partner customization grid overrides. + * + * Currently we support: all apps row / column count. + */ + private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { + Partner p = Partner.get(context.getPackageManager()); + if (p != null) { + p.applyInvariantDeviceProfileOverrides(this, dm); + } + } + + @Thunk float dist(float x0, float y0, float x1, float y1) { + return (float) Math.hypot(x1 - x0, y1 - y0); + } + + /** + * Returns the closest device profiles ordered by closeness to the specified width and height + */ + // Package private visibility for testing. + ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles( + final float width, final float height, ArrayList<InvariantDeviceProfile> points) { + + // Sort the profiles by their closeness to the dimensions + ArrayList<InvariantDeviceProfile> pointsByNearness = points; + Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() { + public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { + return (int) (dist(width, height, a.minWidthDps, a.minHeightDps) + - dist(width, height, b.minWidthDps, b.minHeightDps)); + } + }); + + return pointsByNearness; + } + + // Package private visibility for testing. + InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, + ArrayList<InvariantDeviceProfile> points) { + float weights = 0; + + InvariantDeviceProfile p = points.get(0); + if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { + return p; + } + + InvariantDeviceProfile out = new InvariantDeviceProfile(); + for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { + p = new InvariantDeviceProfile(points.get(i)); + float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); + weights += w; + out.add(p.multiply(w)); + } + return out.multiply(1.0f/weights); + } + + private void add(InvariantDeviceProfile p) { + iconSize += p.iconSize; + iconTextSize += p.iconTextSize; + hotseatIconSize += p.hotseatIconSize; + } + + private InvariantDeviceProfile multiply(float w) { + iconSize *= w; + iconTextSize *= w; + hotseatIconSize *= w; + return this; + } + + private float weight(float x0, float y0, float x1, float y1, float pow) { + float d = dist(x0, y0, x1, y1); + if (Float.compare(d, 0f) == 0) { + return Float.POSITIVE_INFINITY; + } + return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index 09b77f756..f7e0ea488 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -20,13 +20,10 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.util.Log; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.util.Arrays; /** @@ -39,7 +36,7 @@ public class ItemInfo { */ static final String EXTRA_PROFILE = "profile"; - static final int NO_ID = -1; + public static final int NO_ID = -1; /** * The id in the settings database for this item @@ -85,7 +82,7 @@ public class ItemInfo { /** * Indicates the Y cell span. */ - int spanY = 1; + public int spanY = 1; /** * Indicates the minimum X cell span. @@ -98,6 +95,11 @@ public class ItemInfo { public int minSpanY = 1; /** + * Indicates the position in an ordered list. + */ + public int rank = 0; + + /** * Indicates that this item needs to be updated in the db */ public boolean requiresDbUpdate = false; @@ -105,21 +107,21 @@ public class ItemInfo { /** * Title of the item */ - CharSequence title; + public CharSequence title; /** * Content description of the item. */ - CharSequence contentDescription; + public CharSequence contentDescription; /** * The position of the item in a drag-and-drop operation. */ - int[] dropPos = null; + public int[] dropPos = null; - UserHandleCompat user; + public UserHandleCompat user; - ItemInfo() { + public ItemInfo() { user = UserHandleCompat.myUserHandle(); } @@ -135,6 +137,7 @@ public class ItemInfo { cellY = info.cellY; spanX = info.spanX; spanY = info.spanY; + rank = info.rank; screenId = info.screenId; itemType = info.itemType; container = info.container; @@ -161,6 +164,7 @@ public class ItemInfo { values.put(LauncherSettings.Favorites.CELLY, cellY); values.put(LauncherSettings.Favorites.SPANX, spanX); values.put(LauncherSettings.Favorites.SPANY, spanY); + values.put(LauncherSettings.Favorites.RANK, rank); long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); values.put(LauncherSettings.Favorites.PROFILE_ID, serialNumber); @@ -170,30 +174,9 @@ public class ItemInfo { } } - void updateValuesWithCoordinates(ContentValues values, int cellX, int cellY) { - values.put(LauncherSettings.Favorites.CELLX, cellX); - values.put(LauncherSettings.Favorites.CELLY, cellY); - } - - static byte[] flattenBitmap(Bitmap bitmap) { - // Try go guesstimate how much space the icon will take when serialized - // to avoid unnecessary allocations/copies during the write. - int size = bitmap.getWidth() * bitmap.getHeight() * 4; - ByteArrayOutputStream out = new ByteArrayOutputStream(size); - try { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); - out.flush(); - out.close(); - return out.toByteArray(); - } catch (IOException e) { - Log.w("Favorite", "Could not write icon"); - return null; - } - } - static void writeBitmap(ContentValues values, Bitmap bitmap) { if (bitmap != null) { - byte[] data = flattenBitmap(bitmap); + byte[] data = Utilities.flattenBitmap(bitmap); values.put(LauncherSettings.Favorites.ICON, data); } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index ac46fd33d..6648b6e74 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1,4 +1,3 @@ - /* * Copyright (C) 2008 The Android Open Source Project * @@ -22,8 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; @@ -37,25 +36,24 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; @@ -66,6 +64,7 @@ import android.os.Handler; import android.os.Message; import android.os.StrictMode; import android.os.SystemClock; +import android.os.UserHandle; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -82,40 +81,41 @@ import android.view.Surface; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; -import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; +import android.view.animation.OvershootInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.PagedView.PageSwitchListener; +import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; -import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.launcher3.widget.WidgetHostViewLoader; +import com.android.launcher3.widget.WidgetsContainerView; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.FileDescriptor; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.DateFormat; @@ -132,12 +132,13 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, - View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { + View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener, + LauncherStateTransitionAnimation.Callbacks { static final String TAG = "Launcher"; static final boolean LOGD = false; static final boolean PROFILE_STARTUP = false; - static final boolean DEBUG_WIDGETS = false; + static final boolean DEBUG_WIDGETS = true; static final boolean DEBUG_STRICT_MODE = false; static final boolean DEBUG_RESUME_TIME = false; static final boolean DEBUG_DUMP_LOG = false; @@ -146,28 +147,29 @@ public class Launcher extends Activity private static final int REQUEST_CREATE_SHORTCUT = 1; private static final int REQUEST_CREATE_APPWIDGET = 5; - private static final int REQUEST_PICK_SHORTCUT = 7; private static final int REQUEST_PICK_APPWIDGET = 9; private static final int REQUEST_PICK_WALLPAPER = 10; private static final int REQUEST_BIND_APPWIDGET = 11; private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; + private static final int WORKSPACE_BACKGROUND_GRADIENT = 0; + private static final int WORKSPACE_BACKGROUND_TRANSPARENT = 1; + private static final int WORKSPACE_BACKGROUND_BLACK = 2; + + private static final float BOUNCE_ANIMATION_TENSION = 1.3f; + /** * IntentStarter uses request codes starting with this. This must be greater than all activity * request codes used internally. */ protected static final int REQUEST_LAST = 100; - static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; - static final int SCREEN_COUNT = 5; - static final int DEFAULT_SCREEN = 2; // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] static final String DUMP_STATE_PROPERTY = "launcher_dump_state"; - static final String DISABLE_ALL_APPS_PROPERTY = "launcher_noallapps"; // The Intent extra that defines whether to ignore the launch animation static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = @@ -185,10 +187,6 @@ public class Launcher extends Activity private static final String RUNTIME_STATE_PENDING_ADD_CELL_X = "launcher.add_cell_x"; // Type: int private static final String RUNTIME_STATE_PENDING_ADD_CELL_Y = "launcher.add_cell_y"; - // Type: boolean - private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME = "launcher.rename_folder"; - // Type: long - private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME_ID = "launcher.rename_folder_id"; // Type: int private static final String RUNTIME_STATE_PENDING_ADD_SPAN_X = "launcher.add_span_x"; // Type: int @@ -216,9 +214,10 @@ 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. */ - private enum State { NONE, WORKSPACE, APPS_CUSTOMIZE, APPS_CUSTOMIZE_SPRING_LOADED }; - private State mState = State.WORKSPACE; - private AnimatorSet mStateAnimation; + enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED } + + @Thunk State mState = State.WORKSPACE; + @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation; private boolean mIsSafeModeEnabled; @@ -231,50 +230,50 @@ public class Launcher extends Activity private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500; private static final int ACTIVITY_START_DELAY = 1000; - private static final Object sLock = new Object(); - private static int sScreen = DEFAULT_SCREEN; - private HashMap<Integer, Integer> mItemIdToViewId = new HashMap<Integer, Integer>(); private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); // How long to wait before the new-shortcut animation automatically pans the workspace private static int NEW_APPS_PAGE_MOVE_DELAY = 500; private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5; - private static int NEW_APPS_ANIMATION_DELAY = 500; - private static final int SINGLE_FRAME_DELAY = 16; + @Thunk static int NEW_APPS_ANIMATION_DELAY = 500; private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); - private final ContentObserver mWidgetObserver = new AppWidgetResetObserver(); private LayoutInflater mInflater; - private Workspace mWorkspace; + @Thunk Workspace mWorkspace; private View mLauncherView; private View mPageIndicators; - private DragLayer mDragLayer; + @Thunk DragLayer mDragLayer; private DragController mDragController; private View mWeightWatcher; private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; - private ItemInfo mPendingAddInfo = new ItemInfo(); - private AppWidgetProviderInfo mPendingAddWidgetInfo; + @Thunk ItemInfo mPendingAddInfo = new ItemInfo(); + private LauncherAppWidgetProviderInfo mPendingAddWidgetInfo; private int mPendingAddWidgetId = -1; private int[] mTmpAddItemCellCoordinates = new int[2]; - private FolderInfo mFolderInfo; - - private Hotseat mHotseat; + @Thunk Hotseat mHotseat; private ViewGroup mOverviewPanel; private View mAllAppsButton; + private View mWidgetsButton; private SearchDropTargetBar mSearchDropTargetBar; - private AppsCustomizeTabHost mAppsCustomizeTabHost; - private AppsCustomizePagedView mAppsCustomizeContent; + + // Main container view for the all apps screen. + @Thunk AllAppsContainerView mAppsView; + + // Main container view and the model for the widget tray screen. + @Thunk WidgetsContainerView mWidgetsView; + @Thunk WidgetsModel mWidgetsModel; + private boolean mAutoAdvanceRunning = false; private AppWidgetHostView mQsb; @@ -286,7 +285,7 @@ public class Launcher extends Activity private SpannableStringBuilder mDefaultKeySsb = null; - private boolean mWorkspaceLoading = true; + @Thunk boolean mWorkspaceLoading = true; private boolean mPaused = true; private boolean mRestoring; @@ -300,14 +299,12 @@ public class Launcher extends Activity private LauncherModel mModel; private IconCache mIconCache; - private boolean mUserPresent = true; + @Thunk boolean mUserPresent = true; private boolean mVisible = false; private boolean mHasFocus = false; private boolean mAttached = false; - private static LocaleConfiguration sLocaleConfiguration = null; - - private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>(); + private static LongArrayMap<FolderInfo> sFolders = new LongArrayMap<>(); private View.OnTouchListener mHapticFeedbackTouchListener; @@ -317,14 +314,14 @@ public class Launcher extends Activity private final int mAdvanceStagger = 250; private long mAutoAdvanceSentTime; private long mAutoAdvanceTimeLeft = -1; - private HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = + @Thunk HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = new HashMap<View, AppWidgetProviderInfo>(); // Determines how long to wait after a rotation before restoring the screen orientation to // match the sensor state. private final int mRestoreScreenOrientationDelay = 500; - private Drawable mWorkspaceBackgroundDrawable; + @Thunk Drawable mWorkspaceBackgroundDrawable; private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>(); private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false; @@ -340,18 +337,43 @@ public class Launcher extends Activity // it from the context. private SharedPreferences mSharedPrefs; - private static ArrayList<ComponentName> mIntentsOnWorkspaceFromUpgradePath = null; - // Holds the page that we need to animate to, and the icon views that we need to animate up // when we scroll to that page on resume. - private ImageView mFolderIconImageView; + @Thunk ImageView mFolderIconImageView; private Bitmap mFolderIconBitmap; private Canvas mFolderIconCanvas; private Rect mRectForFolderAnimation = new Rect(); + private DeviceProfile mDeviceProfile; + + // This is set to the view that launched the activity that navigated the user away from + // launcher. Since there is no callback for when the activity has finished launching, enable + // the press state and keep this reference to reset the press state when we return to launcher. private BubbleTextView mWaitingForResume; - private Runnable mBuildLayersRunnable = new Runnable() { + protected static HashMap<String, CustomAppWidget> sCustomAppWidgets = + new HashMap<String, CustomAppWidget>(); + + private static final boolean ENABLE_CUSTOM_WIDGET_TEST = false; + static { + if (ENABLE_CUSTOM_WIDGET_TEST) { + sCustomAppWidgets.put(DummyWidget.class.getName(), new DummyWidget()); + } + } + + // TODO: remove this field and call method directly when Launcher3 can depend on M APIs + private static Method sClipRevealMethod = null; + static { + Class<?> activityOptionsClass = ActivityOptions.class; + try { + sClipRevealMethod = activityOptionsClass.getDeclaredMethod("makeClipRevealAnimation", + View.class, int.class, int.class, int.class, int.class); + } catch (Exception e) { + // Earlier version + } + } + + @Thunk Runnable mBuildLayersRunnable = new Runnable() { public void run() { if (mWorkspace != null) { mWorkspace.buildPageHardwareLayers(); @@ -361,7 +383,7 @@ public class Launcher extends Activity private static PendingAddArguments sPendingAddItem; - private static class PendingAddArguments { + @Thunk static class PendingAddArguments { int requestCode; Intent intent; long container; @@ -372,8 +394,23 @@ public class Launcher extends Activity } private Stats mStats; - FocusIndicatorView mFocusHandler; + private boolean mRotationEnabled = false; + + @Thunk void setOrientation() { + if (mRotationEnabled) { + unlockScreenOrientation(true); + } else { + setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); + } + } + + private Runnable mUpdateOrientationRunnable = new Runnable() { + public void run() { + setOrientation(); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -400,20 +437,22 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); - LauncherAppState.getLauncherProvider().setLauncherProviderChangeListener(this); - // Lazy-initialize the dynamic grid - DeviceProfile grid = app.initDynamicGrid(this); + // Load configuration-specific DeviceProfile + mDeviceProfile = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE ? + app.getInvariantDeviceProfile().landscapeProfile + : app.getInvariantDeviceProfile().portraitProfile; - // the LauncherApplication should call this, but in case of Instrumentation it might not be present yet mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); mIsSafeModeEnabled = getPackageManager().isSafeMode(); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); - mIconCache.flushInvalidIcons(grid); + mDragController = new DragController(this); mInflater = getLayoutInflater(); + mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); mStats = new Stats(this); @@ -432,13 +471,10 @@ public class Launcher extends Activity Environment.getExternalStorageDirectory() + "/launcher"); } - checkForLocaleChange(); setContentView(R.layout.launcher); setupViews(); - grid.layout(this); - - registerContentObservers(); + mDeviceProfile.layout(this); lockAllApps(); @@ -453,11 +489,11 @@ public class Launcher extends Activity if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) { // If the user leaves launcher, then we should just load items asynchronously when // they return. - mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE); } else { // We only load the page synchronously if the user rotates (or triggers a // configuration change) while launcher is in the foreground - mModel.startLoader(true, mWorkspace.getRestorePage()); + mModel.startLoader(mWorkspace.getRestorePage()); } } @@ -468,8 +504,16 @@ 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 - unlockScreenOrientation(true); + 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) { + mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext(), false); + } + + // 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 + setOrientation(); if (mLauncherCallbacks != null) { mLauncherCallbacks.onCreate(savedInstanceState); @@ -490,6 +534,16 @@ public class Launcher extends Activity } } + @Override + public void onSettingsChanged(String settings, boolean value) { + if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(settings)) { + mRotationEnabled = value; + if (!waitUntilResume(mUpdateOrientationRunnable, true)) { + mUpdateOrientationRunnable.run(); + } + } + } + private LauncherCallbacks mLauncherCallbacks; public void onPostCreate(Bundle savedInstanceState) { @@ -501,6 +555,47 @@ public class Launcher extends Activity public boolean setLauncherCallbacks(LauncherCallbacks callbacks) { mLauncherCallbacks = callbacks; + mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() { + private boolean mWorkspaceImportanceStored = false; + private boolean mHotseatImportanceStored = false; + private int mWorkspaceImportanceForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + private int mHotseatImportanceForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + + @Override + public void onSearchOverlayOpened() { + if (mWorkspaceImportanceStored || mHotseatImportanceStored) { + return; + } + // The underlying workspace and hotseat are temporarily suppressed by the search + // overlay. So they sholudn't be accessible. + if (mWorkspace != null) { + mWorkspaceImportanceForAccessibility = + mWorkspace.getImportantForAccessibility(); + mWorkspace.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + mWorkspaceImportanceStored = true; + } + if (mHotseat != null) { + mHotseatImportanceForAccessibility = mHotseat.getImportantForAccessibility(); + mHotseat.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + mHotseatImportanceStored = true; + } + } + + @Override + public void onSearchOverlayClosed() { + if (mWorkspaceImportanceStored && mWorkspace != null) { + mWorkspace.setImportantForAccessibility(mWorkspaceImportanceForAccessibility); + } + if (mHotseatImportanceStored && mHotseat != null) { + mHotseat.setImportantForAccessibility(mHotseatImportanceForAccessibility); + } + mWorkspaceImportanceStored = false; + mHotseatImportanceStored = false; + } + }); return true; } @@ -511,6 +606,14 @@ public class Launcher extends Activity } } + /** + * Updates the bounds of all the overlays to match the new fixed bounds. + */ + public void updateOverlayBounds(Rect newBounds) { + mAppsView.setSearchBarBounds(newBounds); + mWidgetsView.setSearchBarBounds(newBounds); + } + /** To be overridden by subclasses to hint to Launcher that we have custom content */ protected boolean hasCustomContentToLeft() { if (mLauncherCallbacks != null) { @@ -549,108 +652,6 @@ public class Launcher extends Activity } } - private void checkForLocaleChange() { - if (sLocaleConfiguration == null) { - new AsyncTask<Void, Void, LocaleConfiguration>() { - @Override - protected LocaleConfiguration doInBackground(Void... unused) { - LocaleConfiguration localeConfiguration = new LocaleConfiguration(); - readConfiguration(Launcher.this, localeConfiguration); - return localeConfiguration; - } - - @Override - protected void onPostExecute(LocaleConfiguration result) { - sLocaleConfiguration = result; - checkForLocaleChange(); // recursive, but now with a locale configuration - } - }.execute(); - return; - } - - final Configuration configuration = getResources().getConfiguration(); - - final String previousLocale = sLocaleConfiguration.locale; - final String locale = configuration.locale.toString(); - - final int previousMcc = sLocaleConfiguration.mcc; - final int mcc = configuration.mcc; - - final int previousMnc = sLocaleConfiguration.mnc; - final int mnc = configuration.mnc; - - boolean localeChanged = !locale.equals(previousLocale) || mcc != previousMcc || mnc != previousMnc; - - if (localeChanged) { - sLocaleConfiguration.locale = locale; - sLocaleConfiguration.mcc = mcc; - sLocaleConfiguration.mnc = mnc; - - mIconCache.flush(); - - final LocaleConfiguration localeConfiguration = sLocaleConfiguration; - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - writeConfiguration(Launcher.this, localeConfiguration); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - } - - private static class LocaleConfiguration { - public String locale; - public int mcc = -1; - public int mnc = -1; - } - - private static void readConfiguration(Context context, LocaleConfiguration configuration) { - DataInputStream in = null; - try { - in = new DataInputStream(context.openFileInput(LauncherFiles.LAUNCHER_PREFERENCES)); - configuration.locale = in.readUTF(); - configuration.mcc = in.readInt(); - configuration.mnc = in.readInt(); - } catch (FileNotFoundException e) { - // Ignore - } catch (IOException e) { - // Ignore - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - // Ignore - } - } - } - } - - private static void writeConfiguration(Context context, LocaleConfiguration configuration) { - DataOutputStream out = null; - try { - out = new DataOutputStream(context.openFileOutput( - LauncherFiles.LAUNCHER_PREFERENCES, MODE_PRIVATE)); - out.writeUTF(configuration.locale); - out.writeInt(configuration.mcc); - out.writeInt(configuration.mnc); - out.flush(); - } catch (FileNotFoundException e) { - // Ignore - } catch (IOException e) { - //noinspection ResultOfMethodCallIgnored - context.getFileStreamPath(LauncherFiles.LAUNCHER_PREFERENCES).delete(); - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - // Ignore - } - } - } - } - public Stats getStats() { return mStats; } @@ -659,24 +660,13 @@ public class Launcher extends Activity return mInflater; } - boolean isDraggingEnabled() { + public boolean isDraggingEnabled() { // We prevent dragging when we are loading the workspace as it is possible to pick up a view // that is subsequently removed from the workspace in startBinding(). return !mModel.isLoadingWorkspace(); } - static int getScreen() { - synchronized (sLock) { - return sScreen; - } - } - - static void setScreen(int screen) { - synchronized (sLock) { - sScreen = screen; - } - } - + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static int generateViewId() { if (Build.VERSION.SDK_INT >= 17) { return View.generateViewId(); @@ -766,7 +756,7 @@ public class Launcher extends Activity return; } else if (requestCode == REQUEST_PICK_WALLPAPER) { if (resultCode == RESULT_OK && mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(false); + showWorkspace(false); } return; } @@ -882,6 +872,15 @@ public class Launcher extends Activity } } + /** @Override for MNC */ + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onRequestPermissionsResult(requestCode, permissions, + grantResults); + } + } + private PendingAddArguments preparePendingAddArgs(int requestCode, Intent data, int appWidgetId, ItemInfo info) { PendingAddArguments args = new PendingAddArguments(); @@ -914,7 +913,7 @@ public class Launcher extends Activity } } - private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + @Thunk void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { CellLayout cellLayout = (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); Runnable onCompleteRunnable = null; @@ -986,18 +985,26 @@ public class Launcher extends Activity // Restore the previous launcher state if (mOnResumeState == State.WORKSPACE) { showWorkspace(false); - } else if (mOnResumeState == State.APPS_CUSTOMIZE) { - showAllApps(false, mAppsCustomizeContent.getContentType(), false); + } else if (mOnResumeState == State.APPS) { + boolean launchedFromApp = (mWaitingForResume != null); + // Don't update the predicted apps if the user is returning to launcher in the apps + // 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 */, false /* resetListToTop */, + !launchedFromApp /* updatePredictedApps */, false /* focusSearchBar */); + } else if (mOnResumeState == State.WIDGETS) { + showWidgetsView(false, false); } mOnResumeState = State.NONE; - // Background was set to gradient in onPause(), restore to black if in all apps. - setWorkspaceBackground(mState == State.WORKSPACE); + // Background was set to gradient in onPause(), restore to transparent if in all apps. + setWorkspaceBackground(mState == State.WORKSPACE ? WORKSPACE_BACKGROUND_GRADIENT + : WORKSPACE_BACKGROUND_TRANSPARENT); mPaused = false; if (mRestoring || mOnResumeNeedsLoad) { setWorkspaceLoading(true); - mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE); mRestoring = false; mOnResumeNeedsLoad = false; } @@ -1009,15 +1016,9 @@ public class Launcher extends Activity startTimeCallbacks = System.currentTimeMillis(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(true); - } for (int i = 0; i < mBindOnResumeCallbacks.size(); i++) { mBindOnResumeCallbacks.get(i).run(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(false); - } mBindOnResumeCallbacks.clear(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent processing callbacks in onResume: " + @@ -1043,9 +1044,7 @@ public class Launcher extends Activity // (framework issue). On resuming, we ensure that any widgets are inflated for the current // orientation. getWorkspace().reinflateWidgetsIfNecessary(); - - // Process any items that were added while Launcher was away. - InstallShortcutReceiver.disableAndFlushInstallQueue(this); + reinflateQSBIfNecessary(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime)); @@ -1059,10 +1058,13 @@ public class Launcher extends Activity mWorkspace.getCustomContentCallbacks().onShow(true); } } - mWorkspace.updateInteractionForState(); + updateInteraction(Workspace.State.NORMAL, mWorkspace.getState()); mWorkspace.onResume(); - PackageInstallerCompat.getInstance(this).onResume(); + if (!isWorkspaceLoading()) { + // Process any items that were added while Launcher was away. + InstallShortcutReceiver.disableAndFlushInstallQueue(this); + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onResume(); @@ -1073,7 +1075,6 @@ public class Launcher extends Activity protected void onPause() { // Ensure that items added to Launcher are queued until Launcher returns InstallShortcutReceiver.enableInstallQueue(); - PackageInstallerCompat.getInstance(this).onPause(); super.onPause(); mPaused = true; @@ -1136,6 +1137,18 @@ public class Launcher extends Activity public void forceExitFullImmersion(); } + public interface LauncherSearchCallbacks { + /** + * Called when the search overlay is shown. + */ + public void onSearchOverlayOpened(); + + /** + * Called when the search overlay is dismissed. + */ + public void onSearchOverlayClosed(); + } + public interface LauncherOverlayCallbacks { /** * This method indicates whether a call to {@link #enterFullImmersion()} will succeed, @@ -1187,11 +1200,13 @@ 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, CustomContentCallbacks callbacks, String description) { mWorkspace.addToCustomContentPage(customContent, callbacks, description); @@ -1208,9 +1223,8 @@ public class Launcher extends Activity if (mModel.isCurrentCallbacks(this)) { mModel.stopLoader(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.surrender(); - } + //TODO(hyunyoungs): stop the widgets loader when there is a rotation. + return Boolean.TRUE; } @@ -1296,8 +1310,8 @@ public class Launcher extends Activity } State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); - if (state == State.APPS_CUSTOMIZE) { - mOnResumeState = State.APPS_CUSTOMIZE; + if (state == State.APPS || state == State.WIDGETS) { + mOnResumeState = state; } int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, @@ -1316,32 +1330,16 @@ public class Launcher extends Activity mPendingAddInfo.cellY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_Y); mPendingAddInfo.spanX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_X); mPendingAddInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); - mPendingAddWidgetInfo = savedState.getParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); + AppWidgetProviderInfo info = savedState.getParcelable( + RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); + mPendingAddWidgetInfo = info == null ? + null : LauncherAppWidgetProviderInfo.fromProviderInfo(this, info); + mPendingAddWidgetId = savedState.getInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID); setWaitingForResult(true); mRestoring = true; } - boolean renameFolder = savedState.getBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, false); - if (renameFolder) { - long id = savedState.getLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID); - mFolderInfo = mModel.getFolderById(this, sFolders, id); - mRestoring = true; - } - - // Restore the AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - String curTab = savedState.getString("apps_customize_currentTab"); - if (curTab != null) { - mAppsCustomizeTabHost.setContentTypeImmediate( - mAppsCustomizeTabHost.getContentTypeForTabTag(curTab)); - mAppsCustomizeContent.loadAssociatedPages( - mAppsCustomizeContent.getCurrentPage()); - } - - int currentIndex = savedState.getInt("apps_customize_currentIndex"); - mAppsCustomizeContent.restorePageForIndex(currentIndex); - } mItemIdToViewId = (HashMap<Integer, Integer>) savedState.getSerializable(RUNTIME_STATE_VIEW_IDS); } @@ -1369,13 +1367,12 @@ public class Launcher extends Activity // Setup the hotseat mHotseat = (Hotseat) findViewById(R.id.hotseat); if (mHotseat != null) { - mHotseat.setup(this); mHotseat.setOnLongClickListener(this); } mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); - View widgetButton = findViewById(R.id.widget_button); - widgetButton.setOnClickListener(new OnClickListener() { + mWidgetsButton = findViewById(R.id.widget_button); + mWidgetsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { if (!mWorkspace.isSwitchingState()) { @@ -1383,7 +1380,7 @@ public class Launcher extends Activity } } }); - widgetButton.setOnTouchListener(getHapticFeedbackTouchListener()); + mWidgetsButton.setOnTouchListener(getHapticFeedbackTouchListener()); View wallpaperButton = findViewById(R.id.wallpaper_button); wallpaperButton.setOnClickListener(new OnClickListener() { @@ -1423,11 +1420,14 @@ public class Launcher extends Activity mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.search_drop_target_bar); - // Setup AppsCustomize - mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); - mAppsCustomizeContent = (AppsCustomizePagedView) - mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content); - mAppsCustomizeContent.setup(this, dragController); + // Setup Apps and Widgets + mAppsView = (AllAppsContainerView) findViewById(R.id.apps_view); + mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view); + if (mLauncherCallbacks != null && mLauncherCallbacks.getAllAppsSearchBarController() != null) { + mAppsView.setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController()); + } else { + mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); + } // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); @@ -1436,7 +1436,7 @@ public class Launcher extends Activity dragController.addDropTarget(mWorkspace); if (mSearchDropTargetBar != null) { mSearchDropTargetBar.setup(this, dragController); - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } if (getResources().getBoolean(R.bool.debug_memory_enabled)) { @@ -1466,30 +1466,32 @@ public class Launcher extends Activity return mAllAppsButton; } + public View getWidgetsButton() { + return mWidgetsButton; + } + /** * Creates a view representing a shortcut. * * @param info The data structure describing the shortcut. - * - * @return A View inflated from R.layout.application. */ View createShortcut(ShortcutInfo info) { - return createShortcut(R.layout.application, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); } /** * Creates a view representing a shortcut inflated from the specified resource. * - * @param layoutResId The id of the XML layout used to create the shortcut. * @param parent The group the shortcut belongs to. * @param info The data structure describing the shortcut. * * @return A View inflated from layoutResId. */ - View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { - BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false); - favorite.applyFromShortcutInfo(info, mIconCache, true); + public View createShortcut(ViewGroup parent, ShortcutInfo info) { + BubbleTextView favorite = (BubbleTextView) mInflater.inflate(R.layout.app_icon, + parent, false); + favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); favorite.setOnClickListener(this); favorite.setOnFocusChangeListener(mFocusHandler); return favorite; @@ -1499,7 +1501,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) { @@ -1507,14 +1508,13 @@ public class Launcher extends Activity int[] touchXY = mPendingAddInfo.dropPos; CellLayout layout = getCellLayout(container, screenId); - boolean foundCellSpan = false; - - ShortcutInfo info = mModel.infoFromShortcutIntent(this, data); + ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(this, data); if (info == null) { return; } final View view = createShortcut(info); + boolean foundCellSpan = false; // First we check if we already know the exact location where we want to add this item. if (cellX >= 0 && cellY >= 0) { cellXY[0] = cellX; @@ -1545,7 +1545,7 @@ public class Launcher extends Activity return; } - LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1], false); + LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1]); if (!mRestoring) { mWorkspace.addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, @@ -1553,119 +1553,67 @@ public class Launcher extends Activity } } - static int[] getSpanForWidget(Context context, ComponentName component, int minWidth, - int minHeight) { - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context, component, null); + private int[] getSpanForWidget(ComponentName component, int minWidth, int minHeight) { + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(this, component, null); // We want to account for the extra amount of padding that we are adding to the widget // to ensure that it gets the full amount of space that it has requested int requiredWidth = minWidth + padding.left + padding.right; int requiredHeight = minHeight + padding.top + padding.bottom; - return CellLayout.rectToCell(requiredWidth, requiredHeight, null); - } - - static int[] getSpanForWidget(Context context, AppWidgetProviderInfo info) { - return getSpanForWidget(context, info.provider, info.minWidth, info.minHeight); - } - - static int[] getMinSpanForWidget(Context context, AppWidgetProviderInfo info) { - return getSpanForWidget(context, info.provider, info.minResizeWidth, info.minResizeHeight); + return CellLayout.rectToCell(this, requiredWidth, requiredHeight, null); } - static int[] getSpanForWidget(Context context, PendingAddWidgetInfo info) { - return getSpanForWidget(context, info.componentName, info.minWidth, info.minHeight); + public int[] getSpanForWidget(AppWidgetProviderInfo info) { + return getSpanForWidget(info.provider, info.minWidth, info.minHeight); } - static int[] getMinSpanForWidget(Context context, PendingAddWidgetInfo info) { - return getSpanForWidget(context, info.componentName, info.minResizeWidth, - info.minResizeHeight); + public int[] getMinSpanForWidget(AppWidgetProviderInfo info) { + return getSpanForWidget(info.provider, info.minResizeWidth, info.minResizeHeight); } /** * Add a widget to the workspace. * * @param appWidgetId The app widget id - * @param cellInfo The position on screen where to create the widget. */ - private void completeAddAppWidget(final int appWidgetId, long container, long screenId, - AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) { - if (appWidgetInfo == null) { - appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); - } + @Thunk void completeAddAppWidget(int appWidgetId, long container, long screenId, + AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) { - // Calculate the grid spans needed to fit this widget - CellLayout layout = getCellLayout(container, screenId); - - int[] minSpanXY = getMinSpanForWidget(this, appWidgetInfo); - int[] spanXY = getSpanForWidget(this, appWidgetInfo); - - // Try finding open space on Launcher screen - // We have saved the position to which the widget was dragged-- this really only matters - // if we are placing widgets on a "spring-loaded" screen - int[] cellXY = mTmpAddItemCellCoordinates; - int[] touchXY = mPendingAddInfo.dropPos; - int[] finalSpan = new int[2]; - boolean foundCellSpan = false; - if (mPendingAddInfo.cellX >= 0 && mPendingAddInfo.cellY >= 0) { - cellXY[0] = mPendingAddInfo.cellX; - cellXY[1] = mPendingAddInfo.cellY; - spanXY[0] = mPendingAddInfo.spanX; - spanXY[1] = mPendingAddInfo.spanY; - foundCellSpan = true; - } else if (touchXY != null) { - // when dragging and dropping, just find the closest free spot - int[] result = layout.findNearestVacantArea( - touchXY[0], touchXY[1], minSpanXY[0], minSpanXY[1], spanXY[0], - spanXY[1], cellXY, finalSpan); - spanXY[0] = finalSpan[0]; - spanXY[1] = finalSpan[1]; - foundCellSpan = (result != null); - } else { - foundCellSpan = layout.findCellForSpan(cellXY, minSpanXY[0], minSpanXY[1]); + ItemInfo info = mPendingAddInfo; + if (appWidgetInfo == null) { + appWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(this, + mAppWidgetManager.getAppWidgetInfo(appWidgetId)); } - if (!foundCellSpan) { - if (appWidgetId != -1) { - // Deleting an app widget ID is a void call but writes to disk before returning - // to the caller... - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - mAppWidgetHost.deleteAppWidgetId(appWidgetId); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - showOutOfSpaceMessage(isHotseatLayout(layout)); - return; + if (appWidgetInfo.isCustomWidget) { + appWidgetId = LauncherAppWidgetInfo.CUSTOM_WIDGET_ID; } - // Build Launcher-specific widget info and save to database - LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId, - appWidgetInfo.provider); - launcherInfo.spanX = spanXY[0]; - launcherInfo.spanY = spanXY[1]; - launcherInfo.minSpanX = mPendingAddInfo.minSpanX; - launcherInfo.minSpanY = mPendingAddInfo.minSpanY; + LauncherAppWidgetInfo launcherInfo; + launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider); + launcherInfo.spanX = info.spanX; + launcherInfo.spanY = info.spanY; + launcherInfo.minSpanX = info.minSpanX; + launcherInfo.minSpanY = info.minSpanY; launcherInfo.user = mAppWidgetManager.getUser(appWidgetInfo); LauncherModel.addItemToDatabase(this, launcherInfo, - container, screenId, cellXY[0], cellXY[1], false); + container, screenId, info.cellX, info.cellY); if (!mRestoring) { if (hostView == null) { // Perform actual inflation because we're live - launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); - launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); + launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, + appWidgetInfo); } else { // The AppWidgetHostView has already been inflated and instantiated launcherInfo.hostView = hostView; } - launcherInfo.hostView.setTag(launcherInfo); launcherInfo.hostView.setVisibility(View.VISIBLE); launcherInfo.notifyWidgetSizeChanged(this); - mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, cellXY[0], cellXY[1], - launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); + mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, info.cellX, + info.cellY, launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); } @@ -1679,28 +1627,26 @@ public class Launcher extends Activity if (Intent.ACTION_SCREEN_OFF.equals(action)) { mUserPresent = false; mDragLayer.clearAllResizeFrames(); - updateRunning(); + updateAutoAdvanceState(); // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop - if (mAppsCustomizeTabHost != null && mPendingAddInfo.container == ItemInfo.NO_ID) { + if (mAppsView != null && mWidgetsView != null && + mPendingAddInfo.container == ItemInfo.NO_ID) { showWorkspace(false); } } else if (Intent.ACTION_USER_PRESENT.equals(action)) { mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); } else if (ENABLE_DEBUG_INTENTS && DebugIntents.DELETE_DATABASE.equals(action)) { mModel.resetLoadedState(false, true); - mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE); } else if (ENABLE_DEBUG_INTENTS && DebugIntents.MIGRATE_DATABASE.equals(action)) { mModel.resetLoadedState(false, true); - mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); - } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) - || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { - getModel().forceReload(); } } }; @@ -1714,8 +1660,6 @@ public class Launcher extends Activity filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); // For handling managed profiles - filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); - filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); if (ENABLE_DEBUG_INTENTS) { filter.addAction(DebugIntents.DELETE_DATABASE); filter.addAction(DebugIntents.MIGRATE_DATABASE); @@ -1731,40 +1675,19 @@ public class Launcher extends Activity * Sets up transparent navigation and status bars in LMP. * This method is a no-op for other platform versions. */ - @TargetApi(19) + @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setupTransparentSystemBarsForLmp() { - // TODO(sansid): use the APIs directly when compiling against L sdk. - // Currently we use reflection to access the flags and the API to set the transparency - // on the System bars. if (Utilities.isLmpOrAbove()) { - try { - getWindow().getAttributes().systemUiVisibility |= - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - Field drawsSysBackgroundsField = WindowManager.LayoutParams.class.getField( - "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS"); - getWindow().addFlags(drawsSysBackgroundsField.getInt(null)); - - Method setStatusBarColorMethod = - Window.class.getDeclaredMethod("setStatusBarColor", int.class); - Method setNavigationBarColorMethod = - Window.class.getDeclaredMethod("setNavigationBarColor", int.class); - setStatusBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); - setNavigationBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); - } catch (NoSuchFieldException e) { - Log.w(TAG, "NoSuchFieldException while setting up transparent bars"); - } catch (NoSuchMethodException ex) { - Log.w(TAG, "NoSuchMethodException while setting up transparent bars"); - } catch (IllegalAccessException e) { - Log.w(TAG, "IllegalAccessException while setting up transparent bars"); - } catch (IllegalArgumentException e) { - Log.w(TAG, "IllegalArgumentException while setting up transparent bars"); - } catch (InvocationTargetException e) { - Log.w(TAG, "InvocationTargetException while setting up transparent bars"); - } finally {} + Window window = getWindow(); + window.getAttributes().systemUiVisibility |= + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); } } @@ -1777,17 +1700,16 @@ public class Launcher extends Activity unregisterReceiver(mReceiver); mAttached = false; } - updateRunning(); + updateAutoAdvanceState(); } public void onWindowVisibilityChanged(int visibility) { mVisible = visibility == View.VISIBLE; - updateRunning(); + updateAutoAdvanceState(); // The following code used to be in onResume, but it turns out onResume is called when // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged // is a more appropriate event to handle if (mVisible) { - mAppsCustomizeTabHost.onWindowVisible(); if (!mWorkspaceLoading) { final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); // We want to let Launcher draw itself at least once before we force it to build @@ -1822,14 +1744,14 @@ public class Launcher extends Activity } } - private void sendAdvanceMessage(long delay) { + @Thunk void sendAdvanceMessage(long delay) { mHandler.removeMessages(ADVANCE_MSG); Message msg = mHandler.obtainMessage(ADVANCE_MSG); mHandler.sendMessageDelayed(msg, delay); mAutoAdvanceSentTime = System.currentTimeMillis(); } - private void updateRunning() { + @Thunk void updateAutoAdvanceState() { boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); if (autoAdvanceRunning != mAutoAdvanceRunning) { mAutoAdvanceRunning = autoAdvanceRunning; @@ -1847,16 +1769,17 @@ public class Launcher extends Activity } } - private final Handler mHandler = new Handler() { + @Thunk final Handler mHandler = new Handler(new Handler.Callback() { + @Override - public void handleMessage(Message msg) { + public boolean handleMessage(Message msg) { if (msg.what == ADVANCE_MSG) { int i = 0; for (View key: mWidgetsToAdvance.keySet()) { final View v = key.findViewById(mWidgetsToAdvance.get(key).autoAdvanceViewId); final int delay = mAdvanceStagger * i; if (v instanceof Advanceable) { - postDelayed(new Runnable() { + mHandler.postDelayed(new Runnable() { public void run() { ((Advanceable) v).advance(); } @@ -1866,8 +1789,9 @@ public class Launcher extends Activity } sendAdvanceMessage(mAdvanceInterval); } + return true; } - }; + }); void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return; @@ -1875,14 +1799,14 @@ public class Launcher extends Activity if (v instanceof Advanceable) { mWidgetsToAdvance.put(hostView, appWidgetInfo); ((Advanceable) v).fyiWillBeAdvancedByHostKThx(); - updateRunning(); + updateAutoAdvanceState(); } } void removeWidgetToAutoAdvance(View hostView) { if (mWidgetsToAdvance.containsKey(hostView)) { mWidgetsToAdvance.remove(hostView); - updateRunning(); + updateAutoAdvanceState(); } } @@ -1891,7 +1815,7 @@ public class Launcher extends Activity launcherInfo.hostView = null; } - void showOutOfSpaceMessage(boolean isHotseatLayout) { + public void showOutOfSpaceMessage(boolean isHotseatLayout) { int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); } @@ -1900,6 +1824,14 @@ public class Launcher extends Activity return mDragLayer; } + public AllAppsContainerView getAppsView() { + return mAppsView; + } + + public WidgetsContainerView getWidgetsView() { + return mWidgetsView; + } + public Workspace getWorkspace() { return mWorkspace; } @@ -1928,6 +1860,10 @@ public class Launcher extends Activity return mSharedPrefs; } + public DeviceProfile getDeviceProfile() { + return mDeviceProfile; + } + public void closeSystemDialogs() { getWindow().closeAllPanels(); @@ -1985,9 +1921,14 @@ public class Launcher extends Activity imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } - // Reset the apps customize page - if (!alreadyOnHome && mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.reset(); + // Reset the apps view + if (!alreadyOnHome && mAppsView != null) { + mAppsView.scrollToTop(); + } + + // Reset the widgets view + if (!alreadyOnHome && mWidgetsView != null) { + mWidgetsView.scrollToTop(); } if (mLauncherCallbacks != null) { @@ -2037,21 +1978,8 @@ public class Launcher extends Activity outState.putInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID, mPendingAddWidgetId); } - if (mFolderInfo != null && mWaitingForResult) { - outState.putBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, true); - outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id); - } - - // Save the current AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - AppsCustomizePagedView.ContentType type = mAppsCustomizeContent.getContentType(); - String currentTabTag = mAppsCustomizeTabHost.getTabTagForContentType(type); - if (currentTabTag != null) { - outState.putString("apps_customize_currentTab", currentTabTag); - } - int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex(); - outState.putInt("apps_customize_currentIndex", currentIndex); - } + // Save the current widgets tray? + // TODO(hyunyoungs) outState.putSerializable(RUNTIME_STATE_VIEW_IDS, mItemIdToViewId); if (mLauncherCallbacks != null) { @@ -2089,13 +2017,6 @@ public class Launcher extends Activity TextKeyListener.getInstance().release(); - // Disconnect any of the callbacks and drawables associated with ItemInfos on the workspace - // to prevent leaking Launcher activities on orientation change. - if (mModel != null) { - mModel.unbindItemInfosAndClearQueuedBindRunnables(); - } - - getContentResolver().unregisterContentObserver(mWidgetObserver); unregisterReceiver(mCloseSystemDialogsReceiver); mDragLayer.clearAllResizeFrames(); @@ -2117,10 +2038,26 @@ public class Launcher extends Activity @Override public void startActivityForResult(Intent intent, int requestCode) { + onStartForResult(requestCode); + super.startActivityForResult(intent, requestCode); + } + + @Override + public void startIntentSenderForResult (IntentSender intent, int requestCode, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { + onStartForResult(requestCode); + try { + super.startIntentSenderForResult(intent, requestCode, + fillInIntent, flagsMask, flagsValues, extraFlags, options); + } catch (IntentSender.SendIntentException e) { + throw new ActivityNotFoundException(); + } + } + + private void onStartForResult(int requestCode) { if (requestCode >= 0) { setWaitingForResult(true); } - super.startActivityForResult(intent, requestCode); } /** @@ -2131,8 +2068,6 @@ public class Launcher extends Activity public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { - showWorkspace(true); - if (initialQuery == null) { // Use any text typed in the launcher as the initial query initialQuery = getTypedText(); @@ -2151,6 +2086,9 @@ public class Launcher extends Activity if (clearTextImmediately) { clearTypedText(); } + + // We need to show the workspace after starting the search + showWorkspace(true); } /** @@ -2284,14 +2222,14 @@ public class Launcher extends Activity mPendingAddInfo.dropPos = null; } - void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, - final AppWidgetHostView boundWidget, final AppWidgetProviderInfo appWidgetInfo) { + void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, final + AppWidgetHostView boundWidget, final LauncherAppWidgetProviderInfo appWidgetInfo) { addAppWidgetImpl(appWidgetId, info, boundWidget, appWidgetInfo, 0); } void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, - final AppWidgetHostView boundWidget, final AppWidgetProviderInfo appWidgetInfo, int - delay) { + final AppWidgetHostView boundWidget, final LauncherAppWidgetProviderInfo appWidgetInfo, + int delay) { if (appWidgetInfo.configure != null) { mPendingAddWidgetInfo = appWidgetInfo; mPendingAddWidgetId = appWidgetId; @@ -2321,20 +2259,39 @@ public class Launcher extends Activity closeFolder(); mWorkspace.moveToCustomContentScreen(animate); } + + public void addPendingItem(PendingAddItemInfo info, long container, long screenId, + int[] cell, int spanX, int spanY) { + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + int span[] = new int[2]; + span[0] = spanX; + span[1] = spanY; + addAppWidgetFromDrop((PendingAddWidgetInfo) info, + container, screenId, cell, span); + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + processShortcutFromDrop(info.componentName, container, screenId, cell); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); + } + } + /** * Process a shortcut drop. * * @param componentName The name of the component * @param screenId The ID of the screen where it should be added * @param cell The cell it should be added to, optional - * @param position The location on the screen where it was dropped, optional */ - void processShortcutFromDrop(ComponentName componentName, long container, long screenId, - int[] cell, int[] loc) { + private void processShortcutFromDrop(ComponentName componentName, long container, long screenId, + int[] cell) { resetAddInfo(); mPendingAddInfo.container = container; mPendingAddInfo.screenId = screenId; - mPendingAddInfo.dropPos = loc; + mPendingAddInfo.dropPos = null; if (cell != null) { mPendingAddInfo.cellX = cell[0]; @@ -2343,7 +2300,7 @@ public class Launcher extends Activity Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); createShortcutIntent.setComponent(componentName); - processShortcut(createShortcutIntent); + Utilities.startActivityForResultSafely(this, createShortcutIntent, REQUEST_CREATE_SHORTCUT); } /** @@ -2352,14 +2309,13 @@ public class Launcher extends Activity * @param info The PendingAppWidgetInfo of the widget being added. * @param screenId The ID of the screen where it should be added * @param cell The cell it should be added to, optional - * @param position The location on the screen where it was dropped, optional */ - void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, long screenId, - int[] cell, int[] span, int[] loc) { + private void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, long screenId, + int[] cell, int[] span) { resetAddInfo(); mPendingAddInfo.container = info.container = container; mPendingAddInfo.screenId = info.screenId = screenId; - mPendingAddInfo.dropPos = loc; + mPendingAddInfo.dropPos = null; mPendingAddInfo.minSpanX = info.minSpanX; mPendingAddInfo.minSpanY = info.minSpanY; @@ -2377,6 +2333,9 @@ public class Launcher extends Activity if (hostView != null) { appWidgetId = hostView.getAppWidgetId(); addAppWidgetImpl(appWidgetId, info, hostView, info.info); + + // Clear the boundWidget so that it doesn't get destroyed. + info.boundWidget = null; } else { // In this case, we either need to start an activity to get permission to bind // the widget, or we need to start an activity to configure the widget, or both. @@ -2401,22 +2360,14 @@ public class Launcher extends Activity } } - void processShortcut(Intent intent) { - Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_SHORTCUT); - } - - void processWallpaper(Intent intent) { - startActivityForResult(intent, REQUEST_PICK_WALLPAPER); - } - FolderIcon addFolder(CellLayout layout, long container, final long screenId, int cellX, int cellY) { final FolderInfo folderInfo = new FolderInfo(); folderInfo.title = getText(R.string.folder_name); // Update the model - LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, cellX, cellY, - false); + LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, + cellX, cellY); sFolders.put(folderInfo.id, folderInfo); // Create the view @@ -2434,23 +2385,6 @@ public class Launcher extends Activity sFolders.remove(folder.id); } - protected ComponentName getWallpaperPickerComponent() { - if (mLauncherCallbacks != null) { - return mLauncherCallbacks.getWallpaperPickerComponent(); - } - return new ComponentName(getPackageName(), LauncherWallpaperPickerActivity.class.getName()); - } - - /** - * Registers various content observers. The current implementation registers - * only a favorites observer to keep track of the favorites applications. - */ - private void registerContentObservers() { - ContentResolver resolver = getContentResolver(); - resolver.registerContentObserver(LauncherProvider.CONTENT_APPWIDGET_RESET_URI, - true, mWidgetObserver); - } - @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -2480,15 +2414,17 @@ public class Launcher extends Activity return; } - if (isAllAppsVisible()) { - if (mAppsCustomizeContent.getContentType() == - AppsCustomizePagedView.ContentType.Applications) { - showWorkspace(true); - } else { - showOverviewMode(true); - } + if (mDragController.isDragging()) { + mDragController.cancelDrag(); + return; + } + + if (isAppsViewVisible()) { + showWorkspace(true); + } else if (isWidgetsViewVisible()) { + showOverviewMode(true); } else if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(true); + showWorkspace(true); } else if (mWorkspace.getOpenFolder() != null) { Folder openFolder = mWorkspace.getOpenFolder(); if (openFolder.isEditingName()) { @@ -2505,9 +2441,10 @@ public class Launcher extends Activity } /** - * Re-listen when widgets are reset. + * Re-listen when widget host is reset. */ - private void onAppWidgetReset() { + @Override + public void onAppWidgetHostReset() { if (mAppWidgetHost != null) { mAppWidgetHost.startListening(); } @@ -2531,14 +2468,14 @@ public class Launcher extends Activity if (v instanceof Workspace) { if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(true); + showWorkspace(true); } return; } if (v instanceof CellLayout) { if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(mWorkspace.indexOfChild(v), true); + showWorkspace(mWorkspace.indexOfChild(v), true); } } @@ -2560,13 +2497,7 @@ 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; } @@ -2585,7 +2516,8 @@ public class Launcher extends Activity int widgetId = info.appWidgetId; AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId); if (appWidgetInfo != null) { - mPendingAddWidgetInfo = appWidgetInfo; + mPendingAddWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo( + this, appWidgetInfo); mPendingAddInfo.copyFrom(info); mPendingAddWidgetId = widgetId; @@ -2616,13 +2548,17 @@ public class Launcher extends Activity */ protected void onClickAllAppsButton(View v) { if (LOGD) Log.d(TAG, "onClickAllAppsButton"); - if (isAllAppsVisible()) { - showWorkspace(true); - } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false); + if (!isAppsViewVisible()) { + showAppsView(true /* animated */, false /* resetListToTop */, + true /* updatePredictedApps */, false /* focusSearchBar */); } - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onClickAllAppsButton(v); + } + + protected void onLongClickAllAppsButton(View v) { + if (LOGD) Log.d(TAG, "onLongClickAllAppsButton"); + if (!isAppsViewVisible()) { + showAppsView(true /* animated */, false /* resetListToTop */, + true /* updatePredictedApps */, true /* focusSearchBar */); } } @@ -2704,7 +2640,7 @@ public class Launcher extends Activity } } - private void startAppShortcutOrInfoActivity(View v) { + @Thunk void startAppShortcutOrInfoActivity(View v) { Object tag = v.getTag(); final ShortcutInfo shortcut; final Intent intent; @@ -2724,7 +2660,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; @@ -2790,7 +2726,7 @@ public class Launcher extends Activity if (mIsSafeModeEnabled) { Toast.makeText(this, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true); + showWidgetsView(true /* animated */, true /* resetPageToZero */); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickAddWidgetButton(view); } @@ -2803,9 +2739,8 @@ public class Launcher extends Activity */ protected void onClickWallpaperPicker(View v) { if (LOGD) Log.d(TAG, "onClickWallpaperPicker"); - final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); - pickWallpaper.setComponent(getWallpaperPickerComponent()); - startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER); + startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()), + REQUEST_PICK_WALLPAPER); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickWallpaperPicker(v); @@ -2820,22 +2755,15 @@ public class Launcher extends Activity if (LOGD) Log.d(TAG, "onClickSettingsButton"); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickSettingsButton(v); + } else { + startActivity(new Intent(this, SettingsActivity.class)); } } - public void onTouchDownAllAppsButton(View v) { - // Provide the same haptic feedback that the system offers for virtual keys. - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - } - - public void performHapticFeedbackOnTouchDown(View v) { - // Provide the same haptic feedback that the system offers for virtual keys. - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - } - public View.OnTouchListener getHapticFeedbackTouchListener() { if (mHapticFeedbackTouchListener == null) { mHapticFeedbackTouchListener = new View.OnTouchListener() { + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { @@ -2885,6 +2813,19 @@ public class Launcher extends Activity } } + /** Updates the interaction state. */ + public void updateInteraction(Workspace.State fromState, Workspace.State toState) { + // Only update the interacting state if we are transitioning to/from a view with an + // overlay + boolean fromStateWithOverlay = fromState != Workspace.State.NORMAL; + boolean toStateWithOverlay = toState != Workspace.State.NORMAL; + if (toStateWithOverlay) { + onInteractionBegin(); + } else if (fromStateWithOverlay) { + onInteractionEnd(); + } + } + void startApplicationDetailsActivity(ComponentName componentName, UserHandleCompat user) { try { LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); @@ -2922,7 +2863,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 @@ -2940,10 +2881,45 @@ public class Launcher extends Activity Bundle optsBundle = null; if (useLaunchAnimation) { - ActivityOptions opts = Utilities.isLmpOrAbove() ? - ActivityOptions.makeCustomAnimation(this, R.anim.task_open_enter, R.anim.no_anim) : - ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); - optsBundle = opts.toBundle(); + ActivityOptions opts = null; + if (sClipRevealMethod != null) { + // TODO: call method directly when Launcher3 can depend on M APIs + int left = 0, top = 0; + int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); + if (v instanceof TextView) { + // Launch from center of icon, not entire view + Drawable icon = Workspace.getTextViewIcon((TextView) v); + if (icon != null) { + Rect bounds = icon.getBounds(); + left = (width - bounds.width()) / 2; + top = v.getPaddingTop(); + width = bounds.width(); + height = bounds.height(); + } + } + try { + opts = (ActivityOptions) sClipRevealMethod.invoke(null, v, + left, top, width, height); + } catch (IllegalAccessException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } catch (InvocationTargetException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } + } + if (opts == null && !Utilities.isLmpOrAbove()) { + // Below L, we use a scale up animation + opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + } else if (opts == null && Utilities.isLmpMR1()) { + // On L devices, we use the device default slide-up transition. + // On L MR1 devices, we a custom version of the slide-up transition which + // doesn't have the delay present in the device default. + opts = ActivityOptions.makeCustomAnimation(this, + R.anim.task_open_enter, R.anim.no_anim); + } + optsBundle = opts != null ? opts.toBundle() : null; } if (user == null || user.equals(UserHandleCompat.myUserHandle())) { @@ -2965,7 +2941,7 @@ public class Launcher extends Activity return false; } - boolean startActivitySafely(View v, Intent intent, Object tag) { + @Thunk 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(); @@ -3095,10 +3071,19 @@ public class Launcher extends Activity */ public void openFolder(FolderIcon folderIcon) { Folder folder = folderIcon.getFolder(); + Folder openFolder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; + if (openFolder != null && openFolder != folder) { + // Close any open folder before opening a folder. + closeFolder(); + } + FolderInfo info = folder.mInfo; info.opened = true; + // While the folder is open, the position of the icon cannot change. + ((CellLayout.LayoutParams) folderIcon.getLayoutParams()).canReorder = false; + // Just verify that the folder hasn't already been added to the DragLayer. // There was a one-off crash where the folder had a parent already. if (folder.getParent() == null) { @@ -3127,13 +3112,16 @@ public class Launcher extends Activity } } - void closeFolder(Folder folder) { + public void closeFolder(Folder folder) { folder.getInfo().opened = false; ViewGroup parent = (ViewGroup) folder.getParent().getParent(); if (parent != null) { FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); shrinkAndFadeInFolderIcon(fi); + if (fi != null) { + ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true; + } } folder.animateClosed(); @@ -3147,9 +3135,15 @@ public class Launcher extends Activity if (isWorkspaceLocked()) return false; if (mState != State.WORKSPACE) return false; + if (v == mAllAppsButton) { + onLongClickAllAppsButton(v); + return true; + } + if (v instanceof Workspace) { if (!mWorkspace.isInOverviewMode()) { - if (mWorkspace.enterOverviewMode()) { + if (!mWorkspace.isTouchActive()) { + showOverviewMode(true); mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); return true; @@ -3165,7 +3159,7 @@ public class Launcher extends Activity View itemUnderLongClick = null; if (v.getTag() instanceof ItemInfo) { ItemInfo info = (ItemInfo) v.getTag(); - longClickCellInfo = new CellLayout.CellInfo(v, info);; + longClickCellInfo = new CellLayout.CellInfo(v, info); itemUnderLongClick = longClickCellInfo.cell; resetAddInfo(); } @@ -3173,8 +3167,7 @@ public class Launcher extends Activity // The hotseat touch handling does not go through Workspace, and we always allow long press // on hotseat items. final boolean inHotseat = isHotseatLayout(v); - boolean allowLongPress = inHotseat || mWorkspace.allowLongPress(); - if (allowLongPress && !mDragController.isDragging()) { + if (!mDragController.isDragging()) { if (itemUnderLongClick == null) { // User long pressed on empty space mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, @@ -3182,7 +3175,7 @@ public class Launcher extends Activity if (mWorkspace.isInOverviewMode()) { mWorkspace.startReordering(v); } else { - mWorkspace.enterOverviewMode(); + showOverviewMode(true); } } else { final boolean isAllAppsButton = inHotseat && isAllAppsButtonRank( @@ -3206,7 +3199,7 @@ public class Launcher extends Activity /** * Returns the CellLayout of the specified container at the specified screen. */ - CellLayout getCellLayout(long container, long screenId) { + public CellLayout getCellLayout(long container, long screenId) { if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { if (mHotseat != null) { return mHotseat.getLayout(); @@ -3214,17 +3207,36 @@ public class Launcher extends Activity return null; } } else { - return (CellLayout) mWorkspace.getScreenWithId(screenId); + return mWorkspace.getScreenWithId(screenId); } } + /** + * For overridden classes. + */ public boolean isAllAppsVisible() { - return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); + return isAppsViewVisible(); + } + + public boolean isAppsViewVisible() { + return (mState == State.APPS) || (mOnResumeState == State.APPS); + } + + public boolean isWidgetsViewVisible() { + return (mState == State.WIDGETS) || (mOnResumeState == State.WIDGETS); } - private void setWorkspaceBackground(boolean workspace) { - mLauncherView.setBackground(workspace ? - mWorkspaceBackgroundDrawable : null); + private void setWorkspaceBackground(int background) { + switch (background) { + case WORKSPACE_BACKGROUND_TRANSPARENT: + getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + break; + case WORKSPACE_BACKGROUND_BLACK: + getWindow().setBackgroundDrawable(null); + break; + default: + getWindow().setBackgroundDrawable(mWorkspaceBackgroundDrawable); + } } protected void changeWallpaperVisiblity(boolean visible) { @@ -3234,577 +3246,7 @@ public class Launcher extends Activity if (wpflags != curflags) { getWindow().setFlags(wpflags, WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); } - setWorkspaceBackground(visible); - } - - private void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionPrepare(this, animated, toWorkspace); - } - } - - private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 0f); - } - - private void dispatchOnLauncherTransitionStep(View v, float t) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStep(this, t); - } - } - - private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 1f); - } - - /** - * Things to test when changing the following seven functions. - * - Home from workspace - * - from center screen - * - from other screens - * - Home from all apps - * - from center screen - * - from other screens - * - Back from all apps - * - from center screen - * - from other screens - * - Launch app from workspace and quit - * - with back - * - with home - * - Launch app from all apps and quit - * - with back - * - with home - * - Go to a screen that's not the default, then all - * apps, and launch and app, and go back - * - with back - * -with home - * - On workspace, long press power and go back - * - with back - * - with home - * - On all apps, long press power and go back - * - with back - * - with home - * - On workspace, power off - * - On all apps, power off - * - Launch an app and turn off the screen while in that app - * - Go back with home key - * - Go back with back key TODO: make this not go to workspace - * - From all apps - * - From workspace - * - Enter and exit car mode (becuase it causes an extra configuration changed) - * - From all apps - * - From the center workspace - * - From another workspace - */ - - /** - * Zoom the camera out from the workspace to reveal 'toView'. - * Assumes that the view to show is anchored at either the very top or very bottom - * of the screen. - */ - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) { - AppsCustomizePagedView.ContentType contentType = mAppsCustomizeContent.getContentType(); - showAppsCustomizeHelper(animated, springLoaded, contentType); - } - - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded, - final AppsCustomizePagedView.ContentType contentType) { - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - - final Resources res = getResources(); - - final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); - final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); - final View fromView = mWorkspace; - final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; - - final ArrayList<View> layerViews = new ArrayList<View>(); - - Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ? - Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN; - Animator workspaceAnim = - mWorkspace.getChangeStateAnimation(workspaceState, animated, layerViews); - if (!LauncherAppState.isDisableAllApps() - || contentType == AppsCustomizePagedView.ContentType.Widgets) { - // Set the content type for the all apps/widgets space - mAppsCustomizeTabHost.setContentTypeImmediate(contentType); - } - - // If for some reason our views aren't initialized, don't animate - boolean initialized = getAllAppsButton() != null; - - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - final AppsCustomizePagedView content = (AppsCustomizePagedView) - toView.findViewById(R.id.apps_customize_pane_content); - - final View page = content.getPageAt(content.getCurrentPage()); - final View revealView = toView.findViewById(R.id.fake_page); - - final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets; - if (isWidgetTray) { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); - } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); - } - - // Hide the real page background, and swap in the fake one - content.setPageBackgroundsVisible(false); - revealView.setVisibility(View.VISIBLE); - // We need to hide this view as the animation start will be posted. - revealView.setAlpha(0); - - int width = revealView.getMeasuredWidth(); - int height = revealView.getMeasuredHeight(); - float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); - - revealView.setTranslationY(0); - revealView.setTranslationX(0); - - // Get the y delta between the center of the page and the center of the all apps button - int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, - getAllAppsButton(), null); - - float alpha = 0; - float xDrift = 0; - float yDrift = 0; - if (material) { - alpha = isWidgetTray ? 0.3f : 1f; - yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; - xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; - } else { - yDrift = 2 * height / 3; - xDrift = 0; - } - final float initAlpha = alpha; - - revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - layerViews.add(revealView); - PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat("alpha", initAlpha, 1f); - PropertyValuesHolder panelDriftY = - PropertyValuesHolder.ofFloat("translationY", yDrift, 0); - PropertyValuesHolder panelDriftX = - PropertyValuesHolder.ofFloat("translationX", xDrift, 0); - - ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, - panelAlpha, panelDriftY, panelDriftX); - - panelAlphaAndDrift.setDuration(revealDuration); - panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); - - mStateAnimation.play(panelAlphaAndDrift); - - if (page != null) { - page.setVisibility(View.VISIBLE); - page.setLayerType(View.LAYER_TYPE_HARDWARE, null); - layerViews.add(page); - - ObjectAnimator pageDrift = ObjectAnimator.ofFloat(page, "translationY", yDrift, 0); - page.setTranslationY(yDrift); - pageDrift.setDuration(revealDuration); - pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); - pageDrift.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(pageDrift); - - page.setAlpha(0f); - ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(page, "alpha", 0f, 1f); - itemsAlpha.setDuration(revealDuration); - itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - itemsAlpha.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(itemsAlpha); - } - - View pageIndicators = toView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(0.01f); - ObjectAnimator indicatorsAlpha = - ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f); - indicatorsAlpha.setDuration(revealDuration); - mStateAnimation.play(indicatorsAlpha); - - if (material) { - final View allApps = getAllAppsButton(); - int allAppsButtonSize = LauncherAppState.getInstance(). - getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; - float startRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; - Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2, - height / 2, startRadius, revealRadius); - reveal.setDuration(revealDuration); - reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - - reveal.addListener(new AnimatorListenerAdapter() { - public void onAnimationStart(Animator animation) { - if (!isWidgetTray) { - allApps.setVisibility(View.INVISIBLE); - } - } - public void onAnimationEnd(Animator animation) { - if (!isWidgetTray) { - allApps.setVisibility(View.VISIBLE); - } - } - }); - mStateAnimation.play(reveal); - } - - mStateAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - - revealView.setVisibility(View.INVISIBLE); - revealView.setLayerType(View.LAYER_TYPE_NONE, null); - if (page != null) { - page.setLayerType(View.LAYER_TYPE_NONE, null); - } - content.setPageBackgroundsVisible(true); - - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - - }); - - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - final AnimatorSet stateAnimation = mStateAnimation; - final Runnable startAnimRunnable = new Runnable() { - public void run() { - // Check that mStateAnimation hasn't changed while - // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) - return; - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - - revealView.setAlpha(initAlpha); - if (Utilities.isLmpOrAbove()) { - for (int i = 0; i < layerViews.size(); i++) { - View v = layerViews.get(i); - if (v != null) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - } - mStateAnimation.start(); - } - }; - toView.bringToFront(); - toView.setVisibility(View.VISIBLE); - toView.post(startAnimRunnable); - } else { - toView.setTranslationX(0.0f); - toView.setTranslationY(0.0f); - toView.setScaleX(1.0f); - toView.setScaleY(1.0f); - toView.setVisibility(View.VISIBLE); - toView.bringToFront(); - - if (!springLoaded && !LauncherAppState.getInstance().isScreenLarge()) { - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - } - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - } - } - - /** - * Zoom the camera back into the workspace, hiding 'fromView'. - * This is the opposite of showAppsCustomizeHelper. - * @param animated If true, the transition will be animated. - */ - private void hideAppsCustomizeHelper(Workspace.State toState, final boolean animated, - final boolean springLoaded, final Runnable onCompleteRunnable) { - - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - Resources res = getResources(); - - final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); - final int fadeOutDuration = res.getInteger(R.integer.config_appsCustomizeFadeOutTime); - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeConcealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final float scaleFactor = (float) - res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); - final View fromView = mAppsCustomizeTabHost; - final View toView = mWorkspace; - Animator workspaceAnim = null; - final ArrayList<View> layerViews = new ArrayList<View>(); - - if (toState == Workspace.State.NORMAL) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, layerViews); - } else if (toState == Workspace.State.SPRING_LOADED || - toState == Workspace.State.OVERVIEW) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, layerViews); - } - - // If for some reason our views aren't initialized, don't animate - boolean initialized = getAllAppsButton() != null; - - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - - final AppsCustomizePagedView content = (AppsCustomizePagedView) - fromView.findViewById(R.id.apps_customize_pane_content); - - final View page = content.getPageAt(content.getNextPage()); - - // We need to hide side pages of the Apps / Widget tray to avoid some ugly edge cases - int count = content.getChildCount(); - for (int i = 0; i < count; i++) { - View child = content.getChildAt(i); - if (child != page) { - child.setVisibility(View.INVISIBLE); - } - } - final View revealView = fromView.findViewById(R.id.fake_page); - - // hideAppsCustomizeHelper is called in some cases when it is already hidden - // don't perform all these no-op animations. In particularly, this was causing - // the all-apps button to pop in and out. - if (fromView.getVisibility() == View.VISIBLE) { - AppsCustomizePagedView.ContentType contentType = content.getContentType(); - final boolean isWidgetTray = - contentType == AppsCustomizePagedView.ContentType.Widgets; - - if (isWidgetTray) { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); - } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); - } - - int width = revealView.getMeasuredWidth(); - int height = revealView.getMeasuredHeight(); - float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); - - // Hide the real page background, and swap in the fake one - revealView.setVisibility(View.VISIBLE); - content.setPageBackgroundsVisible(false); - - final View allAppsButton = getAllAppsButton(); - revealView.setTranslationY(0); - int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, - allAppsButton, null); - - float xDrift = 0; - float yDrift = 0; - if (material) { - yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; - xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; - } else { - yDrift = 2 * height / 3; - xDrift = 0; - } - - revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - TimeInterpolator decelerateInterpolator = material ? - new LogDecelerateInterpolator(100, 0) : - new DecelerateInterpolator(1f); - - // The vertical motion of the apps panel should be delayed by one frame - // from the conceal animation in order to give the right feel. We correpsondingly - // shorten the duration so that the slide and conceal end at the same time. - ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY", - 0, yDrift); - panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); - panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - panelDriftY.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftY); - - ObjectAnimator panelDriftX = LauncherAnimUtils.ofFloat(revealView, "translationX", - 0, xDrift); - panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); - panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - panelDriftX.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftX); - - if (isWidgetTray || !material) { - float finalAlpha = material ? 0.4f : 0f; - revealView.setAlpha(1f); - ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha", - 1f, finalAlpha); - panelAlpha.setDuration(material ? revealDuration : 150); - panelAlpha.setInterpolator(decelerateInterpolator); - panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(panelAlpha); - } - - if (page != null) { - page.setLayerType(View.LAYER_TYPE_HARDWARE, null); - - ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(page, "translationY", - 0, yDrift); - page.setTranslationY(0); - pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); - pageDrift.setInterpolator(decelerateInterpolator); - pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(pageDrift); - - page.setAlpha(1f); - ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(page, "alpha", 1f, 0f); - itemsAlpha.setDuration(100); - itemsAlpha.setInterpolator(decelerateInterpolator); - mStateAnimation.play(itemsAlpha); - } - - View pageIndicators = fromView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(1f); - ObjectAnimator indicatorsAlpha = - LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f); - indicatorsAlpha.setDuration(revealDuration); - indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); - mStateAnimation.play(indicatorsAlpha); - - width = revealView.getMeasuredWidth(); - - if (material) { - if (!isWidgetTray) { - allAppsButton.setVisibility(View.INVISIBLE); - } - int allAppsButtonSize = LauncherAppState.getInstance(). - getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; - float finalRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; - Animator reveal = - LauncherAnimUtils.createCircularReveal(revealView, width / 2, - height / 2, revealRadius, finalRadius); - reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - reveal.setDuration(revealDuration); - reveal.setStartDelay(itemsAlphaStagger); - - reveal.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - revealView.setVisibility(View.INVISIBLE); - if (!isWidgetTray) { - allAppsButton.setVisibility(View.VISIBLE); - } - } - }); - - mStateAnimation.play(reveal); - } - - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - mAppsCustomizeContent.stopScrolling(); - } - - mStateAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - - revealView.setLayerType(View.LAYER_TYPE_NONE, null); - if (page != null) { - page.setLayerType(View.LAYER_TYPE_NONE, null); - } - content.setPageBackgroundsVisible(true); - // Unhide side pages - int count = content.getChildCount(); - for (int i = 0; i < count; i++) { - View child = content.getChildAt(i); - child.setVisibility(View.VISIBLE); - } - - // Reset page transforms - if (page != null) { - page.setTranslationX(0); - page.setTranslationY(0); - page.setAlpha(1); - } - content.setCurrentPage(content.getNextPage()); - - mAppsCustomizeContent.updateCurrentPageScroll(); - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - }); - - final AnimatorSet stateAnimation = mStateAnimation; - final Runnable startAnimRunnable = new Runnable() { - public void run() { - // Check that mStateAnimation hasn't changed while - // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) - return; - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - - if (Utilities.isLmpOrAbove()) { - for (int i = 0; i < layerViews.size(); i++) { - View v = layerViews.get(i); - if (v != null) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - } - mStateAnimation.start(); - } - }; - fromView.post(startAnimRunnable); - } else { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionStart(fromView, animated, true); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - dispatchOnLauncherTransitionStart(toView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - } + setWorkspaceBackground(visible ? WORKSPACE_BACKGROUND_GRADIENT : WORKSPACE_BACKGROUND_BLACK); } @Override @@ -3816,25 +3258,42 @@ public class Launcher extends Activity SQLiteDatabase.releaseMemory(); // This clears all widget bitmaps from the widget tray - if (mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.trimMemory(); - } + // TODO(hyunyoungs) + } + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onTrimMemory(level); } } - protected void showWorkspace(boolean animated) { - showWorkspace(animated, null); + @Override + public void onStateTransitionHideSearchBar() { + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false /* animated */); + } } - protected void showWorkspace() { - showWorkspace(true); + public void showWorkspace(boolean animated) { + showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null); + } + + public void showWorkspace(boolean animated, Runnable onCompleteRunnable) { + showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, + onCompleteRunnable); + } + + protected void showWorkspace(int snapToPage, boolean animated) { + showWorkspace(snapToPage, animated, null); } - void showWorkspace(boolean animated, Runnable onCompleteRunnable) { - if (mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL) { + void showWorkspace(int snapToPage, boolean animated, Runnable onCompleteRunnable) { + boolean changed = mState != State.WORKSPACE || + mWorkspace.getState() != Workspace.State.NORMAL; + if (changed) { boolean wasInSpringLoadedMode = (mState != State.WORKSPACE); mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.NORMAL, animated, false, onCompleteRunnable); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.NORMAL, + snapToPage, animated, onCompleteRunnable); // Show the search bar (only animate if we were showing the drop target bar in spring // loaded mode) @@ -3853,73 +3312,131 @@ public class Launcher extends Activity // Resume the auto-advance of widgets mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); - // Send an accessibility event to announce the context change - getWindow().getDecorView() - .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - - onWorkspaceShown(animated); + if (changed) { + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } } void showOverviewMode(boolean animated) { mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.OVERVIEW, animated, false, null); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.OVERVIEW, + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, + null /* onCompleteRunnable */); mState = State.WORKSPACE; - onWorkspaceShown(animated); } - public void onWorkspaceShown(boolean animated) { + /** + * Shows the apps view. + */ + void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps, + boolean focusSearchBar) { + if (resetListToTop) { + mAppsView.scrollToTop(); + } + if (updatePredictedApps) { + tryAndUpdatePredictedApps(); + } + showAppsOrWidgets(State.APPS, animated, focusSearchBar); } - void showAllApps(boolean animated, AppsCustomizePagedView.ContentType contentType, - boolean resetPageToZero) { - if (mState != State.WORKSPACE) return; - + /** + * Shows the widgets view. + */ + void showWidgetsView(boolean animated, boolean resetPageToZero) { + if (LOGD) Log.d(TAG, "showWidgetsView:" + animated + " resetPageToZero:" + resetPageToZero); if (resetPageToZero) { - mAppsCustomizeTabHost.reset(); + mWidgetsView.scrollToTop(); } - showAppsCustomizeHelper(animated, false, contentType); - mAppsCustomizeTabHost.post(new Runnable() { + showAppsOrWidgets(State.WIDGETS, animated, false); + + mWidgetsView.post(new Runnable() { @Override public void run() { - // We post this in-case the all apps view isn't yet constructed. - mAppsCustomizeTabHost.requestFocus(); + mWidgetsView.requestFocus(); } }); + } + + /** + * Sets up the transition to show the apps/widgets view. + * + * @return whether the current from and to state allowed this operation + */ + // TODO: calling method should use the return value so that when {@code false} is returned + // the workspace transition doesn't fall into invalid state. + private boolean showAppsOrWidgets(State toState, boolean animated, boolean focusSearchBar) { + if (mState != State.WORKSPACE && mState != State.APPS_SPRING_LOADED && + mState != State.WIDGETS_SPRING_LOADED) { + return false; + } + if (toState != State.APPS && toState != State.WIDGETS) { + return false; + } + + if (toState == State.APPS) { + mStateTransitionAnimation.startAnimationToAllApps(animated, focusSearchBar); + } else { + mStateTransitionAnimation.startAnimationToWidgets(animated); + } // Change the state *after* we've called all the transition code - mState = State.APPS_CUSTOMIZE; + mState = toState; // Pause the auto-advance of widgets until we are out of AllApps mUserPresent = false; - updateRunning(); + updateAutoAdvanceState(); closeFolder(); // Send an accessibility event to announce the context change getWindow().getDecorView() .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + return true; } - void enterSpringLoadedDragMode() { - if (isAllAppsVisible()) { - hideAppsCustomizeHelper(Workspace.State.SPRING_LOADED, true, true, null); - mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + /** + * Updates the workspace and interaction state on state change, and return the animation to this + * new state. + */ + public Animator startWorkspaceStateChangeAnimation(Workspace.State toState, int toPage, + boolean animated, boolean hasOverlaySearchBar, HashMap<View, Integer> layerViews) { + Workspace.State fromState = mWorkspace.getState(); + Animator anim = mWorkspace.setStateWithAnimation(toState, toPage, animated, + hasOverlaySearchBar, layerViews); + updateInteraction(fromState, toState); + return anim; + } + + public void enterSpringLoadedDragMode() { + if (LOGD) Log.d(TAG, String.format("enterSpringLoadedDragMode [mState=%s", mState.name())); + if (mState == State.WORKSPACE || mState == State.APPS_SPRING_LOADED || + mState == State.WIDGETS_SPRING_LOADED) { + return; } + + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.SPRING_LOADED, + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, true /* animated */, + null /* onCompleteRunnable */); + mState = isAppsViewVisible() ? State.APPS_SPRING_LOADED : State.WIDGETS_SPRING_LOADED; } - void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, + public void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, final Runnable onCompleteRunnable) { - if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; + if (mState != State.APPS_SPRING_LOADED && mState != State.WIDGETS_SPRING_LOADED) return; mHandler.postDelayed(new Runnable() { @Override public void run() { if (successfulDrop) { + // TODO(hyunyoungs): verify if this hack is still needed, if not, delete. + // // Before we show workspace, hide all apps again because // exitSpringLoadedDragMode made it visible. This is a bit hacky; we should // clean up our state transition functions - mAppsCustomizeTabHost.setVisibility(View.GONE); + mWidgetsView.setVisibility(View.GONE); showWorkspace(true, onCompleteRunnable); } else { exitSpringLoadedDragMode(); @@ -3929,13 +3446,25 @@ public class Launcher extends Activity } void exitSpringLoadedDragMode() { - if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) { - final boolean animated = true; - final boolean springLoaded = true; - showAppsCustomizeHelper(animated, springLoaded); - mState = State.APPS_CUSTOMIZE; + if (mState == State.APPS_SPRING_LOADED) { + showAppsView(true /* animated */, false /* resetListToTop */, + false /* updatePredictedApps */, false /* focusSearchBar */); + } else if (mState == State.WIDGETS_SPRING_LOADED) { + showWidgetsView(true, false); + } + } + + /** + * Updates the set of predicted apps if it hasn't been updated since the last time Launcher was + * resumed. + */ + private void tryAndUpdatePredictedApps() { + if (mLauncherCallbacks != null) { + List<ComponentKey> apps = mLauncherCallbacks.getPredictedApps(); + if (apps != null) { + mAppsView.setPredictedApps(apps); + } } - // Otherwise, we are not in spring loaded mode, so don't do anything. } void lockAllApps() { @@ -3950,7 +3479,7 @@ public class Launcher extends Activity // NO-OP } - public View getQsbBar() { + public View getOrCreateQsbBar() { if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) { return mLauncherCallbacks.getQsbBar(); } @@ -3994,24 +3523,39 @@ public class Launcher extends Activity .commit(); } + mAppWidgetHost.setQsbWidgetId(widgetId); if (widgetId != -1) { mQsb = mAppWidgetHost.createView(this, widgetId, searchProvider); mQsb.updateAppWidgetOptions(opts); mQsb.setPadding(0, 0, 0, 0); mSearchDropTargetBar.addView(mQsb); + mSearchDropTargetBar.setQsbSearchBar(mQsb); } } return mQsb; } + private void reinflateQSBIfNecessary() { + if (mQsb instanceof LauncherAppWidgetHostView && + ((LauncherAppWidgetHostView) mQsb).isReinflateRequired()) { + mSearchDropTargetBar.removeView(mQsb); + mQsb = null; + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { final boolean result = super.dispatchPopulateAccessibilityEvent(event); final List<CharSequence> text = event.getText(); text.clear(); // Populate event with a fake title based on the current state. - if (mState == State.APPS_CUSTOMIZE) { - text.add(mAppsCustomizeTabHost.getContentTag()); + if (mState == State.APPS) { + text.add(getString(R.string.all_apps_button_label)); + } else if (mState == State.WIDGETS) { + text.add(getString(R.string.widget_button_text)); + } else if (mWorkspace != null) { + text.add(mWorkspace.getCurrentPageDescription()); } else { text.add(getString(R.string.all_apps_home_button_label)); } @@ -4021,7 +3565,7 @@ public class Launcher extends Activity /** * Receives notifications when system dialogs are to be closed. */ - private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { + @Thunk class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { closeSystemDialogs(); @@ -4029,36 +3573,21 @@ public class Launcher extends Activity } /** - * Receives notifications whenever the appwidgets are reset. - */ - private class AppWidgetResetObserver extends ContentObserver { - public AppWidgetResetObserver() { - super(new Handler()); - } - - @Override - public void onChange(boolean selfChange) { - onAppWidgetReset(); - } - } - - /** * If the activity is currently paused, signal that we need to run the passed Runnable * in onResume. * * This needs to be called from incoming places where resources might have been loaded - * while we are paused. That is becaues the Configuration might be wrong - * when we're not running, and if it comes back to what it was when we - * were paused, we are not restarted. + * while the activity is paused. That is because the Configuration (e.g., rotation) might be + * wrong when we're not running, and if the activity comes back to what the configuration was + * when we were paused, activity is not restarted. * * Implementation of the method from LauncherModel.Callbacks. * - * @return true if we are currently paused. The caller might be able to - * skip some work in that case since we will come back again. + * @return {@code true} if we are currently paused. The caller might be able to skip some work */ - private boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { + @Thunk boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { if (mPaused) { - Log.i(TAG, "Deferring update until onResume"); + if (LOGD) Log.d(TAG, "Deferring update until onResume"); if (deletePreviousRunnables) { while (mBindOnResumeCallbacks.remove(run)) { } @@ -4094,7 +3623,7 @@ public class Launcher extends Activity */ public boolean setLoadOnResume() { if (mPaused) { - Log.i(TAG, "setLoadOnResume"); + if (LOGD) Log.d(TAG, "setLoadOnResume"); mOnResumeNeedsLoad = true; return true; } else { @@ -4155,10 +3684,6 @@ public class Launcher extends Activity @Override public void bindAddScreens(ArrayList<Long> orderedScreenIds) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - bindAddScreens()", true); - Launcher.addDumpLog(TAG, "11683562 - orderedScreenIds: " + - TextUtils.join(", ", orderedScreenIds), true); int count = orderedScreenIds.size(); for (int i = 0; i < count; i++) { mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(orderedScreenIds.get(i)); @@ -4221,9 +3746,8 @@ public class Launcher extends Activity // Remove the extra empty screen mWorkspace.removeExtraEmptyScreen(false, false); - if (!LauncherAppState.isDisableAllApps() && - addedApps != null && mAppsCustomizeContent != null) { - mAppsCustomizeContent.addApps(addedApps); + if (addedApps != null && mAppsView != null) { + mAppsView.addApps(addedApps); } } @@ -4339,7 +3863,7 @@ public class Launcher extends Activity /** * Implementation of the method from LauncherModel.Callbacks. */ - public void bindFolders(final HashMap<Long, FolderInfo> folders) { + public void bindFolders(final LongArrayMap<FolderInfo> folders) { Runnable r = new Runnable() { public void run() { bindFolders(folders); @@ -4348,8 +3872,7 @@ public class Launcher extends Activity if (waitUntilResume(r)) { return; } - sFolders.clear(); - sFolders.putAll(folders); + sFolders = folders.clone(); } /** @@ -4373,12 +3896,12 @@ public class Launcher extends Activity } final Workspace workspace = mWorkspace; - AppWidgetProviderInfo appWidgetInfo; + LauncherAppWidgetProviderInfo appWidgetInfo = + LauncherModel.getProviderInfo(this, item.providerName, item.user); + if (!mIsSafeModeEnabled && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0) && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0)) { - - appWidgetInfo = mModel.findAppWidgetProviderInfoWithComponent(this, item.providerName); if (appWidgetInfo == null) { if (DEBUG_WIDGETS) { Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId @@ -4391,13 +3914,13 @@ public class Launcher extends Activity // Note: This assumes that the id remap broadcast is received before this step. // If that is not the case, the id remap will be ignored and user may see the // click to setup view. - PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo, null, null); + PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo, null); pendingInfo.spanX = item.spanX; pendingInfo.spanY = item.spanY; pendingInfo.minSpanX = item.minSpanX; pendingInfo.minSpanY = item.minSpanY; - Bundle options = - AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo); + Bundle options = null; + WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( @@ -4428,9 +3951,9 @@ public class Launcher extends Activity if (!mIsSafeModeEnabled && item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { final int appWidgetId = item.appWidgetId; - appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); if (DEBUG_WIDGETS) { - Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); + Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + + appWidgetInfo.provider); } item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); @@ -4449,7 +3972,9 @@ public class Launcher extends Activity workspace.addInScreen(item.hostView, item.container, item.screenId, item.cellX, item.cellY, item.spanX, item.spanY, false); - addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + if (!item.isCustomWidget()) { + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + } workspace.requestLayout(); @@ -4488,10 +4013,10 @@ public class Launcher extends Activity * * Implementation of the method from LauncherModel.Callbacks. */ - public void finishBindingItems(final boolean upgradePath) { + public void finishBindingItems() { Runnable r = new Runnable() { public void run() { - finishBindingItems(upgradePath); + finishBindingItems(); } }; if (waitUntilResume(r)) { @@ -4526,14 +4051,10 @@ public class Launcher extends Activity sPendingAddItem = null; } - if (upgradePath) { - mWorkspace.getUniqueComponents(true, null); - mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null); - } - PackageInstallerCompat.getInstance(this).onFinishBind(); + InstallShortcutReceiver.disableAndFlushInstallQueue(this); if (mLauncherCallbacks != null) { - mLauncherCallbacks.finishBindingItems(upgradePath); + mLauncherCallbacks.finishBindingItems(false); } } @@ -4568,18 +4089,16 @@ public class Launcher extends Activity PropertyValuesHolder.ofFloat("scaleY", 1f)); bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION); bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY); - bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator()); + bounceAnim.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION)); return bounceAnim; } public boolean useVerticalBarLayout() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().isVerticalBarLayout(); + return mDeviceProfile.isVerticalBarLayout(); } protected Rect getSearchBarBounds() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().getSearchBarBounds(); + return mDeviceProfile.getSearchBarBounds(Utilities.isRtl(getResources())); } public void bindSearchablesChanged() { @@ -4590,33 +4109,34 @@ public class Launcher extends Activity mSearchDropTargetBar.removeView(mQsb); mQsb = null; } - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } /** + * A runnable that we can dequeue and re-enqueue when all applications are bound (to prevent + * multiple calls to bind the same list.) + */ + @Thunk ArrayList<AppInfo> mTmpAppsList; + private Runnable mBindAllApplicationsRunnable = new Runnable() { + public void run() { + bindAllApplications(mTmpAppsList); + mTmpAppsList = null; + } + }; + + /** * Add the icons for all apps. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAllApplications(final ArrayList<AppInfo> apps) { - if (LauncherAppState.isDisableAllApps()) { - if (mIntentsOnWorkspaceFromUpgradePath != null) { - if (LauncherModel.UPGRADE_USE_MORE_APPS_FOLDER) { - getHotseat().addAllAppsFolder(mIconCache, apps, - mIntentsOnWorkspaceFromUpgradePath, Launcher.this, mWorkspace); - } - mIntentsOnWorkspaceFromUpgradePath = null; - } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this)); - } - } else { - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setApps(apps); - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this)); - } + if (waitUntilResume(mBindAllApplicationsRunnable, true)) { + mTmpAppsList = apps; + return; + } + + if (mAppsView != null) { + mAppsView.setApps(apps); } if (mLauncherCallbacks != null) { mLauncherCallbacks.bindAllApplications(apps); @@ -4638,9 +4158,8 @@ public class Launcher extends Activity return; } - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.updateApps(apps); + if (mAppsView != null) { + mAppsView.updateApps(apps); } } @@ -4695,22 +4214,17 @@ public class Launcher extends Activity * Implementation of the method from LauncherModel.Callbacks. */ @Override - public void updatePackageState(ArrayList<PackageInstallInfo> installInfo) { - if (mWorkspace != null) { - mWorkspace.updatePackageState(installInfo); + public void bindRestoreItemsChange(final HashSet<ItemInfo> updates) { + Runnable r = new Runnable() { + public void run() { + bindRestoreItemsChange(updates); + } + }; + if (waitUntilResume(r)) { + return; } - } - /** - * Update the label and icon of all the icons in a package - * - * Implementation of the method from LauncherModel.Callbacks. - */ - @Override - public void updatePackageBadge(String packageName) { - if (mWorkspace != null) { - mWorkspace.updatePackageBadge(packageName, UserHandleCompat.myUserHandle()); - } + mWorkspace.updateRestoreItems(updates); } /** @@ -4754,31 +4268,27 @@ public class Launcher extends Activity } // Update AllApps - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.removeApps(appInfos); + if (mAppsView != null) { + mAppsView.removeApps(appInfos); } } - /** - * A number of packages were updated. - */ - private ArrayList<Object> mWidgetsAndShortcuts; private Runnable mBindPackagesUpdatedRunnable = new Runnable() { public void run() { - bindPackagesUpdated(mWidgetsAndShortcuts); - mWidgetsAndShortcuts = null; + bindAllPackages(mWidgetsModel); } }; - public void bindPackagesUpdated(final ArrayList<Object> widgetsAndShortcuts) { + + @Override + public void bindAllPackages(final WidgetsModel model) { if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { - mWidgetsAndShortcuts = widgetsAndShortcuts; + mWidgetsModel = model; return; } - // Update the widgets pane - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts); + if (mWidgetsView != null && model != null) { + mWidgetsView.addWidgets(model); + mWidgetsModel = null; } } @@ -4815,13 +4325,18 @@ public class Launcher extends Activity } public void lockScreenOrientation() { - if (Utilities.isRotationEnabled(this)) { - setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() - .getConfiguration().orientation)); + if (mRotationEnabled) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() + .getConfiguration().orientation)); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + } } } + public void unlockScreenOrientation(boolean immediate) { - if (Utilities.isRotationEnabled(this)) { + if (mRotationEnabled) { if (immediate) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } else { @@ -4984,7 +4499,7 @@ public class Launcher extends Activity editor.apply(); } - private void showFirstRunClings() { + @Thunk void showFirstRunClings() { // The two first run cling paths are mutually exclusive, if the launcher is preinstalled // on the device, then we always show the first run cling experience (or if there is no // launcher2). Otherwise, we prompt the user upon started for migration @@ -5012,42 +4527,54 @@ public class Launcher extends Activity if (mSearchDropTargetBar != null) mSearchDropTargetBar.hideSearchBar(false); } + // TODO: These method should be a part of LauncherSearchCallback + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ItemInfo createAppDragInfo(Intent appLaunchIntent) { - // Called from search suggestion, not supported in other profiles. - final UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + // Called from search suggestion + UserHandleCompat user = null; + if (Utilities.isLmpOrAbove()) { + UserHandle userHandle = appLaunchIntent.getParcelableExtra(Intent.EXTRA_USER); + if (userHandle != null) { + user = UserHandleCompat.fromUser(userHandle); + } + } + return createAppDragInfo(appLaunchIntent, user); + } + + // TODO: This method should be a part of LauncherSearchCallback + public ItemInfo createAppDragInfo(Intent intent, UserHandleCompat user) { + if (user == null) { + user = UserHandleCompat.myUserHandle(); + } + + // Called from search suggestion, add the profile extra to the intent to ensure that we + // can launch it correctly LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); - LauncherActivityInfoCompat activityInfo = launcherApps.resolveActivity(appLaunchIntent, - myUser); + LauncherActivityInfoCompat activityInfo = launcherApps.resolveActivity(intent, user); if (activityInfo == null) { return null; } - return new AppInfo(this, activityInfo, myUser, mIconCache, null); + return new AppInfo(this, activityInfo, user, mIconCache); } + // TODO: This method should be a part of LauncherSearchCallback public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, Bitmap icon) { - // Called from search suggestion, not supported in other profiles. - return createShortcutDragInfo(shortcutIntent, caption, icon, + return new ShortcutInfo(shortcutIntent, caption, caption, icon, UserHandleCompat.myUserHandle()); } - public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, - Bitmap icon, UserHandleCompat user) { - UserManagerCompat userManager = UserManagerCompat.getInstance(this); - CharSequence contentDescription = userManager.getBadgedLabelForUser(caption, user); - return new ShortcutInfo(shortcutIntent, caption, contentDescription, icon, user); - } - - protected void moveWorkspaceToDefaultScreen() { - mWorkspace.moveToDefaultScreen(false); - } - + // TODO: This method should be a part of LauncherSearchCallback public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) { dragView.setTag(dragInfo); mWorkspace.onExternalDragStartedWithItem(dragView); mWorkspace.beginExternalDragShared(dragView, source); } + protected void moveWorkspaceToDefaultScreen() { + mWorkspace.moveToDefaultScreen(false); + } + @Override public void onPageSwitch(View newPage, int newPageIndex) { if (mLauncherCallbacks != null) { @@ -5056,6 +4583,23 @@ public class Launcher extends Activity } /** + * Returns a FastBitmapDrawable with the icon, accurately sized. + */ + public FastBitmapDrawable createIconDrawable(Bitmap icon) { + FastBitmapDrawable d = new FastBitmapDrawable(icon); + d.setFilterBitmap(true); + resizeIconDrawable(d); + return d; + } + + /** + * Resizes an icon drawable to the correct icon size. + */ + public void resizeIconDrawable(Drawable icon) { + icon.setBounds(0, 0, mDeviceProfile.iconSizePx, mDeviceProfile.iconSizePx); + } + + /** * Prints out out state for debugging. */ public void dumpState() { @@ -5067,10 +4611,8 @@ public class Launcher extends Activity Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState); Log.d(TAG, "sFolders.size=" + sFolders.size()); mModel.dumpState(); + // TODO(hyunyoungs): add mWidgetsView.dumpState(); or mWidgetsModel.dumpState(); - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.dumpState(); - } Log.d(TAG, "END launcher3 dump state"); } @@ -5125,6 +4667,14 @@ public class Launcher extends Activity } } + public static CustomAppWidget getCustomAppWidget(String name) { + return sCustomAppWidgets.get(name); + } + + public static HashMap<String, CustomAppWidget> getCustomAppWidgets() { + return sCustomAppWidgets; + } + public void dumpLogsToLocalData() { if (DEBUG_DUMP_LOG) { new AsyncTask<Void, Void, Void>() { @@ -5173,14 +4723,6 @@ public class Launcher extends Activity } } -interface LauncherTransitionable { - View getContent(); - void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace); - void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); - void onLauncherTransitionStep(Launcher l, float t); - void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); -} - interface DebugIntents { static final String DELETE_DATABASE = "com.android.launcher3.action.DELETE_DATABASE"; static final String MIGRATE_DATABASE = "com.android.launcher3.action.MIGRATE_DATABASE"; diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index be295f8b3..6a248a332 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -21,10 +21,14 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewTreeObserver; +import com.android.launcher3.util.UiThreadCircularReveal; + import java.util.HashSet; import java.util.WeakHashMap; @@ -128,13 +132,12 @@ public class LauncherAnimUtils { return anim; } - public static Animator createCircularReveal(View view, int centerX, + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static ValueAnimator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) { - Animator anim = ViewAnimationUtils.createCircularReveal(view, centerX, + ValueAnimator anim = UiThreadCircularReveal.createCircularReveal(view, centerX, centerY, startRadius, endRadius); - if (anim instanceof ValueAnimator) { - new FirstFrameAnimatorHelper((ValueAnimator) anim, view); - } + new FirstFrameAnimatorHelper(anim, view); return anim; } } diff --git a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java b/src/com/android/launcher3/LauncherAnimatorUpdateListener.java deleted file mode 100644 index ec9fd4d16..000000000 --- a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2011 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.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; - -abstract class LauncherAnimatorUpdateListener implements AnimatorUpdateListener { - public void onAnimationUpdate(ValueAnimator animation) { - final float b = (Float) animation.getAnimatedValue(); - final float a = 1f - b; - onAnimationUpdate(a, b); - } - - abstract void onAnimationUpdate(float a, float b); -}
\ No newline at end of file diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index b7c45a340..0b7b1fdc4 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -16,44 +16,28 @@ package com.android.launcher3; -import android.annotation.TargetApi; import android.app.SearchManager; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Point; -import android.os.Build; -import android.os.Handler; -import android.util.DisplayMetrics; import android.util.Log; -import android.view.Display; -import android.view.WindowManager; + +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; -import java.lang.ref.WeakReference; -import java.util.ArrayList; +import com.android.launcher3.util.Thunk; -public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { - private static final String TAG = "LauncherAppState"; +import java.lang.ref.WeakReference; - private static final boolean DEBUG = false; +public class LauncherAppState { private final AppFilter mAppFilter; private final BuildInfo mBuildInfo; - private final LauncherModel mModel; + @Thunk final LauncherModel mModel; private final IconCache mIconCache; + private final WidgetPreviewLoader mWidgetCache; - private final boolean mIsScreenLarge; - private final float mScreenDensity; - private final int mLongPressTimeout = 300; - - private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb; private boolean mWallpaperChangedSinceLastCheck; private static WeakReference<LauncherProvider> sLauncherProvider; @@ -61,7 +45,9 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { private static LauncherAppState INSTANCE; - private DynamicGrid mDynamicGrid; + private InvariantDeviceProfile mInvariantDeviceProfile; + + private LauncherAccessibilityDelegate mAccessibilityDelegate; public static LauncherAppState getInstance() { if (INSTANCE == null) { @@ -96,42 +82,26 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { MemoryTracker.startTrackingMe(sContext, "L"); } - // set sIsScreenXLarge and mScreenDensity *before* creating icon cache - mIsScreenLarge = isScreenLarge(sContext.getResources()); - mScreenDensity = sContext.getResources().getDisplayMetrics().density; - - recreateWidgetPreviewDb(); - mIconCache = new IconCache(sContext); + mInvariantDeviceProfile = new InvariantDeviceProfile(sContext); + mIconCache = new IconCache(sContext, mInvariantDeviceProfile); + mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache); mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class)); mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class)); mModel = new LauncherModel(this, mIconCache, mAppFilter); - final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); - launcherApps.addOnAppsChangedCallback(mModel); + + LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel); // Register intent receivers IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); - filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); - sContext.registerReceiver(mModel, filter); - - // Register for changes to the favorites - ContentResolver resolver = sContext.getContentResolver(); - resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, - mFavoritesObserver); - } + // For handling managed profiles + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); - public void recreateWidgetPreviewDb() { - if (mWidgetPreviewCacheDb != null) { - mWidgetPreviewCacheDb.close(); - } - mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext); + sContext.registerReceiver(mModel, filter); } /** @@ -142,45 +112,37 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); launcherApps.removeOnAppsChangedCallback(mModel); PackageInstallerCompat.getInstance(sContext).onStop(); - - ContentResolver resolver = sContext.getContentResolver(); - resolver.unregisterContentObserver(mFavoritesObserver); } /** - * Receives notifications whenever the user favorites have changed. + * Reloads the workspace items from the DB and re-binds the workspace. This should generally + * not be called as DB updates are automatically followed by UI update */ - private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - // If the database has ever changed, then we really need to force a reload of the - // workspace on the next load - mModel.resetLoadedState(false, true); - mModel.startLoaderFromBackground(); - } - }; + public void reloadWorkspace() { + mModel.resetLoadedState(false, true); + mModel.startLoaderFromBackground(); + } LauncherModel setLauncher(Launcher launcher) { + getLauncherProvider().setLauncherProviderChangeListener(launcher); mModel.initialize(launcher); + mAccessibilityDelegate = ((launcher != null) && Utilities.isLmpOrAbove()) ? + new LauncherAccessibilityDelegate(launcher) : null; return mModel; } + public LauncherAccessibilityDelegate getAccessibilityDelegate() { + return mAccessibilityDelegate; + } + public IconCache getIconCache() { return mIconCache; } - LauncherModel getModel() { + public LauncherModel getModel() { return mModel; } - boolean shouldShowAppOrWidgetProvider(ComponentName componentName) { - return mAppFilter == null || mAppFilter.shouldShowApp(componentName); - } - - WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() { - return mWidgetPreviewCacheDb; - } - static void setLauncherProvider(LauncherProvider provider) { sLauncherProvider = new WeakReference<LauncherProvider>(provider); } @@ -193,71 +155,10 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return LauncherFiles.SHARED_PREFERENCES_KEY; } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - DeviceProfile initDynamicGrid(Context context) { - mDynamicGrid = createDynamicGrid(context, mDynamicGrid); - mDynamicGrid.getDeviceProfile().addCallback(this); - return mDynamicGrid.getDeviceProfile(); + public WidgetPreviewLoader getWidgetCache() { + return mWidgetCache; } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - static DynamicGrid createDynamicGrid(Context context, DynamicGrid dynamicGrid) { - // Determine the dynamic grid properties - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - - Point realSize = new Point(); - display.getRealSize(realSize); - DisplayMetrics dm = new DisplayMetrics(); - display.getMetrics(dm); - - if (dynamicGrid == null) { - Point smallestSize = new Point(); - Point largestSize = new Point(); - display.getCurrentSizeRange(smallestSize, largestSize); - - dynamicGrid = new DynamicGrid(context, - context.getResources(), - Math.min(smallestSize.x, smallestSize.y), - Math.min(largestSize.x, largestSize.y), - realSize.x, realSize.y, - dm.widthPixels, dm.heightPixels); - } - - // Update the icon size - DeviceProfile grid = dynamicGrid.getDeviceProfile(); - grid.updateFromConfiguration(context, context.getResources(), - realSize.x, realSize.y, - dm.widthPixels, dm.heightPixels); - return dynamicGrid; - } - - public DynamicGrid getDynamicGrid() { - return mDynamicGrid; - } - - public boolean isScreenLarge() { - return mIsScreenLarge; - } - - // Need a version that doesn't require an instance of LauncherAppState for the wallpaper picker - public static boolean isScreenLarge(Resources res) { - return res.getBoolean(R.bool.is_large_tablet); - } - - public static boolean isScreenLandscape(Context context) { - return context.getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - } - - public float getScreenDensity() { - return mScreenDensity; - } - - public int getLongPressTimeout() { - return mLongPressTimeout; - } - + public void onWallpaperChanged() { mWallpaperChangedSinceLastCheck = true; } @@ -268,29 +169,11 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return result; } - @Override - public void onAvailableSizeChanged(DeviceProfile grid) { - Utilities.setIconSize(grid.iconSizePx); - } - - public static boolean isDisableAllApps() { - // Returns false on non-dogfood builds. - return getInstance().mBuildInfo.isDogfoodBuild() && - Utilities.isPropertyEnabled(Launcher.DISABLE_ALL_APPS_PROPERTY); + public InvariantDeviceProfile getInvariantDeviceProfile() { + return mInvariantDeviceProfile; } public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } - - public void setPackageState(ArrayList<PackageInstallInfo> installInfo) { - mModel.setPackageState(installInfo); - } - - /** - * Updates the icons and label of all icons for the provided package name. - */ - public void updatePackageBadge(String packageName) { - mModel.updatePackageBadge(packageName); - } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index a309f268c..de7c61000 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -21,9 +21,12 @@ import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.os.TransactionTooLargeException; +import android.view.LayoutInflater; +import android.view.View; import java.util.ArrayList; + /** * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} * which correctly captures all long-press events. This ensures that users can @@ -33,16 +36,31 @@ public class LauncherAppWidgetHost extends AppWidgetHost { private final ArrayList<Runnable> mProviderChangeListeners = new ArrayList<Runnable>(); - Launcher mLauncher; + private int mQsbWidgetId = -1; + private Launcher mLauncher; public LauncherAppWidgetHost(Launcher launcher, int hostId) { super(launcher, hostId); mLauncher = launcher; } + public void setQsbWidgetId(int widgetId) { + mQsbWidgetId = widgetId; + } + @Override protected AppWidgetHostView onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { + if (appWidgetId == mQsbWidgetId) { + return new LauncherAppWidgetHostView(context) { + + @Override + protected View getErrorView() { + // For the QSB, show an empty view instead of an error view. + return new View(getContext()); + } + }; + } return new LauncherAppWidgetHostView(context); } @@ -77,12 +95,37 @@ public class LauncherAppWidgetHost extends AppWidgetHost { } protected void onProvidersChanged() { - // Once we get the message that widget packages are updated, we need to rebind items - // in AppsCustomize accordingly. - mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher)); + mLauncher.getModel().loadAndBindWidgetsAndShortcuts(mLauncher, mLauncher, + true /* refresh */); + if (!mProviderChangeListeners.isEmpty()) { + for (Runnable callback : new ArrayList<>(mProviderChangeListeners)) { + callback.run(); + } + } + } - for (Runnable callback : mProviderChangeListeners) { - callback.run(); + public AppWidgetHostView createView(Context context, int appWidgetId, + LauncherAppWidgetProviderInfo appWidget) { + if (appWidget.isCustomWidget) { + LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context); + LayoutInflater inflater = (LayoutInflater) + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(appWidget.initialLayout, lahv); + lahv.setAppWidget(0, appWidget); + lahv.updateLastInflationOrientation(); + return lahv; + } else { + return super.createView(context, appWidgetId, appWidget); } } + + /** + * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. + */ + @Override + protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { + LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( + mLauncher, appWidget); + super.onProviderChanged(appWidgetId, info); + } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java index e39727b17..cf461a5b8 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -35,6 +36,7 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc LayoutInflater mInflater; private CheckLongPressHelper mLongPressHelper; + private StylusEventHelper mStylusEventHelper; private Context mContext; private int mPreviousOrientation; private DragLayer mDragLayer; @@ -45,8 +47,10 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc super(context); mContext = context; mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDragLayer = ((Launcher) context).getDragLayer(); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } @Override @@ -54,10 +58,14 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc return mInflater.inflate(R.layout.appwidget_error, this, false); } + public void updateLastInflationOrientation() { + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + } + @Override public void updateAppWidget(RemoteViews remoteViews) { // Store the orientation in which the widget was inflated - mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + updateLastInflationOrientation(); super.updateAppWidget(remoteViews); } @@ -83,11 +91,17 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc return true; } - // Watch for longpress events at this level to make sure - // users can always pick up this widget + // Watch for longpress or stylus button press events at this level to + // make sure users can always pick up this widget + if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + mLongPressHelper.cancelLongPress(); + return true; + } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { - mLongPressHelper.postCheckForLongPress(); + if (!mStylusEventHelper.inStylusButtonPressed()) { + mLongPressHelper.postCheckForLongPress(); + } mDragLayer.setTouchCompleteListener(this); break; } @@ -137,6 +151,20 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc } @Override + public AppWidgetProviderInfo getAppWidgetInfo() { + AppWidgetProviderInfo info = super.getAppWidgetInfo(); + if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { + throw new IllegalStateException("Launcher widget must have" + + " LauncherAppWidgetProviderInfo"); + } + return info; + } + + public LauncherAppWidgetProviderInfo getLauncherAppWidgetProviderInfo() { + return (LauncherAppWidgetProviderInfo) getAppWidgetInfo(); + } + + @Override public void onTouchComplete() { if (!mLongPressHelper.hasPerformedLongPress()) { // If a long press has been performed, we don't want to clear the record of that since diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index 5c6535a24..aad18b578 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -56,6 +56,11 @@ public class LauncherAppWidgetInfo extends ItemInfo { static final int NO_ID = -1; /** + * Indicates that this is a locally defined widget and hence has no system allocated id. + */ + static final int CUSTOM_WIDGET_ID = -100; + + /** * Identifier for this widget when talking with * {@link android.appwidget.AppWidgetManager} for updates. */ @@ -86,7 +91,12 @@ public class LauncherAppWidgetInfo extends ItemInfo { AppWidgetHostView hostView = null; LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) { - itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + if (appWidgetId == CUSTOM_WIDGET_ID) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.appWidgetId = appWidgetId; this.providerName = providerName; @@ -99,6 +109,10 @@ public class LauncherAppWidgetInfo extends ItemInfo { restoreStatus = RESTORE_COMPLETED; } + public boolean isCustomWidget() { + return appWidgetId == CUSTOM_WIDGET_ID; + } + @Override void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java new file mode 100644 index 000000000..85af92f30 --- /dev/null +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -0,0 +1,113 @@ +package com.android.launcher3; + +import android.annotation.TargetApi; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; + +/** + * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords + * a common object for describing both framework provided AppWidgets as well as custom widgets + * (who's implementation is owned by the launcher). This object represents a widget type / class, + * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo} + */ +public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { + + public boolean isCustomWidget = false; + + private int mSpanX = -1; + private int mSpanY = -1; + private int mMinSpanX = -1; + private int mMinSpanY = -1; + + public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context, + AppWidgetProviderInfo info) { + + // In lieu of a public super copy constructor, we first write the AppWidgetProviderInfo + // into a parcel, and then construct a new LauncherAppWidgetProvider info from the + // associated super parcel constructor. This allows us to copy non-public members without + // using reflection. + Parcel p = Parcel.obtain(); + info.writeToParcel(p, 0); + p.setDataPosition(0); + LauncherAppWidgetProviderInfo lawpi = new LauncherAppWidgetProviderInfo(p); + p.recycle(); + return lawpi; + } + + public LauncherAppWidgetProviderInfo(Parcel in) { + super(in); + } + + public LauncherAppWidgetProviderInfo(Context context, CustomAppWidget widget) { + isCustomWidget = true; + + provider = new ComponentName(context, widget.getClass().getName()); + icon = widget.getIcon(); + label = widget.getLabel(); + previewImage = widget.getPreviewImage(); + initialLayout = widget.getWidgetLayout(); + resizeMode = widget.getResizeMode(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public String getLabel(PackageManager packageManager) { + if (isCustomWidget) { + return Utilities.trim(label); + } + return super.loadLabel(packageManager); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Drawable getIcon(Context context, IconCache cache) { + if (isCustomWidget) { + return cache.getFullResIcon(provider.getPackageName(), icon); + } + return super.loadIcon(context, + LauncherAppState.getInstance().getInvariantDeviceProfile().fillResIconDpi); + } + + public String toString(PackageManager pm) { + if (isCustomWidget) { + return "WidgetProviderInfo(" + provider + ")"; + } + return String.format("WidgetProviderInfo provider:%s package:%s short:%s label:%s", + provider.toString(), provider.getPackageName(), provider.getShortClassName(), getLabel(pm)); + } + + public int getSpanX(Launcher launcher) { + lazyLoadSpans(launcher); + return mSpanX; + } + + public int getSpanY(Launcher launcher) { + lazyLoadSpans(launcher); + return mSpanY; + } + + public int getMinSpanX(Launcher launcher) { + lazyLoadSpans(launcher); + return mMinSpanX; + } + + public int getMinSpanY(Launcher launcher) { + lazyLoadSpans(launcher); + return mMinSpanY; + } + + private void lazyLoadSpans(Launcher launcher) { + if (mSpanX < 0 || mSpanY < 0 || mMinSpanX < 0 || mMinSpanY < 0) { + int[] minResizeSpan = launcher.getMinSpanForWidget(this); + int[] span = launcher.getSpanForWidget(this); + + mSpanX = span[0]; + mSpanY = span[1]; + mMinSpanX = minResizeSpan[0]; + mMinSpanY = minResizeSpan[1]; + } + } + } diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index 3868a57f1..a92a889f9 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -32,7 +32,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { private static final String LAUNCHER_DATA_PREFIX = "L"; - static final boolean VERBOSE = true; + static final boolean VERBOSE = false; static final boolean DEBUG = false; private static BackupManager sBackupManager; @@ -78,7 +78,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { super.onRestore(data, appVersionCode, newState); // If no favorite was migrated, clear the data and start fresh. final Cursor c = getContentResolver().query( - LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, null, null, null, null); + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); hasData = c.moveToNext(); c.close(); } catch (Exception e) { @@ -90,6 +90,12 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { if (hasData && mHelper.restoreSuccessful) { LauncherAppState.getLauncherProvider().clearFlagEmptyDbCreated(); LauncherClings.synchonouslyMarkFirstRunClingDismissed(this); + + // TODO: Update the backup set to include rank. + if (mHelper.restoredBackupVersion <= 3) { + LauncherAppState.getLauncherProvider().updateFolderItemsRank(); + LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities(); + } } else { if (VERBOSE) Log.v(TAG, "Nothing was restored, clearing DB"); LauncherAppState.getLauncherProvider().createEmptyDB(); diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 437434748..8c6fedbdc 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -19,14 +19,16 @@ import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; import android.app.backup.BackupManager; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -49,18 +51,20 @@ 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 java.io.ByteArrayOutputStream; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.zip.CRC32; /** @@ -71,7 +75,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; private static final boolean DEBUG = LauncherBackupAgentHelper.DEBUG; - private static final int BACKUP_VERSION = 2; + private static final int BACKUP_VERSION = 3; private static final int MAX_JOURNAL_SIZE = 1000000; // Journal key is such that it is always smaller than any dynamically generated @@ -84,11 +88,6 @@ public class LauncherBackupHelper implements BackupHelper { /** widgets contain previews, which are very large, dribble them out */ private static final int MAX_WIDGETS_PER_PASS = 5; - private static final int IMAGE_COMPRESSION_QUALITY = 75; - - private static final Bitmap.CompressFormat IMAGE_FORMAT = - android.graphics.Bitmap.CompressFormat.PNG; - private static final String[] FAVORITE_PROJECTION = { Favorites._ID, // 0 Favorites.MODIFIED, // 1 @@ -136,24 +135,33 @@ 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; + private final long mUserSerial; - private IconCache mIconCache; private BackupManager mBackupManager; - private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap; private byte[] mBuffer = new byte[512]; private long mLastBackupRestoreTime; + private boolean mBackupDataWasUpdated; + + private IconCache mIconCache; + private DeviceProfieData mDeviceProfileData; + private InvariantDeviceProfile mIdp; - private DeviceProfieData mCurrentProfile; boolean restoreSuccessful; + int restoredBackupVersion = 1; public LauncherBackupHelper(Context context) { mContext = context; mExistingKeys = new HashSet<String>(); mKeys = new ArrayList<Key>(); restoreSuccessful = true; + mItemTypeMatchers = new ItemTypeMatcher[CommonAppTypeParser.SUPPORTED_TYPE_COUNT]; + + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + mUserSerial = userManager.getSerialNumberForUser(UserHandleCompat.myUserHandle()); } private void dataChanged() { @@ -171,6 +179,7 @@ public class LauncherBackupHelper implements BackupHelper { mExistingKeys.add(keyToBackupKey(key)); } } + restoredBackupVersion = journal.backupVersion; } /** @@ -191,10 +200,19 @@ public class LauncherBackupHelper implements BackupHelper { Journal in = readJournal(oldState); if (!launcherIsReady()) { + dataChanged(); // Perform backup later. writeJournal(newState, in); return; } + + if (mDeviceProfileData == null) { + LauncherAppState app = LauncherAppState.getInstance(); + mIdp = app.getInvariantDeviceProfile(); + mDeviceProfileData = initDeviceProfileData(mIdp); + mIconCache = app.getIconCache(); + } + Log.v(TAG, "lastBackupTime = " + in.t); mKeys.clear(); applyJournal(in); @@ -202,7 +220,7 @@ public class LauncherBackupHelper implements BackupHelper { // Record the time before performing backup so that entries edited while the backup // was going on, do not get missed in next backup. long newBackupTime = System.currentTimeMillis(); - + mBackupDataWasUpdated = false; try { backupFavorites(data); backupScreens(data); @@ -220,16 +238,30 @@ public class LauncherBackupHelper implements BackupHelper { for (String deleted: mExistingKeys) { if (VERBOSE) Log.v(TAG, "dropping deleted item " + deleted); data.writeEntityHeader(deleted, -1); + mBackupDataWasUpdated = true; } mExistingKeys.clear(); - mLastBackupRestoreTime = newBackupTime; + if (!mBackupDataWasUpdated) { + // Check if any metadata has changed + mBackupDataWasUpdated = (in.profile == null) + || !Arrays.equals(DeviceProfieData.toByteArray(in.profile), + DeviceProfieData.toByteArray(mDeviceProfileData)) + || (in.backupVersion != BACKUP_VERSION) + || (in.appVersion != getAppVersion()); + } - // We store the journal at two places. - // 1) Storing it in newState allows us to do partial backups by comparing old state - // 2) Storing it in backup data allows us to validate keys during restore - Journal state = getCurrentStateJournal(); - writeRowToBackup(JOURNAL_KEY, state, data); + if (mBackupDataWasUpdated) { + mLastBackupRestoreTime = newBackupTime; + + // We store the journal at two places. + // 1) Storing it in newState allows us to do partial backups by comparing old state + // 2) Storing it in backup data allows us to validate keys during restore + Journal state = getCurrentStateJournal(); + writeRowToBackup(JOURNAL_KEY, state, data); + } else { + if (DEBUG) Log.d(TAG, "Nothing was written during backup"); + } } catch (IOException e) { Log.e(TAG, "launcher backup has failed", e); } @@ -242,8 +274,7 @@ public class LauncherBackupHelper implements BackupHelper { * to this device. */ private boolean isBackupCompatible(Journal oldState) { - DeviceProfieData currentProfile = getDeviceProfieData(); - + DeviceProfieData currentProfile = mDeviceProfileData; DeviceProfieData oldProfile = oldState.profile; if (oldProfile == null || oldProfile.desktopCols == 0) { @@ -277,6 +308,14 @@ public class LauncherBackupHelper implements BackupHelper { return; } + if (mDeviceProfileData == null) { + // This call does not happen on a looper thread. So LauncherAppState + // can't be created . Instead initialize required dependencies directly. + mIdp = new InvariantDeviceProfile(mContext); + mDeviceProfileData = initDeviceProfileData(mIdp); + mIconCache = new IconCache(mContext, mIdp); + } + int dataSize = data.size(); if (mBuffer.length < dataSize) { mBuffer = new byte[dataSize]; @@ -351,7 +390,7 @@ public class LauncherBackupHelper implements BackupHelper { journal.key = mKeys.toArray(new BackupProtos.Key[mKeys.size()]); journal.appVersion = getAppVersion(); journal.backupVersion = BACKUP_VERSION; - journal.profile = getDeviceProfieData(); + journal.profile = mDeviceProfileData; return journal; } @@ -364,23 +403,13 @@ public class LauncherBackupHelper implements BackupHelper { } } - /** - * @return the current device profile information. - */ - private DeviceProfieData getDeviceProfieData() { - if (mCurrentProfile != null) { - return mCurrentProfile; - } - final Context applicationContext = mContext.getApplicationContext(); - DeviceProfile profile = LauncherAppState.createDynamicGrid(applicationContext, null) - .getDeviceProfile(); - - mCurrentProfile = new DeviceProfieData(); - mCurrentProfile.desktopRows = profile.numRows; - mCurrentProfile.desktopCols = profile.numColumns; - mCurrentProfile.hotseatCount = profile.numHotseatIcons; - mCurrentProfile.allappsRank = profile.hotseatAllAppsRank; - return mCurrentProfile; + private DeviceProfieData initDeviceProfileData(InvariantDeviceProfile profile) { + DeviceProfieData data = new DeviceProfieData(); + data.desktopRows = profile.numRows; + data.desktopCols = profile.numColumns; + data.hotseatCount = profile.numHotseatIcons; + data.allappsRank = profile.hotseatAllAppsRank; + return data; } /** @@ -430,7 +459,7 @@ public class LauncherBackupHelper implements BackupHelper { ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackFavorite(buffer, dataSize); - cr.insert(Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(Favorites.CONTENT_URI, values); } /** @@ -491,11 +520,6 @@ public class LauncherBackupHelper implements BackupHelper { */ private void backupIcons(BackupDataOutput data) throws IOException { // persist icons that haven't been persisted yet - if (!initializeIconCache()) { - dataChanged(); // try again later - if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying icon backup"); - return; - } final ContentResolver cr = mContext.getContentResolver(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; final UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); @@ -579,7 +603,8 @@ public class LauncherBackupHelper implements BackupHelper { Log.w(TAG, "failed to unpack icon for " + key.name); } if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); - IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(key.name), icon, res.dpi); + mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, + "" /* label */, mUserSerial); } /** @@ -591,17 +616,8 @@ public class LauncherBackupHelper implements BackupHelper { */ private void backupWidgets(BackupDataOutput data) throws IOException { // persist static widget info that hasn't been persisted yet - final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); - if (appState == null || !initializeIconCache()) { - Log.w(TAG, "Failed to get icon cache during restore"); - return; - } final ContentResolver cr = mContext.getContentResolver(); - final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext); - final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; - final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile(); - if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx); int backupWidgetCount = 0; String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET + " AND " @@ -613,8 +629,6 @@ public class LauncherBackupHelper implements BackupHelper { while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); - final int spanX = cursor.getInt(SPANX_INDEX); - final int spanY = cursor.getInt(SPANY_INDEX); final ComponentName provider = ComponentName.unflattenFromString(providerName); Key key = null; String backupKey = null; @@ -624,7 +638,7 @@ public class LauncherBackupHelper implements BackupHelper { } else { Log.w(TAG, "empty intent on appwidget: " + id); } - if (mExistingKeys.contains(backupKey)) { + if (mExistingKeys.contains(backupKey) && restoredBackupVersion >= BACKUP_VERSION) { if (DEBUG) Log.d(TAG, "already saved widget " + backupKey); // remember that we already backed this up previously @@ -633,9 +647,8 @@ public class LauncherBackupHelper implements BackupHelper { if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount); if (backupWidgetCount < MAX_WIDGETS_PER_PASS) { if (DEBUG) Log.d(TAG, "saving widget " + backupKey); - previewLoader.setPreviewSize(spanX * profile.cellWidthPx, - spanY * profile.cellHeightPx, widgetSpacingLayout); - writeRowToBackup(key, packWidget(dpi, previewLoader, mIconCache, provider), data); + UserHandleCompat user = UserHandleCompat.myUserHandle(); + writeRowToBackup(key, packWidget(dpi, provider, user), data); mKeys.add(key); backupWidgetCount ++; } else { @@ -671,8 +684,8 @@ public class LauncherBackupHelper implements BackupHelper { if (icon == null) { Log.w(TAG, "failed to unpack widget icon for " + key.name); } else { - IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(widget.provider), - icon, widget.icon.dpi); + mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider), + icon, widget.icon.dpi, widget.label, mUserSerial); } } @@ -738,6 +751,17 @@ public class LauncherBackupHelper implements BackupHelper { return checksum.getValue(); } + /** + * @return true if its an hotseat item, that can be replaced during restore. + * TODO: Extend check for folders in hotseat. + */ + private boolean isReplaceableHotseatItem(Favorite favorite) { + return favorite.container == Favorites.CONTAINER_HOTSEAT + && favorite.intent != null + && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION + || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT); + } + /** Serialize a Favorite for persistence, including a checksum wrapper. */ private Favorite packFavorite(Cursor c) { Favorite favorite = new Favorite(); @@ -749,30 +773,16 @@ public class LauncherBackupHelper implements BackupHelper { favorite.spanX = c.getInt(SPANX_INDEX); favorite.spanY = c.getInt(SPANY_INDEX); favorite.iconType = c.getInt(ICON_TYPE_INDEX); - if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { - String iconPackage = c.getString(ICON_PACKAGE_INDEX); - if (!TextUtils.isEmpty(iconPackage)) { - favorite.iconPackage = iconPackage; - } - String iconResource = c.getString(ICON_RESOURCE_INDEX); - if (!TextUtils.isEmpty(iconResource)) { - favorite.iconResource = iconResource; - } - } - if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) { - byte[] blob = c.getBlob(ICON_INDEX); - if (blob != null && blob.length > 0) { - favorite.icon = blob; - } - } + String title = c.getString(TITLE_INDEX); if (!TextUtils.isEmpty(title)) { favorite.title = title; } String intentDescription = c.getString(INTENT_INDEX); + Intent intent = null; if (!TextUtils.isEmpty(intentDescription)) { try { - Intent intent = Intent.parseUri(intentDescription, 0); + intent = Intent.parseUri(intentDescription, 0); intent.removeExtra(ItemInfo.EXTRA_PROFILE); favorite.intent = intent.toUri(0); } catch (URISyntaxException e) { @@ -786,6 +796,47 @@ public class LauncherBackupHelper implements BackupHelper { if (!TextUtils.isEmpty(appWidgetProvider)) { favorite.appWidgetProvider = appWidgetProvider; } + } else if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { + if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { + String iconPackage = c.getString(ICON_PACKAGE_INDEX); + if (!TextUtils.isEmpty(iconPackage)) { + favorite.iconPackage = iconPackage; + } + String iconResource = c.getString(ICON_RESOURCE_INDEX); + if (!TextUtils.isEmpty(iconResource)) { + favorite.iconResource = iconResource; + } + } + + byte[] blob = c.getBlob(ICON_INDEX); + if (blob != null && blob.length > 0) { + favorite.icon = blob; + } + } + + if (isReplaceableHotseatItem(favorite)) { + if (intent != null && intent.getComponent() != null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo activity = null;; + try { + activity = pm.getActivityInfo(intent.getComponent(), 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target not found", e); + } + if (activity == null) { + return favorite; + } + for (int i = 0; i < mItemTypeMatchers.length; i++) { + if (mItemTypeMatchers[i] == null) { + mItemTypeMatchers[i] = new ItemTypeMatcher( + CommonAppTypeParser.getResourceForItemType(i)); + } + if (mItemTypeMatchers[i].matches(activity, pm)) { + favorite.targetType = i; + break; + } + } + } } return favorite; @@ -795,6 +846,7 @@ public class LauncherBackupHelper implements BackupHelper { private ContentValues unpackFavorite(byte[] buffer, int dataSize) throws IOException { Favorite favorite = unpackProto(new Favorite(), buffer, dataSize); + ContentValues values = new ContentValues(); values.put(Favorites._ID, favorite.id); values.put(Favorites.SCREEN, favorite.screen); @@ -803,14 +855,16 @@ public class LauncherBackupHelper implements BackupHelper { values.put(Favorites.CELLY, favorite.cellY); values.put(Favorites.SPANX, favorite.spanX); values.put(Favorites.SPANY, favorite.spanY); - values.put(Favorites.ICON_TYPE, favorite.iconType); - if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { - values.put(Favorites.ICON_PACKAGE, favorite.iconPackage); - values.put(Favorites.ICON_RESOURCE, favorite.iconResource); - } - if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) { + + if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { + values.put(Favorites.ICON_TYPE, favorite.iconType); + if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { + values.put(Favorites.ICON_PACKAGE, favorite.iconPackage); + values.put(Favorites.ICON_RESOURCE, favorite.iconResource); + } values.put(Favorites.ICON, favorite.icon); } + if (!TextUtils.isEmpty(favorite.title)) { values.put(Favorites.TITLE, favorite.title); } else { @@ -826,7 +880,7 @@ public class LauncherBackupHelper implements BackupHelper { UserManagerCompat.getInstance(mContext).getSerialNumberForUser(myUserHandle); values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); - DeviceProfieData currentProfile = getDeviceProfieData(); + DeviceProfieData currentProfile = mDeviceProfileData; if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { if (!TextUtils.isEmpty(favorite.appWidgetProvider)) { @@ -845,8 +899,17 @@ public class LauncherBackupHelper implements BackupHelper { throw new InvalidBackupException("Widget not in screen bounds, aborting restore"); } } else { - // Let LauncherModel know we've been here. - values.put(LauncherSettings.Favorites.RESTORED, 1); + // Check if it is an hotseat item, that can be replaced. + if (isReplaceableHotseatItem(favorite) + && favorite.targetType != Favorite.TARGET_NONE + && favorite.targetType < CommonAppTypeParser.SUPPORTED_TYPE_COUNT) { + Log.e(TAG, "Added item type flag"); + values.put(LauncherSettings.Favorites.RESTORED, + 1 | CommonAppTypeParser.encodeItemTypeToFlag(favorite.targetType)); + } else { + // Let LauncherModel know we've been here. + values.put(LauncherSettings.Favorites.RESTORED, 1); + } // Verify placement if (favorite.container == Favorites.CONTAINER_HOTSEAT) { @@ -889,40 +952,35 @@ public class LauncherBackupHelper implements BackupHelper { private Resource packIcon(int dpi, Bitmap icon) { Resource res = new Resource(); res.dpi = dpi; - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - res.data = os.toByteArray(); - } + res.data = Utilities.flattenBitmap(icon); return res; } /** Serialize a widget for persistence, including a checksum wrapper. */ - private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache, - ComponentName provider) { - final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider); + private Widget packWidget(int dpi, ComponentName provider, UserHandleCompat user) { + final LauncherAppWidgetProviderInfo info = + LauncherModel.getProviderInfo(mContext, provider, user); Widget widget = new Widget(); widget.provider = provider.flattenToShortString(); widget.label = info.label; widget.configure = info.configure != null; if (info.icon != 0) { widget.icon = new Resource(); - Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon); + Drawable fullResIcon = mIconCache.getFullResIcon(provider.getPackageName(), info.icon); Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - widget.icon.data = os.toByteArray(); - widget.icon.dpi = dpi; - } - } - if (info.previewImage != 0) { - widget.preview = new Resource(); - Bitmap preview = previewLoader.generateWidgetPreview(info, null); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - widget.preview.data = os.toByteArray(); - widget.preview.dpi = dpi; - } + widget.icon.data = Utilities.flattenBitmap(icon); + widget.icon.dpi = dpi; } + + // Calculate the spans corresponding to any one of the orientations as it should not change + // based on orientation. + int[] minSpans = CellLayout.rectToCell( + mIdp.portraitProfile, mContext, info.minResizeWidth, info.minResizeHeight, null); + widget.minSpanX = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 + ? minSpans[0] : -1; + widget.minSpanY = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_VERTICAL) != 0 + ? minSpans[1] : -1; + return widget; } @@ -1019,6 +1077,7 @@ public class LauncherBackupHelper implements BackupHelper { byte[] blob = writeCheckedBytes(proto); data.writeEntityHeader(backupKey, blob.length); data.writeEntityData(blob, blob.length); + mBackupDataWasUpdated = true; if (VERBOSE) Log.v(TAG, "Writing New entry " + backupKey); } @@ -1067,36 +1126,6 @@ public class LauncherBackupHelper implements BackupHelper { return wrapper.payload; } - private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) { - if (mWidgetMap == null) { - List<AppWidgetProviderInfo> widgets = - AppWidgetManager.getInstance(mContext).getInstalledProviders(); - mWidgetMap = new HashMap<ComponentName, AppWidgetProviderInfo>(widgets.size()); - for (AppWidgetProviderInfo info : widgets) { - mWidgetMap.put(info.provider, info); - } - } - return mWidgetMap.get(component); - } - - - private boolean initializeIconCache() { - if (mIconCache != null) { - return true; - } - - final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); - if (appState == null) { - Throwable stackTrace = new Throwable(); - stackTrace.fillInStackTrace(); - Log.w(TAG, "Failed to get app state during backup/restore", stackTrace); - return false; - } - mIconCache = appState.getIconCache(); - return mIconCache != null; - } - - /** * @return true if the launcher is in a state to support backup */ @@ -1109,9 +1138,8 @@ public class LauncherBackupHelper implements BackupHelper { } cursor.close(); - if (!initializeIconCache()) { + if (LauncherAppState.getInstanceNoCreate() == null) { // launcher services are unavailable, try again later - dataChanged(); return false; } @@ -1123,13 +1151,66 @@ public class LauncherBackupHelper implements BackupHelper { .getSerialNumberForUser(UserHandleCompat.myUserHandle()); } - private class InvalidBackupException extends IOException { - private InvalidBackupException(Throwable cause) { + @Thunk class InvalidBackupException extends IOException { + + private static final long serialVersionUID = 8931456637211665082L; + + @Thunk InvalidBackupException(Throwable cause) { super(cause); } - public InvalidBackupException(String reason) { + @Thunk InvalidBackupException(String reason) { super(reason); } } + + /** + * A class to check if an activity can handle one of the intents from a list of + * predefined intents. + */ + private class ItemTypeMatcher { + + private final ArrayList<Intent> mIntents; + + ItemTypeMatcher(int xml_res) { + mIntents = xml_res == 0 ? new ArrayList<Intent>() : parseIntents(xml_res); + } + + private ArrayList<Intent> parseIntents(int xml_res) { + ArrayList<Intent> intents = new ArrayList<Intent>(); + XmlResourceParser parser = mContext.getResources().getXml(xml_res); + try { + DefaultLayoutParser.beginDocument(parser, DefaultLayoutParser.TAG_RESOLVE); + final int depth = parser.getDepth(); + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } else if (DefaultLayoutParser.TAG_FAVORITE.equals(parser.getName())) { + final String uri = DefaultLayoutParser.getAttributeValue( + parser, DefaultLayoutParser.ATTR_URI); + intents.add(Intent.parseUri(uri, 0)); + } + } + } catch (URISyntaxException | XmlPullParserException | IOException e) { + Log.e(TAG, "Unable to parse " + xml_res, e); + } finally { + parser.close(); + } + return intents; + } + + public boolean matches(ActivityInfo activity, PackageManager pm) { + for (Intent intent : mIntents) { + intent.setPackage(activity.packageName); + ResolveInfo info = pm.resolveActivity(intent, 0); + if (info != null && (info.activityInfo.name.equals(activity.name) + || info.activityInfo.name.equals(activity.targetActivity))) { + return true; + } + } + return false; + } + } } diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index a1f4e0b90..6618cca78 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -7,10 +7,13 @@ import android.os.Bundle; import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.allapps.AllAppsSearchBarController; +import com.android.launcher3.util.ComponentKey; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * LauncherCallbacks is an interface used to extend the Launcher activity. It includes many hooks @@ -37,11 +40,14 @@ public interface LauncherCallbacks { public void onPostCreate(Bundle savedInstanceState); public void onNewIntent(Intent intent); public void onActivityResult(int requestCode, int resultCode, Intent data); + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults); public void onWindowFocusChanged(boolean hasFocus); public boolean onPrepareOptionsMenu(Menu menu); public void dump(String prefix, FileDescriptor fd, PrintWriter w, String[] args); public void onHomeIntent(); public boolean handleBackPressed(); + public void onTrimMemory(int level); /* * Extension points for providing custom behavior on certain user interactions. @@ -52,6 +58,7 @@ 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); @@ -65,10 +72,12 @@ public interface LauncherCallbacks { /* * Extension points for replacing the search experience */ + @Deprecated public boolean forceDisableVoiceButtonProxy(); public boolean providesSearch(); public boolean startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds); + @Deprecated public void startVoice(); public boolean hasCustomContentToLeft(); public void populateCustomContentContainer(); @@ -83,9 +92,12 @@ public interface LauncherCallbacks { public View getIntroScreen(); public boolean shouldMoveToDefaultScreenOnHomeIntent(); public boolean hasSettings(); + @Deprecated public ComponentName getWallpaperPickerComponent(); public boolean overrideWallpaperDimensions(); public boolean isLauncherPreinstalled(); + public AllAppsSearchBarController getAllAppsSearchBarController(); + public List<ComponentKey> getPredictedApps(); /** * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup, @@ -105,4 +117,11 @@ public interface LauncherCallbacks { public Launcher.LauncherOverlay setLauncherOverlayView(InsettableFrameLayout container, Launcher.LauncherOverlayCallbacks callbacks); + /** + * Sets the callbacks to allow reacting the actions of search overlays of the launcher. + * + * @param callbacks A set of callbacks to the Launcher, is actually a LauncherSearchCallback, + * but for implementation purposes is passed around as an object. + */ + public void setLauncherSearchCallback(Object callbacks); } diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index ef8e8abcf..c13752cb1 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -20,10 +20,12 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.os.UserManager; import android.provider.Settings; @@ -34,6 +36,7 @@ import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.accessibility.AccessibilityManager; +import com.android.launcher3.util.Thunk; class LauncherClings implements OnClickListener { private static final String MIGRATION_CLING_DISMISSED_KEY = "cling_gel.migration.dismissed"; @@ -49,7 +52,7 @@ class LauncherClings implements OnClickListener { // New Secure Setting in L private static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints"; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private LayoutInflater mInflater; /** Ctor */ @@ -68,7 +71,7 @@ class LauncherClings implements OnClickListener { // Copy the shortcuts from the old database LauncherModel model = mLauncher.getModel(); model.resetLoadedState(false, true); - model.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + model.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); // Set the flag to skip the folder cling @@ -124,7 +127,7 @@ class LauncherClings implements OnClickListener { @Override public boolean onLongClick(View v) { - mLauncher.getWorkspace().enterOverviewMode(); + mLauncher.showOverviewMode(true); dismissLongPressCling(); return true; } @@ -174,7 +177,7 @@ class LauncherClings implements OnClickListener { }); } - private void dismissLongPressCling() { + @Thunk void dismissLongPressCling() { Runnable dismissCb = new Runnable() { public void run() { dismissCling(mLauncher.findViewById(R.id.longpress_cling), null, @@ -185,7 +188,7 @@ class LauncherClings implements OnClickListener { } /** Hides the specified Cling */ - private void dismissCling(final View cling, final Runnable postAnimationCb, + @Thunk void dismissCling(final View cling, final Runnable postAnimationCb, final String flag, int duration) { // To catch cases where siblings of top-level views are made invisible, just check whether // the cling is directly set to GONE before dismissing it. @@ -210,6 +213,7 @@ class LauncherClings implements OnClickListener { } /** Returns whether the clings are enabled or should be shown */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private boolean areClingsEnabled() { if (DISABLE_CLINGS) { return false; diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index fa053650f..c08cd0bf5 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -17,24 +17,30 @@ public class LauncherFiles { public static final String DEFAULT_WALLPAPER_THUMBNAIL = "default_thumb2.jpg"; public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg"; public static final String LAUNCHER_DB = "launcher.db"; - public static final String LAUNCHER_PREFERENCES = "launcher.preferences"; - public static final String LAUNCHES_LOG = "launches.log"; public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs"; - public static final String STATS_LOG = "stats.log"; public static final String WALLPAPER_CROP_PREFERENCES_KEY = - WallpaperCropActivity.class.getName(); + "com.android.launcher3.WallpaperCropActivity"; + public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs"; + public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db"; public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db"; + public static final String APP_ICONS_DB = "app_icons.db"; public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList( DEFAULT_WALLPAPER_THUMBNAIL, DEFAULT_WALLPAPER_THUMBNAIL_OLD, LAUNCHER_DB, - LAUNCHER_PREFERENCES, - LAUNCHES_LOG, SHARED_PREFERENCES_KEY + XML, - STATS_LOG, WALLPAPER_CROP_PREFERENCES_KEY + XML, WALLPAPER_IMAGES_DB, - WIDGET_PREVIEWS_DB)); + WIDGET_PREVIEWS_DB, + MANAGED_USER_PREFERENCES_KEY, + APP_ICONS_DB)); + + // TODO: Delete these files on upgrade + public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList( + "launches.log", + "stats.log", + "launcher.preferences", + "com.android.launcher3.compat.PackageInstallerCompatV16.queue")); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 954887dc3..b60477fa0 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -17,11 +17,9 @@ package com.android.launcher3; import android.app.SearchManager; -import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; -import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; @@ -29,27 +27,25 @@ import android.content.Context; import android.content.Intent; import android.content.Intent.ShortcutIconResource; import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.LauncherApps.Callback; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; -import android.content.res.Configuration; -import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Parcelable; import android.os.Process; -import android.os.RemoteException; import android.os.SystemClock; +import android.os.TransactionTooLargeException; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; +import android.util.LongSparseArray; import android.util.Pair; import com.android.launcher3.compat.AppWidgetManagerCompat; @@ -59,11 +55,16 @@ import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.CursorIconInfo; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; import java.net.URISyntaxException; import java.security.InvalidParameterException; -import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -75,7 +76,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; -import java.util.TreeMap; /** * Maintains in-memory state of the Launcher. It is expected that there should be only one @@ -87,13 +87,9 @@ public class LauncherModel extends BroadcastReceiver static final boolean DEBUG_LOADERS = false; private static final boolean DEBUG_RECEIVER = false; private static final boolean REMOVE_UNRESTORED_ICONS = true; - private static final boolean ADD_MANAGED_PROFILE_SHORTCUTS = false; static final String TAG = "Launcher.Model"; - // true = use a "More Apps" folder for non-workspace apps on upgrade - // false = strew non-workspace apps across the workspace on upgrade - public static final boolean UPGRADE_USE_MORE_APPS_FOLDER = false; public static final int LOADER_FLAG_NONE = 0; public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0; public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1; @@ -101,39 +97,29 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; - private final boolean mAppsCanBeOnRemoveableStorage; + @Thunk final boolean mAppsCanBeOnRemoveableStorage; private final boolean mOldContentProviderExists; - private final LauncherAppState mApp; - private final Object mLock = new Object(); - private DeferredHandler mHandler = new DeferredHandler(); - private LoaderTask mLoaderTask; - private boolean mIsLoaderTaskRunning; - private volatile boolean mFlushingWorkerThread; - - /** - * Maintain a set of packages per user, for which we added a shortcut on the workspace. - */ - private static final String INSTALLED_SHORTCUTS_SET_PREFIX = "installed_shortcuts_set_for_user_"; - - // Specific runnable types that are run on the main thread deferred handler, this allows us to - // clear all queued binding runnables when the Launcher activity is destroyed. - private static final int MAIN_THREAD_NORMAL_RUNNABLE = 0; - private static final int MAIN_THREAD_BINDING_RUNNABLE = 1; + @Thunk final LauncherAppState mApp; + @Thunk final Object mLock = new Object(); + @Thunk DeferredHandler mHandler = new DeferredHandler(); + @Thunk LoaderTask mLoaderTask; + @Thunk boolean mIsLoaderTaskRunning; + @Thunk boolean mHasLoaderCompletedOnce; private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings"; - private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); + @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); static { sWorkerThread.start(); } - private static final Handler sWorker = new Handler(sWorkerThread.getLooper()); + @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper()); // We start off with everything not loaded. After that, we assume that // our monitoring of the package manager provides all updates and we never // need to do a requery. These are only ever touched from the loader thread. - private boolean mWorkspaceLoaded; - private boolean mAllAppsLoaded; + @Thunk boolean mWorkspaceLoaded; + @Thunk boolean mAllAppsLoaded; // When we are loading pages synchronously, we can't just post the binding of items on the side // pages as this delays the rotation process. Instead, we wait for a callback from the first @@ -141,10 +127,18 @@ public class LauncherModel extends BroadcastReceiver // a normal load, we also clear this set of Runnables. static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>(); - private WeakReference<Callbacks> mCallbacks; + /** + * Set of runnables to be called on the background thread after the workspace binding + * is complete. + */ + static final ArrayList<Runnable> mBindCompleteRunnables = new ArrayList<Runnable>(); + + @Thunk WeakReference<Callbacks> mCallbacks; // < only access in worker thread > AllAppsList mBgAllAppsList; + // Entire list of widgets. + WidgetsModel mBgWidgetsModel; // The lock that must be acquired before referencing any static bg data structures. Unlike // other locks, this one can generally be held long-term because we never expect any of these @@ -154,7 +148,7 @@ public class LauncherModel extends BroadcastReceiver // sBgItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by // LauncherModel to their ids - static final HashMap<Long, ItemInfo> sBgItemsIdMap = new HashMap<Long, ItemInfo>(); + static final LongArrayMap<ItemInfo> sBgItemsIdMap = new LongArrayMap<>(); // sBgWorkspaceItems is passed to bindItems, which expects a list of all folders and shortcuts // created by LauncherModel that are directly on the home screen (however, no widgets or @@ -166,26 +160,24 @@ public class LauncherModel extends BroadcastReceiver new ArrayList<LauncherAppWidgetInfo>(); // sBgFolders is all FolderInfos created by LauncherModel. Passed to bindFolders() - static final HashMap<Long, FolderInfo> sBgFolders = new HashMap<Long, FolderInfo>(); - - // sBgDbIconCache is the set of ItemInfos that need to have their icons updated in the database - static final HashMap<Object, byte[]> sBgDbIconCache = new HashMap<Object, byte[]>(); + static final LongArrayMap<FolderInfo> sBgFolders = new LongArrayMap<>(); // sBgWorkspaceScreens is the ordered set of workspace screens. static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>(); + // sBgWidgetProviders is the set of widget providers including custom internal widgets + public static HashMap<ComponentKey, LauncherAppWidgetProviderInfo> sBgWidgetProviders; + // sPendingPackages is a set of packages which could be on sdcard and are not available yet static final HashMap<UserHandleCompat, HashSet<String>> sPendingPackages = new HashMap<UserHandleCompat, HashSet<String>>(); // </ only access in worker thread > - private IconCache mIconCache; - - protected int mPreviousConfigMcc; + @Thunk IconCache mIconCache; - private final LauncherAppsCompat mLauncherApps; - private final UserManagerCompat mUserManager; + @Thunk final LauncherAppsCompat mLauncherApps; + @Thunk final UserManagerCompat mUserManager; public interface Callbacks { public boolean setLoadOnResume(); @@ -195,8 +187,8 @@ public class LauncherModel extends BroadcastReceiver boolean forceAnimateIcons); public void bindScreens(ArrayList<Long> orderedScreenIds); public void bindAddScreens(ArrayList<Long> orderedScreenIds); - public void bindFolders(HashMap<Long,FolderInfo> folders); - public void finishBindingItems(boolean upgradePath); + public void bindFolders(LongArrayMap<FolderInfo> folders); + public void finishBindingItems(); public void bindAppWidget(LauncherAppWidgetInfo info); public void bindAllApplications(ArrayList<AppInfo> apps); public void bindAppsAdded(ArrayList<Long> newScreens, @@ -207,11 +199,10 @@ public class LauncherModel extends BroadcastReceiver public void bindShortcutsChanged(ArrayList<ShortcutInfo> updated, ArrayList<ShortcutInfo> removed, UserHandleCompat user); public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets); - public void updatePackageState(ArrayList<PackageInstallInfo> installInfo); - public void updatePackageBadge(String packageName); + public void bindRestoreItemsChange(HashSet<ItemInfo> updates); public void bindComponentsRemoved(ArrayList<String> packageNames, ArrayList<AppInfo> appInfos, UserHandleCompat user, int reason); - public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts); + public void bindAllPackages(WidgetsModel model); public void bindSearchablesChanged(); public boolean isAllAppsButtonRank(int rank); public void onPageBoundSynchronously(int page); @@ -246,21 +237,16 @@ public class LauncherModel extends BroadcastReceiver mApp = app; mBgAllAppsList = new AllAppsList(iconCache, appFilter); + mBgWidgetsModel = new WidgetsModel(context, iconCache, appFilter); mIconCache = iconCache; - final Resources res = context.getResources(); - Configuration config = res.getConfiguration(); - mPreviousConfigMcc = config.mcc; mLauncherApps = LauncherAppsCompat.getInstance(context); mUserManager = UserManagerCompat.getInstance(context); } /** Runs the specified runnable immediately if called from the main thread, otherwise it is * posted on the main thread handler. */ - private void runOnMainThread(Runnable r) { - runOnMainThread(r, 0); - } - private void runOnMainThread(Runnable r, int type) { + @Thunk void runOnMainThread(Runnable r) { if (sWorkerThread.getThreadId() == Process.myTid()) { // If we are on the worker thread, post onto the main handler mHandler.post(r); @@ -280,95 +266,127 @@ public class LauncherModel extends BroadcastReceiver } } + /** + * Runs the specified runnable after the loader is complete + */ + @Thunk void runAfterBindCompletes(Runnable r) { + if (isLoadingWorkspace() || !mHasLoaderCompletedOnce) { + synchronized (mBindCompleteRunnables) { + mBindCompleteRunnables.add(r); + } + } else { + runOnWorkerThread(r); + } + } + boolean canMigrateFromOldLauncherDb(Launcher launcher) { return mOldContentProviderExists && !launcher.isLauncherPreinstalled() ; } - static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> items, int[] xy, - long screen) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int xCount = (int) grid.numColumns; - final int yCount = (int) grid.numRows; - boolean[][] occupied = new boolean[xCount][yCount]; + public void setPackageState(final PackageInstallInfo installInfo) { + Runnable updateRunnable = new Runnable() { - int cellX, cellY, spanX, spanY; - for (int i = 0; i < items.size(); ++i) { - final ItemInfo item = items.get(i); - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { - if (item.screenId == screen) { - cellX = item.cellX; - cellY = item.cellY; - spanX = item.spanX; - spanY = item.spanY; - for (int x = cellX; 0 <= x && x < cellX + spanX && x < xCount; x++) { - for (int y = cellY; 0 <= y && y < cellY + spanY && y < yCount; y++) { - occupied[x][y] = true; - } + @Override + public void run() { + synchronized (sBgLock) { + final HashSet<ItemInfo> updates = new HashSet<>(); + + if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) { + // Ignore install success events as they are handled by Package add events. + return; } - } - } - } - return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied); - } - static Pair<Long, int[]> findNextAvailableIconSpace(Context context, String name, - Intent launchIntent, - int firstScreenIndex, - ArrayList<Long> workspaceScreens) { - // Lock on the app so that we don't try and get the items while apps are being added - LauncherAppState app = LauncherAppState.getInstance(); - LauncherModel model = app.getModel(); - boolean found = false; - synchronized (app) { - if (sWorkerThread.getThreadId() != Process.myTid()) { - // Flush the LauncherModel worker thread, so that if we just did another - // processInstallShortcut, we give it time for its shortcut to get added to the - // database (getItemsInLocalCoordinates reads the database) - model.flushWorkerThread(); - } - final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context); + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (si.isPromise() && (cn != null) + && installInfo.packageName.equals(cn.getPackageName())) { + si.setInstallProgress(installInfo.progress); - // Try adding to the workspace screens incrementally, starting at the default or center - // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1)) - firstScreenIndex = Math.min(firstScreenIndex, workspaceScreens.size()); - int count = workspaceScreens.size(); - for (int screen = firstScreenIndex; screen < count && !found; screen++) { - int[] tmpCoordinates = new int[2]; - if (findNextAvailableIconSpaceInScreen(items, tmpCoordinates, - workspaceScreens.get(screen))) { - // Update the Launcher db - return new Pair<Long, int[]>(workspaceScreens.get(screen), tmpCoordinates); - } - } - } - return null; - } + if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) { + // Mark this info as broken. + si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + } + updates.add(si); + } + } + } - public void setPackageState(final ArrayList<PackageInstallInfo> installInfo) { - // Process the updated package state - Runnable r = new Runnable() { - public void run() { - Callbacks callbacks = getCallback(); - if (callbacks != null) { - callbacks.updatePackageState(installInfo); + for (LauncherAppWidgetInfo widget : sBgAppWidgets) { + if (widget.providerName.getPackageName().equals(installInfo.packageName)) { + widget.installProgress = installInfo.progress; + updates.add(widget); + } + } + + if (!updates.isEmpty()) { + // Push changes to the callback. + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = getCallback(); + if (callbacks != null) { + callbacks.bindRestoreItemsChange(updates); + } + } + }; + mHandler.post(r); + } } } }; - mHandler.post(r); + runOnWorkerThread(updateRunnable); } - public void updatePackageBadge(final String packageName) { - // Process the updated package badge - Runnable r = new Runnable() { + /** + * Updates the icons and label of all pending icons for the provided package name. + */ + public void updateSessionDisplayInfo(final String packageName) { + Runnable updateRunnable = new Runnable() { + + @Override public void run() { - Callbacks callbacks = getCallback(); - if (callbacks != null) { - callbacks.updatePackageBadge(packageName); + synchronized (sBgLock) { + final ArrayList<ShortcutInfo> updates = new ArrayList<>(); + final UserHandleCompat user = UserHandleCompat.myUserHandle(); + + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (si.isPromise() && (cn != null) + && packageName.equals(cn.getPackageName())) { + if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { + // For auto install apps update the icon as well as label. + mIconCache.getTitleAndIcon(si, + si.promisedIntent, user, + si.shouldUseLowResIcon()); + } else { + // Only update the icon for restored apps. + si.updateIcon(mIconCache); + } + updates.add(si); + } + } + } + + if (!updates.isEmpty()) { + // Push changes to the callback. + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = getCallback(); + if (callbacks != null) { + callbacks.bindShortcutsChanged(updates, + new ArrayList<ShortcutInfo>(), user); + } + } + }; + mHandler.post(r); + } } } }; - mHandler.post(r); + runOnWorkerThread(updateRunnable); } public void addAppsToAllApps(final Context ctx, final ArrayList<AppInfo> allAppsApps) { @@ -397,13 +415,103 @@ public class LauncherModel extends BroadcastReceiver runOnWorkerThread(r); } - public void addAndBindAddedWorkspaceApps(final Context context, - final ArrayList<ItemInfo> workspaceApps) { - final Callbacks callbacks = getCallback(); + private static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos, + int[] xy, int spanX, int spanY) { + LauncherAppState app = LauncherAppState.getInstance(); + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int xCount = (int) profile.numColumns; + final int yCount = (int) profile.numRows; + boolean[][] occupied = new boolean[xCount][yCount]; + if (occupiedPos != null) { + for (ItemInfo r : occupiedPos) { + int right = r.cellX + r.spanX; + int bottom = r.cellY + r.spanY; + for (int x = r.cellX; 0 <= x && x < right && x < xCount; x++) { + for (int y = r.cellY; 0 <= y && y < bottom && y < yCount; y++) { + occupied[x][y] = true; + } + } + } + } + return Utilities.findVacantCell(xy, spanX, spanY, xCount, yCount, occupied); + } - if (workspaceApps == null) { - throw new RuntimeException("workspaceApps and allAppsApps must not be null"); + /** + * Find a position on the screen for the given size or adds a new screen. + * @return screenId and the coordinates for the item. + */ + @Thunk Pair<Long, int[]> findSpaceForItem( + Context context, + ArrayList<Long> workspaceScreens, + ArrayList<Long> addedWorkspaceScreensFinal, + int spanX, int spanY) { + LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>(); + + // Use sBgItemsIdMap as all the items are already loaded. + assertWorkspaceLoaded(); + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + ArrayList<ItemInfo> items = screenItems.get(info.screenId); + if (items == null) { + items = new ArrayList<>(); + screenItems.put(info.screenId, items); + } + items.add(info); + } + } } + + // Find appropriate space for the item. + long screenId = 0; + int[] cordinates = new int[2]; + boolean found = false; + + int screenCount = workspaceScreens.size(); + // First check the preferred screen. + int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1; + if (preferredScreenIndex < screenCount) { + screenId = workspaceScreens.get(preferredScreenIndex); + found = findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY); + } + + if (!found) { + // Search on any of the screens starting from the first screen. + for (int screen = 1; screen < screenCount; screen++) { + screenId = workspaceScreens.get(screen); + if (findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY)) { + // We found a space for it + found = true; + break; + } + } + } + + if (!found) { + // Still no position found. Add a new screen to the end. + screenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); + + // Save the screen id for binding in the workspace + workspaceScreens.add(screenId); + addedWorkspaceScreensFinal.add(screenId); + + // If we still can't find an empty space, then God help us all!!! + if (!findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY)) { + throw new RuntimeException("Can't find space to add the item"); + } + } + return Pair.create(screenId, cordinates); + } + + /** + * Adds the provided items to the workspace. + */ + public void addAndBindAddedWorkspaceItems(final Context context, + final ArrayList<? extends ItemInfo> workspaceApps) { + final Callbacks callbacks = getCallback(); if (workspaceApps.isEmpty()) { return; } @@ -416,71 +524,38 @@ public class LauncherModel extends BroadcastReceiver // Get the list of workspace screens. We need to append to this list and // can not use sBgWorkspaceScreens because loadWorkspace() may not have been // called. - ArrayList<Long> workspaceScreens = new ArrayList<Long>(); - TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(context); - for (Integer i : orderedScreens.keySet()) { - long screenId = orderedScreens.get(i); - workspaceScreens.add(screenId); - } - + ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context); synchronized(sBgLock) { - Iterator<ItemInfo> iter = workspaceApps.iterator(); - while (iter.hasNext()) { - ItemInfo a = iter.next(); - final String name = a.title.toString(); - final Intent launchIntent = a.getIntent(); - - // Short-circuit this logic if the icon exists somewhere on the workspace - if (shortcutExists(context, name, launchIntent, a.user)) { - continue; - } - - // Add this icon to the db, creating a new page if necessary. If there - // is only the empty page then we just add items to the first page. - // Otherwise, we add them to the next pages. - int startSearchPageIndex = workspaceScreens.isEmpty() ? 0 : 1; - Pair<Long, int[]> coords = LauncherModel.findNextAvailableIconSpace(context, - name, launchIntent, startSearchPageIndex, workspaceScreens); - if (coords == null) { - LauncherProvider lp = LauncherAppState.getLauncherProvider(); - - // If we can't find a valid position, then just add a new screen. - // This takes time so we need to re-queue the add until the new - // page is added. Create as many screens as necessary to satisfy - // the startSearchPageIndex. - int numPagesToAdd = Math.max(1, startSearchPageIndex + 1 - - workspaceScreens.size()); - while (numPagesToAdd > 0) { - long screenId = lp.generateNewScreenId(); - // Save the screen id for binding in the workspace - workspaceScreens.add(screenId); - addedWorkspaceScreensFinal.add(screenId); - numPagesToAdd--; + for (ItemInfo item : workspaceApps) { + if (item instanceof ShortcutInfo) { + // Short-circuit this logic if the icon exists somewhere on the workspace + if (shortcutExists(context, item.getIntent(), item.user)) { + continue; } - - // Find the coordinate again - coords = LauncherModel.findNextAvailableIconSpace(context, - name, launchIntent, startSearchPageIndex, workspaceScreens); - } - if (coords == null) { - throw new RuntimeException("Coordinates should not be null"); } - ShortcutInfo shortcutInfo; - if (a instanceof ShortcutInfo) { - shortcutInfo = (ShortcutInfo) a; - } else if (a instanceof AppInfo) { - shortcutInfo = ((AppInfo) a).makeShortcut(); + // Find appropriate space for the item. + Pair<Long, int[]> coords = findSpaceForItem(context, + workspaceScreens, addedWorkspaceScreensFinal, + 1, 1); + long screenId = coords.first; + int[] cordinates = coords.second; + + ItemInfo itemInfo; + if (item instanceof ShortcutInfo || item instanceof FolderInfo) { + itemInfo = item; + } else if (item instanceof AppInfo) { + itemInfo = ((AppInfo) item).makeShortcut(); } else { throw new RuntimeException("Unexpected info type"); } // Add the shortcut to the db - addItemToDatabase(context, shortcutInfo, + addItemToDatabase(context, itemInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - coords.first, coords.second[0], coords.second[1], false); + screenId, cordinates[0], cordinates[1]); // Save the ShortcutInfo for binding in the workspace - addedShortcutsFinal.add(shortcutInfo); + addedShortcutsFinal.add(itemInfo); } } @@ -516,7 +591,7 @@ public class LauncherModel extends BroadcastReceiver runOnWorkerThread(r); } - public void unbindItemInfosAndClearQueuedBindRunnables() { + private void unbindItemInfosAndClearQueuedBindRunnables() { if (sWorkerThread.getThreadId() == Process.myTid()) { throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " + "main thread"); @@ -526,8 +601,9 @@ public class LauncherModel extends BroadcastReceiver synchronized (mDeferredBindRunnables) { mDeferredBindRunnables.clear(); } - // Remove any queued bind runnables - mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE); + + // Remove any queued UI runnables + mHandler.cancelAll(); // Unbind all the workspace items unbindWorkspaceItemsOnMainThread(); } @@ -536,19 +612,15 @@ public class LauncherModel extends BroadcastReceiver void unbindWorkspaceItemsOnMainThread() { // Ensure that we don't use the same workspace items data structure on the main thread // by making a copy of workspace items first. - final ArrayList<ItemInfo> tmpWorkspaceItems = new ArrayList<ItemInfo>(); - final ArrayList<ItemInfo> tmpAppWidgets = new ArrayList<ItemInfo>(); + final ArrayList<ItemInfo> tmpItems = new ArrayList<ItemInfo>(); synchronized (sBgLock) { - tmpWorkspaceItems.addAll(sBgWorkspaceItems); - tmpAppWidgets.addAll(sBgAppWidgets); + tmpItems.addAll(sBgWorkspaceItems); + tmpItems.addAll(sBgAppWidgets); } Runnable r = new Runnable() { @Override public void run() { - for (ItemInfo item : tmpWorkspaceItems) { - item.unbind(); - } - for (ItemInfo item : tmpAppWidgets) { + for (ItemInfo item : tmpItems) { item.unbind(); } } @@ -564,7 +636,7 @@ public class LauncherModel extends BroadcastReceiver long screenId, int cellX, int cellY) { if (item.container == ItemInfo.NO_ID) { // From all apps - addItemToDatabase(context, item, container, screenId, cellX, cellY, false); + addItemToDatabase(context, item, container, screenId, cellX, cellY); } else { // From somewhere else moveItemInDatabase(context, item, container, screenId, cellX, cellY); @@ -630,7 +702,7 @@ public class LauncherModel extends BroadcastReceiver static void updateItemInDatabaseHelper(Context context, final ContentValues values, final ItemInfo item, final String callingFunction) { final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); final ContentResolver cr = context.getContentResolver(); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); @@ -656,7 +728,7 @@ public class LauncherModel extends BroadcastReceiver for (int i = 0; i < count; i++) { ItemInfo item = items.get(i); final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); ContentValues values = valuesList.get(i); ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); @@ -714,39 +786,10 @@ public class LauncherModel extends BroadcastReceiver } } - public void flushWorkerThread() { - mFlushingWorkerThread = true; - Runnable waiter = new Runnable() { - public void run() { - synchronized (this) { - notifyAll(); - mFlushingWorkerThread = false; - } - } - }; - - synchronized(waiter) { - runOnWorkerThread(waiter); - if (mLoaderTask != null) { - synchronized(mLoaderTask) { - mLoaderTask.notify(); - } - } - boolean success = false; - while (!success) { - try { - waiter.wait(); - success = true; - } catch (InterruptedException e) { - } - } - } - } - /** * Move an item in the DB to a new <container, screen, cellX, cellY> */ - static void moveItemInDatabase(Context context, final ItemInfo item, final long container, + public static void moveItemInDatabase(Context context, final ItemInfo item, final long container, final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; @@ -765,6 +808,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase"); @@ -798,6 +842,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); contentValues.add(values); @@ -829,6 +874,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SPANX, item.spanX); values.put(LauncherSettings.Favorites.SPANY, item.spanY); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); @@ -839,104 +885,63 @@ public class LauncherModel extends BroadcastReceiver /** * Update an item to the database in a specified container. */ - static void updateItemInDatabase(Context context, final ItemInfo item) { + public static void updateItemInDatabase(Context context, final ItemInfo item) { final ContentValues values = new ContentValues(); item.onAddToDatabase(context, values); - item.updateValuesWithCoordinates(values, item.cellX, item.cellY); updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase"); } + private void assertWorkspaceLoaded() { + if (LauncherAppState.isDogfoodBuild() && (isLoadingWorkspace() || !mHasLoaderCompletedOnce)) { + throw new RuntimeException("Trying to add shortcut while loader is running"); + } + } + /** - * Returns true if the shortcuts already exists in the database. - * we identify a shortcut by its title and intent. + * Returns true if the shortcuts already exists on the workspace. This must be called after + * the workspace has been loaded. We identify a shortcut by its intent. */ - static boolean shortcutExists(Context context, String title, Intent intent, - UserHandleCompat user) { - final ContentResolver cr = context.getContentResolver(); - final Intent intentWithPkg, intentWithoutPkg; - + @Thunk boolean shortcutExists(Context context, Intent intent, UserHandleCompat user) { + assertWorkspaceLoaded(); + final String intentWithPkg, intentWithoutPkg; if (intent.getComponent() != null) { // If component is not null, an intent with null package will produce // the same result and should also be a match. + String packageName = intent.getComponent().getPackageName(); if (intent.getPackage() != null) { - intentWithPkg = intent; - intentWithoutPkg = new Intent(intent).setPackage(null); + intentWithPkg = intent.toUri(0); + intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0); } else { - intentWithPkg = new Intent(intent).setPackage( - intent.getComponent().getPackageName()); - intentWithoutPkg = intent; + intentWithPkg = new Intent(intent).setPackage(packageName).toUri(0); + intentWithoutPkg = intent.toUri(0); } } else { - intentWithPkg = intent; - intentWithoutPkg = intent; - } - String userSerial = Long.toString(UserManagerCompat.getInstance(context) - .getSerialNumberForUser(user)); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { "title", "intent", "profileId" }, - "title=? and (intent=? or intent=?) and profileId=?", - new String[] { title, intentWithPkg.toUri(0), intentWithoutPkg.toUri(0), userSerial }, - null); - try { - return c.moveToFirst(); - } finally { - c.close(); + intentWithPkg = intent.toUri(0); + intentWithoutPkg = intent.toUri(0); } - } - /** - * Returns an ItemInfo array containing all the items in the LauncherModel. - * The ItemInfo.id is not set through this function. - */ - static ArrayList<ItemInfo> getItemsInLocalCoordinates(Context context) { - ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, new String[] { - LauncherSettings.Favorites.ITEM_TYPE, LauncherSettings.Favorites.CONTAINER, - LauncherSettings.Favorites.SCREEN, - LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, - LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY, - LauncherSettings.Favorites.PROFILE_ID }, null, null, null); - - final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); - final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); - final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); - final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); - final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); - final int profileIdIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID); - UserManagerCompat userManager = UserManagerCompat.getInstance(context); - try { - while (c.moveToNext()) { - ItemInfo item = new ItemInfo(); - item.cellX = c.getInt(cellXIndex); - item.cellY = c.getInt(cellYIndex); - item.spanX = Math.max(1, c.getInt(spanXIndex)); - item.spanY = Math.max(1, c.getInt(spanYIndex)); - item.container = c.getInt(containerIndex); - item.itemType = c.getInt(itemTypeIndex); - item.screenId = c.getInt(screenIndex); - long serialNumber = c.getInt(profileIdIndex); - item.user = userManager.getUserForSerialNumber(serialNumber); - // Skip if user has been deleted. - if (item.user != null) { - items.add(item); + synchronized (sBgLock) { + for (ItemInfo item : sBgItemsIdMap) { + if (item instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) item; + Intent targetIntent = info.promisedIntent == null + ? info.intent : info.promisedIntent; + if (targetIntent != null && info.user.equals(user)) { + String s = targetIntent.toUri(0); + if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) { + return true; + } + } } } - } catch (Exception e) { - items.clear(); - } finally { - c.close(); } - - return items; + return false; } /** * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. */ - FolderInfo getFolderById(Context context, HashMap<Long,FolderInfo> folderList, long id) { + FolderInfo getFolderById(Context context, LongArrayMap<FolderInfo> folderList, long id) { final ContentResolver cr = context.getContentResolver(); Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null, "_id=? and (itemType=? or itemType=?)", @@ -951,6 +956,7 @@ public class LauncherModel extends BroadcastReceiver final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS); FolderInfo folderInfo = null; switch (c.getInt(itemTypeIndex)) { @@ -959,12 +965,14 @@ public class LauncherModel extends BroadcastReceiver break; } + // Do not trim the folder label, as is was set by the user. folderInfo.title = c.getString(titleIndex); folderInfo.id = id; folderInfo.container = c.getInt(containerIndex); folderInfo.screenId = c.getInt(screenIndex); folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); + folderInfo.options = c.getInt(optionsIndex); return folderInfo; } @@ -979,8 +987,8 @@ public class LauncherModel extends BroadcastReceiver * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ - static void addItemToDatabase(Context context, final ItemInfo item, final long container, - final long screenId, final int cellX, final int cellY, final boolean notify) { + public static void addItemToDatabase(Context context, final ItemInfo item, final long container, + final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; item.cellY = cellY; @@ -999,13 +1007,11 @@ public class LauncherModel extends BroadcastReceiver item.id = LauncherAppState.getLauncherProvider().generateNewItemId(); values.put(LauncherSettings.Favorites._ID, item.id); - item.updateValuesWithCoordinates(values, item.cellX, item.cellY); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { - cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : - LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(LauncherSettings.Favorites.CONTENT_URI, values); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { @@ -1056,7 +1062,7 @@ public class LauncherModel extends BroadcastReceiver return cn.getPackageName().equals(pn) && info.user.equals(user); } }; - return filterItemInfos(sBgItemsIdMap.values(), filter); + return filterItemInfos(sBgItemsIdMap, filter); } /** @@ -1072,7 +1078,7 @@ public class LauncherModel extends BroadcastReceiver * @param context * @param item */ - static void deleteItemFromDatabase(Context context, final ItemInfo item) { + public static void deleteItemFromDatabase(Context context, final ItemInfo item) { ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); items.add(item); deleteItemsFromDatabase(context, items); @@ -1085,11 +1091,10 @@ public class LauncherModel extends BroadcastReceiver */ static void deleteItemsFromDatabase(Context context, final ArrayList<? extends ItemInfo> items) { final ContentResolver cr = context.getContentResolver(); - Runnable r = new Runnable() { public void run() { for (ItemInfo item : items) { - final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(item.id); cr.delete(uri, null, null); // Lock on mBgLock *after* the db operation @@ -1097,7 +1102,7 @@ public class LauncherModel extends BroadcastReceiver switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: sBgFolders.remove(item.id); - for (ItemInfo info: sBgItemsIdMap.values()) { + for (ItemInfo info: sBgItemsIdMap) { if (info.container == item.id) { // We are deleting a folder which still contains items that // think they are contained by that folder. @@ -1117,7 +1122,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.remove(item.id); - sBgDbIconCache.remove(item); } } } @@ -1130,10 +1134,6 @@ public class LauncherModel extends BroadcastReceiver * a list of screen ids in the order that they should appear. */ void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - updateWorkspaceScreenOrder()", true); - Launcher.addDumpLog(TAG, "11683562 - screens: " + TextUtils.join(", ", screens), true); - final ArrayList<Long> screensCopy = new ArrayList<Long>(screens); final ContentResolver cr = context.getContentResolver(); final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -1180,27 +1180,25 @@ public class LauncherModel extends BroadcastReceiver /** * Remove the contents of the specified folder from the database */ - static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { + public static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { public void run() { - cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null); + cr.delete(LauncherSettings.Favorites.getContentUri(info.id), null, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { sBgItemsIdMap.remove(info.id); sBgFolders.remove(info.id); - sBgDbIconCache.remove(info); sBgWorkspaceItems.remove(info); } - cr.delete(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, + cr.delete(LauncherSettings.Favorites.CONTENT_URI, LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { for (ItemInfo childInfo : info.contents) { sBgItemsIdMap.remove(childInfo.id); - sBgDbIconCache.remove(childInfo); } } } @@ -1213,6 +1211,9 @@ public class LauncherModel extends BroadcastReceiver */ public void initialize(Callbacks callbacks) { synchronized (mLock) { + // Disconnect any of the callbacks and drawables associated with ItemInfos on the + // workspace to prevent leaking Launcher activities on orientation change. + unbindItemInfosAndClearQueuedBindRunnables(); mCallbacks = new WeakReference<Callbacks>(callbacks); } } @@ -1265,7 +1266,6 @@ public class LauncherModel extends BroadcastReceiver PackageUpdatedTask.OP_UNAVAILABLE, packageNames, user)); } - } /** @@ -1280,24 +1280,15 @@ public class LauncherModel extends BroadcastReceiver if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { // If we have changed locale we need to clear out the labels in all apps/workspace. forceReload(); - } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { - // Check if configuration change was an mcc/mnc change which would affect app resources - // and we would need to clear out the labels in all apps/workspace. Same handling as - // above for ACTION_LOCALE_CHANGED - Configuration currentConfig = context.getResources().getConfiguration(); - if (mPreviousConfigMcc != currentConfig.mcc) { - Log.d(TAG, "Reload apps on config change. curr_mcc:" - + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc); - forceReload(); - } - // Update previousConfig - mPreviousConfigMcc = currentConfig.mcc; } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) || SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) { Callbacks callbacks = getCallback(); if (callbacks != null) { callbacks.bindSearchablesChanged(); } + } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) + || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + forceReload(); } } @@ -1336,38 +1327,32 @@ public class LauncherModel extends BroadcastReceiver } } if (runLoader) { - startLoader(false, PagedView.INVALID_RESTORE_PAGE); + startLoader(PagedView.INVALID_RESTORE_PAGE); } } - // If there is already a loader task running, tell it to stop. - // returns true if isLaunching() was true on the old task - private boolean stopLoaderLocked() { - boolean isLaunching = false; + /** + * If there is already a loader task running, tell it to stop. + */ + private void stopLoaderLocked() { LoaderTask oldTask = mLoaderTask; if (oldTask != null) { - if (oldTask.isLaunching()) { - isLaunching = true; - } oldTask.stopLocked(); } - return isLaunching; } public boolean isCurrentCallbacks(Callbacks callbacks) { return (mCallbacks != null && mCallbacks.get() == callbacks); } - public void startLoader(boolean isLaunching, int synchronousBindPage) { - startLoader(isLaunching, synchronousBindPage, LOADER_FLAG_NONE); + public void startLoader(int synchronousBindPage) { + startLoader(synchronousBindPage, LOADER_FLAG_NONE); } - public void startLoader(boolean isLaunching, int synchronousBindPage, int loadFlags) { + public void startLoader(int synchronousBindPage, int loadFlags) { + // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems + InstallShortcutReceiver.enableInstallQueue(); synchronized (mLock) { - if (DEBUG_LOADERS) { - Log.d(TAG, "startLoader isLaunching=" + isLaunching); - } - // Clear any deferred bind-runnables from the synchronized load process // We must do this before any loading/binding is scheduled below. synchronized (mDeferredBindRunnables) { @@ -1377,11 +1362,10 @@ public class LauncherModel extends BroadcastReceiver // Don't bother to start the thread if we know it's not going to do anything if (mCallbacks != null && mCallbacks.get() != null) { // If there is already one running, tell it to stop. - // also, don't downgrade isLaunching if we're already running - isLaunching = isLaunching || stopLoaderLocked(); - mLoaderTask = new LoaderTask(mApp.getContext(), isLaunching, loadFlags); + stopLoaderLocked(); + mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags); if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE - && mAllAppsLoaded && mWorkspaceLoaded) { + && mAllAppsLoaded && mWorkspaceLoaded && !mIsLoaderTaskRunning) { mLoaderTask.runBindSynchronousPage(synchronousBindPage); } else { sWorkerThread.setPriority(Thread.NORM_PRIORITY); @@ -1401,7 +1385,17 @@ public class LauncherModel extends BroadcastReceiver mDeferredBindRunnables.clear(); } for (final Runnable r : deferredBindRunnables) { - mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE); + mHandler.post(r); + } + } + + // Run all the bind complete runnables after workspace is bound. + if (!mBindCompleteRunnables.isEmpty()) { + synchronized (mBindCompleteRunnables) { + for (final Runnable r : mBindCompleteRunnables) { + runOnWorkerThread(r); + } + mBindCompleteRunnables.clear(); } } } @@ -1414,40 +1408,31 @@ public class LauncherModel extends BroadcastReceiver } } - /** Loads the workspace screens db into a map of Rank -> ScreenId */ - private static TreeMap<Integer, Long> loadWorkspaceScreensDb(Context context) { + /** + * Loads the workspace screen ids in an ordered list. + */ + @Thunk static ArrayList<Long> loadWorkspaceScreensDb(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI; - final Cursor sc = contentResolver.query(screensUri, null, null, null, null); - TreeMap<Integer, Long> orderedScreens = new TreeMap<Integer, Long>(); + // Get screens ordered by rank. + final Cursor sc = contentResolver.query(screensUri, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK); + ArrayList<Long> screenIds = new ArrayList<Long>(); try { - final int idIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens._ID); - final int rankIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens.SCREEN_RANK); + final int idIndex = sc.getColumnIndexOrThrow(LauncherSettings.WorkspaceScreens._ID); while (sc.moveToNext()) { try { - long screenId = sc.getLong(idIndex); - int rank = sc.getInt(rankIndex); - orderedScreens.put(rank, screenId); + screenIds.add(sc.getLong(idIndex)); } catch (Exception e) { - Launcher.addDumpLog(TAG, "Desktop items loading interrupted - invalid screens: " + e, true); + Launcher.addDumpLog(TAG, "Desktop items loading interrupted" + + " - invalid screens: " + e, true); } } } finally { sc.close(); } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadWorkspaceScreensDb()", true); - ArrayList<String> orderedScreensPairs= new ArrayList<String>(); - for (Integer i : orderedScreens.keySet()) { - orderedScreensPairs.add("{ " + i + ": " + orderedScreens.get(i) + " }"); - } - Launcher.addDumpLog(TAG, "11683562 - screens: " + - TextUtils.join(", ", orderedScreensPairs), true); - return orderedScreens; + return screenIds; } public boolean isAllAppsLoaded() { @@ -1471,31 +1456,21 @@ public class LauncherModel extends BroadcastReceiver */ private class LoaderTask implements Runnable { private Context mContext; - private boolean mIsLaunching; - private boolean mIsLoadingAndBindingWorkspace; + @Thunk boolean mIsLoadingAndBindingWorkspace; private boolean mStopped; - private boolean mLoadAndBindStepFinished; + @Thunk boolean mLoadAndBindStepFinished; private int mFlags; - private HashMap<Object, CharSequence> mLabelCache; - - LoaderTask(Context context, boolean isLaunching, int flags) { + LoaderTask(Context context, int flags) { mContext = context; - mIsLaunching = isLaunching; - mLabelCache = new HashMap<Object, CharSequence>(); mFlags = flags; } - boolean isLaunching() { - return mIsLaunching; - } - boolean isLoadingWorkspace() { return mIsLoadingAndBindingWorkspace; } - /** Returns whether this is an upgrade path */ - private boolean loadAndBindWorkspace() { + private void loadAndBindWorkspace() { mIsLoadingAndBindingWorkspace = true; // Load the workspace @@ -1503,20 +1478,18 @@ public class LauncherModel extends BroadcastReceiver Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); } - boolean isUpgradePath = false; if (!mWorkspaceLoaded) { - isUpgradePath = loadWorkspace(); + loadWorkspace(); synchronized (LoaderTask.this) { if (mStopped) { - return isUpgradePath; + return; } mWorkspaceLoaded = true; } } // Bind the workspace - bindWorkspace(-1, isUpgradePath); - return isUpgradePath; + bindWorkspace(-1); } private void waitForIdle() { @@ -1538,7 +1511,7 @@ public class LauncherModel extends BroadcastReceiver } }); - while (!mStopped && !mLoadAndBindStepFinished && !mFlushingWorkerThread) { + while (!mStopped && !mLoadAndBindStepFinished) { try { // Just in case mFlushingWorkerThread changes but we aren't woken up, // wait no longer than 1sec at a time @@ -1585,72 +1558,35 @@ public class LauncherModel extends BroadcastReceiver // Divide the set of loaded items into those that we are binding synchronously, and // everything else that is to be bound normally (asynchronously). - bindWorkspace(synchronousBindPage, false); + bindWorkspace(synchronousBindPage); // XXX: For now, continue posting the binding of AllApps as there are other issues that // arise from that. onlyBindAllApps(); } public void run() { - boolean isUpgrade = false; - synchronized (mLock) { + if (mStopped) { + return; + } mIsLoaderTaskRunning = true; } // Optimize for end-user experience: if the Launcher is up and // running with the // All Apps interface in the foreground, load All Apps first. Otherwise, load the // workspace first (default). keep_running: { - // Elevate priority when Home launches for the first time to avoid - // starving at boot time. Staring at a blank home is not cool. - synchronized (mLock) { - if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to " + - (mIsLaunching ? "DEFAULT" : "BACKGROUND")); - android.os.Process.setThreadPriority(mIsLaunching - ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); - } if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); - isUpgrade = loadAndBindWorkspace(); + loadAndBindWorkspace(); if (mStopped) { break keep_running; } - // Whew! Hard work done. Slow us down, and wait until the UI thread has - // settled down. - synchronized (mLock) { - if (mIsLaunching) { - if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to BACKGROUND"); - android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - } - } waitForIdle(); // second step if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); loadAndBindAllApps(); - - // Restore the default thread priority after we are done loading items - synchronized (mLock) { - android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); - } - } - - // Update the saved icons if necessary - if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons"); - synchronized (sBgLock) { - for (Object key : sBgDbIconCache.keySet()) { - updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key)); - } - sBgDbIconCache.clear(); - } - - if (LauncherAppState.isDisableAllApps()) { - // Ensure that all the applications that are in the system are - // represented on the home screen. - if (!UPGRADE_USE_MORE_APPS_FOLDER || !isUpgrade) { - verifyApplications(); - } } // Clear out this reference, otherwise we end up holding it until all of the @@ -1663,6 +1599,7 @@ public class LauncherModel extends BroadcastReceiver mLoaderTask = null; } mIsLoaderTaskRunning = false; + mHasLoaderCompletedOnce = true; } } @@ -1703,34 +1640,12 @@ public class LauncherModel extends BroadcastReceiver } } - private void verifyApplications() { - final Context context = mApp.getContext(); - - // Cross reference all the applications in our apps list with items in the workspace - ArrayList<ItemInfo> tmpInfos; - ArrayList<ItemInfo> added = new ArrayList<ItemInfo>(); - synchronized (sBgLock) { - for (AppInfo app : mBgAllAppsList.data) { - tmpInfos = getItemInfoForComponentName(app.componentName, app.user); - if (tmpInfos.isEmpty()) { - // We are missing an application icon, so add this to the workspace - added.add(app); - // This is a rare event, so lets log it - Log.e(TAG, "Missing Application on load: " + app); - } - } - } - if (!added.isEmpty()) { - addAndBindAddedWorkspaceApps(context, added); - } - } - // check & update map of what's occupied; used to discard overlapping/invalid items - private boolean checkItemPlacement(HashMap<Long, ItemInfo[][]> occupied, ItemInfo item) { + private boolean checkItemPlacement(LongArrayMap<ItemInfo[][]> occupied, ItemInfo item) { LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int countX = (int) grid.numColumns; - final int countY = (int) grid.numRows; + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int countX = (int) profile.numColumns; + final int countY = (int) profile.numRows; long containerIndex = item.screenId; if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { @@ -1746,10 +1661,10 @@ public class LauncherModel extends BroadcastReceiver final ItemInfo[][] hotseatItems = occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT); - if (item.screenId >= grid.numHotseatIcons) { + if (item.screenId >= profile.numHotseatIcons) { Log.e(TAG, "Error loading shortcut " + item + " into hotseat position " + item.screenId - + ", position out of bounds: (0 to " + (grid.numHotseatIcons - 1) + + ", position out of bounds: (0 to " + (profile.numHotseatIcons - 1) + ")"); return false; } @@ -1767,7 +1682,7 @@ public class LauncherModel extends BroadcastReceiver return true; } } else { - final ItemInfo[][] items = new ItemInfo[(int) grid.numHotseatIcons][1]; + final ItemInfo[][] items = new ItemInfo[(int) profile.numHotseatIcons][1]; items[(int) item.screenId][0] = item; occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items); return true; @@ -1822,31 +1737,25 @@ public class LauncherModel extends BroadcastReceiver sBgAppWidgets.clear(); sBgFolders.clear(); sBgItemsIdMap.clear(); - sBgDbIconCache.clear(); sBgWorkspaceScreens.clear(); } } - /** Returns whether this is an upgrade path */ - private boolean loadWorkspace() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadWorkspace()", true); - + private void loadWorkspace() { final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; final Context context = mContext; final ContentResolver contentResolver = context.getContentResolver(); final PackageManager manager = context.getPackageManager(); - final AppWidgetManager widgets = AppWidgetManager.getInstance(context); final boolean isSafeMode = manager.isSafeMode(); final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); final boolean isSdCardReady = context.registerReceiver(null, new IntentFilter(StartupReceiver.SYSTEM_READY)) != null; LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - int countX = (int) grid.numColumns; - int countY = (int) grid.numRows; + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + int countX = (int) profile.numColumns; + int countY = (int) profile.numRows; if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) { Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true); @@ -1863,27 +1772,21 @@ public class LauncherModel extends BroadcastReceiver LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(); } - // This code path is for our old migration code and should no longer be exercised - boolean loadedOldDb = false; - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadedOldDb: " + loadedOldDb, true); - synchronized (sBgLock) { clearSBgDataStructures(); - final HashSet<String> installingPkgs = PackageInstallerCompat + final HashMap<String, Integer> installingPkgs = PackageInstallerCompat .getInstance(mContext).updateAndGetActiveSessionCache(); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final ArrayList<Long> restoredRows = new ArrayList<Long>(); - final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION; + final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI; if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri); final Cursor c = contentResolver.query(contentUri, null, null, null, null); // +1 for the hotseat (it can be larger than the workspace) // Load workspace in reverse order to ensure that latest items are loaded first (and // before any earlier duplicates) - final HashMap<Long, ItemInfo[][]> occupied = new HashMap<Long, ItemInfo[][]>(); + final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>(); try { final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); @@ -1891,13 +1794,6 @@ public class LauncherModel extends BroadcastReceiver (LauncherSettings.Favorites.INTENT); final int titleIndex = c.getColumnIndexOrThrow (LauncherSettings.Favorites.TITLE); - final int iconTypeIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_TYPE); - final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); - final int iconPackageIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_PACKAGE); - final int iconResourceIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_RESOURCE); final int containerIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.CONTAINER); final int itemTypeIndex = c.getColumnIndexOrThrow( @@ -1916,19 +1812,27 @@ public class LauncherModel extends BroadcastReceiver (LauncherSettings.Favorites.SPANX); final int spanYIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.SPANY); + final int rankIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.RANK); final int restoredIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.RESTORED); final int profileIdIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.PROFILE_ID); - //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); - //final int displayModeIndex = c.getColumnIndexOrThrow( - // LauncherSettings.Favorites.DISPLAY_MODE); + final int optionsIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.OPTIONS); + final CursorIconInfo cursorIconInfo = new CursorIconInfo(c); + + final LongSparseArray<UserHandleCompat> allUsers = new LongSparseArray<>(); + for (UserHandleCompat user : mUserManager.getUserProfiles()) { + allUsers.put(mUserManager.getSerialNumberForUser(user), user); + } ShortcutInfo info; String intentDescription; LauncherAppWidgetInfo appWidgetInfo; int container; long id; + long serialNumber; Intent intent; UserHandleCompat user; @@ -1937,16 +1841,18 @@ public class LauncherModel extends BroadcastReceiver int itemType = c.getInt(itemTypeIndex); boolean restored = 0 != c.getInt(restoredIndex); boolean allowMissingTarget = false; + container = c.getInt(containerIndex); switch (itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: id = c.getLong(idIndex); intentDescription = c.getString(intentIndex); - long serialNumber = c.getInt(profileIdIndex); - user = mUserManager.getUserForSerialNumber(serialNumber); + serialNumber = c.getInt(profileIdIndex); + user = allUsers.get(serialNumber); int promiseType = c.getInt(restoredIndex); int disabledState = 0; + boolean itemReplaced = false; if (user == null) { // User has been deleted remove the item. itemsToRemove.add(id); @@ -1978,9 +1884,7 @@ public class LauncherModel extends BroadcastReceiver ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.INTENT, intent.toUri(0)); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + updateItem(id, values); } } @@ -2004,16 +1908,33 @@ public class LauncherModel extends BroadcastReceiver if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) { // Restore has started once. - } else if (installingPkgs.contains(cn.getPackageName())) { + } else if (installingPkgs.containsKey(cn.getPackageName())) { // App restore has started. Update the flag promiseType |= ShortcutInfo.FLAG_RESTORE_STARTED; ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.RESTORED, promiseType); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); - + updateItem(id, values); + } else if ((promiseType & ShortcutInfo.FLAG_RESTORED_APP_TYPE) != 0) { + // This is a common app. Try to replace this. + int appType = CommonAppTypeParser.decodeItemTypeFromFlag(promiseType); + CommonAppTypeParser parser = new CommonAppTypeParser(id, appType, context); + if (parser.findDefaultApp()) { + // Default app found. Replace it. + intent = parser.parsedIntent; + cn = intent.getComponent(); + ContentValues values = parser.parsedValues; + values.put(LauncherSettings.Favorites.RESTORED, 0); + updateItem(id, values); + restored = false; + itemReplaced = true; + + } else if (REMOVE_UNRESTORED_ICONS) { + Launcher.addDumpLog(TAG, + "Unrestored package removed: " + cn, true); + itemsToRemove.add(id); + continue; + } } else if (REMOVE_UNRESTORED_ICONS) { Launcher.addDumpLog(TAG, "Unrestored package removed: " + cn, true); @@ -2059,12 +1980,26 @@ public class LauncherModel extends BroadcastReceiver continue; } - if (restored) { + boolean useLowResIcon = container >= 0 && + c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + + if (itemReplaced) { + if (user.equals(UserHandleCompat.myUserHandle())) { + info = getAppShortcutInfo(manager, intent, user, context, null, + cursorIconInfo.iconIndex, titleIndex, + false, useLowResIcon); + } else { + // Don't replace items for other profiles. + itemsToRemove.add(id); + continue; + } + } else if (restored) { if (user.equals(UserHandleCompat.myUserHandle())) { Launcher.addDumpLog(TAG, "constructing info for partially restored package", true); - info = getRestoredItemInfo(c, titleIndex, intent, promiseType); + info = getRestoredItemInfo(c, titleIndex, intent, + promiseType, itemType, cursorIconInfo, context); intent = getRestoredItemIntent(c, context, intent); } else { // Don't restore items for other profiles. @@ -2073,12 +2008,11 @@ public class LauncherModel extends BroadcastReceiver } } else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = getShortcutInfo(manager, intent, user, context, c, - iconIndex, titleIndex, mLabelCache, allowMissingTarget); + info = getAppShortcutInfo(manager, intent, user, context, c, + cursorIconInfo.iconIndex, titleIndex, + allowMissingTarget, useLowResIcon); } else { - info = getShortcutInfo(c, context, iconTypeIndex, - iconPackageIndex, iconResourceIndex, iconIndex, - titleIndex); + info = getShortcutInfo(c, context, titleIndex, cursorIconInfo); // App shortcuts that used to be automatically added to Launcher // didn't always have the correct intent flags set, so do that @@ -2096,14 +2030,17 @@ public class LauncherModel extends BroadcastReceiver if (info != null) { info.id = id; info.intent = intent; - container = c.getInt(containerIndex); info.container = container; info.screenId = c.getInt(screenIndex); info.cellX = c.getInt(cellXIndex); info.cellY = c.getInt(cellYIndex); + info.rank = c.getInt(rankIndex); info.spanX = 1; info.spanY = 1; info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); + if (info.promisedIntent != null) { + info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); + } info.isDisabled = disabledState; if (isSafeMode && !Utilities.isSystemApp(context, intent)) { info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE; @@ -2115,6 +2052,18 @@ public class LauncherModel extends BroadcastReceiver break; } + if (restored) { + ComponentName cn = info.getTargetComponent(); + if (cn != null) { + Integer progress = installingPkgs.get(cn.getPackageName()); + if (progress != null) { + info.setInstallProgress(progress); + } else { + info.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + } + } + } + switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: case LauncherSettings.Favorites.CONTAINER_HOTSEAT: @@ -2128,10 +2077,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.put(info.id, info); - - // now that we've loaded everthing re-save it with the - // icon in case it disappears somehow. - queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex); } else { throw new RuntimeException("Unexpected null ShortcutInfo"); } @@ -2141,15 +2086,16 @@ public class LauncherModel extends BroadcastReceiver id = c.getLong(idIndex); FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id); + // Do not trim the folder label, as is was set by the user. folderInfo.title = c.getString(titleIndex); folderInfo.id = id; - container = c.getInt(containerIndex); folderInfo.container = container; folderInfo.screenId = c.getInt(screenIndex); folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); folderInfo.spanX = 1; folderInfo.spanY = 1; + folderInfo.options = c.getInt(optionsIndex); // check & update map of what's occupied if (!checkItemPlacement(occupied, folderInfo)) { @@ -2174,28 +2120,41 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: // Read all Launcher-specific widget details + boolean customWidget = itemType == + LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + int appWidgetId = c.getInt(appWidgetIdIndex); + serialNumber = c.getLong(profileIdIndex); String savedProvider = c.getString(appWidgetProviderIndex); id = c.getLong(idIndex); + user = allUsers.get(serialNumber); + if (user == null) { + itemsToRemove.add(id); + continue; + } + final ComponentName component = ComponentName.unflattenFromString(savedProvider); final int restoreStatus = c.getInt(restoredIndex); final boolean isIdValid = (restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) == 0; - final boolean wasProviderReady = (restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0; - final AppWidgetProviderInfo provider = isIdValid - ? widgets.getAppWidgetInfo(appWidgetId) - : findAppWidgetProviderInfoWithComponent(context, component); + final LauncherAppWidgetProviderInfo provider = + LauncherModel.getProviderInfo(context, + ComponentName.unflattenFromString(savedProvider), + user); final boolean isProviderReady = isValidProvider(provider); - if (!isSafeMode && wasProviderReady && !isProviderReady) { + if (!isSafeMode && !customWidget && + wasProviderReady && !isProviderReady) { String log = "Deleting widget that isn't installed anymore: " + "id=" + id + " appWidgetId=" + appWidgetId; + Log.e(TAG, log); Launcher.addDumpLog(TAG, log, false); itemsToRemove.add(id); @@ -2203,10 +2162,6 @@ public class LauncherModel extends BroadcastReceiver if (isProviderReady) { appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, provider.provider); - int[] minSpan = - Launcher.getMinSpanForWidget(context, provider); - appWidgetInfo.minSpanX = minSpan[0]; - appWidgetInfo.minSpanY = minSpan[1]; int status = restoreStatus; if (!wasProviderReady) { @@ -2229,10 +2184,11 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component); appWidgetInfo.restoreStatus = restoreStatus; + Integer installProgress = installingPkgs.get(component.getPackageName()); if ((restoreStatus & LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) != 0) { // Restore has started once. - } else if (installingPkgs.contains(component.getPackageName())) { + } else if (installProgress != null) { // App restore has started. Update the flag appWidgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; @@ -2242,6 +2198,9 @@ public class LauncherModel extends BroadcastReceiver itemsToRemove.add(id); continue; } + + appWidgetInfo.installProgress = + installProgress == null ? 0 : installProgress; } appWidgetInfo.id = id; @@ -2250,8 +2209,8 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo.cellY = c.getInt(cellYIndex); appWidgetInfo.spanX = c.getInt(spanXIndex); appWidgetInfo.spanY = c.getInt(spanYIndex); + appWidgetInfo.user = user; - container = c.getInt(containerIndex); if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { Log.e(TAG, "Widget found where container != " + @@ -2259,24 +2218,26 @@ public class LauncherModel extends BroadcastReceiver continue; } - appWidgetInfo.container = c.getInt(containerIndex); + appWidgetInfo.container = container; // check & update map of what's occupied if (!checkItemPlacement(occupied, appWidgetInfo)) { itemsToRemove.add(id); break; } - String providerName = appWidgetInfo.providerName.flattenToString(); - if (!providerName.equals(savedProvider) || - (appWidgetInfo.restoreStatus != restoreStatus)) { - ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, - providerName); - values.put(LauncherSettings.Favorites.RESTORED, - appWidgetInfo.restoreStatus); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + if (!customWidget) { + String providerName = + appWidgetInfo.providerName.flattenToString(); + if (!providerName.equals(savedProvider) || + (appWidgetInfo.restoreStatus != restoreStatus)) { + ContentValues values = new ContentValues(); + values.put( + LauncherSettings.Favorites.APPWIDGET_PROVIDER, + providerName); + values.put(LauncherSettings.Favorites.RESTORED, + appWidgetInfo.restoreStatus); + updateItem(id, values); + } } sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); sBgAppWidgets.add(appWidgetInfo); @@ -2296,44 +2257,35 @@ public class LauncherModel extends BroadcastReceiver // Break early if we've stopped loading if (mStopped) { clearSBgDataStructures(); - return false; + return; } if (itemsToRemove.size() > 0) { - ContentProviderClient client = contentResolver.acquireContentProviderClient( - contentUri); // Remove dead items - for (long id : itemsToRemove) { - if (DEBUG_LOADERS) { - Log.d(TAG, "Removed id = " + id); - } - // Don't notify content observers - try { - client.delete(LauncherSettings.Favorites.getContentUri(id, false), - null, null); - } catch (RemoteException e) { - Log.w(TAG, "Could not remove id = " + id); - } + contentResolver.delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, itemsToRemove), null); + if (DEBUG_LOADERS) { + Log.d(TAG, "Removed = " + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, itemsToRemove)); + } + + // Remove any empty folder + for (long folderId : LauncherAppState.getLauncherProvider() + .deleteEmptyFolders()) { + sBgWorkspaceItems.remove(sBgFolders.get(folderId)); + sBgFolders.remove(folderId); + sBgItemsIdMap.remove(folderId); } } if (restoredRows.size() > 0) { - ContentProviderClient updater = contentResolver.acquireContentProviderClient( - contentUri); // Update restored items that no longer require special handling - try { - StringBuilder selectionBuilder = new StringBuilder(); - selectionBuilder.append(LauncherSettings.Favorites._ID); - selectionBuilder.append(" IN ("); - selectionBuilder.append(TextUtils.join(", ", restoredRows)); - selectionBuilder.append(")"); - ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.RESTORED, 0); - updater.update(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, - values, selectionBuilder.toString(), null); - } catch (RemoteException e) { - Log.w(TAG, "Could not update restored rows"); - } + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.RESTORED, 0); + contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, values, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, restoredRows), null); } if (!isSdCardReady && !sPendingPackages.isEmpty()) { @@ -2342,63 +2294,22 @@ public class LauncherModel extends BroadcastReceiver null, sWorker); } - if (loadedOldDb) { - long maxScreenId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - !sBgWorkspaceScreens.contains(screenId)) { - sBgWorkspaceScreens.add(screenId); - if (screenId > maxScreenId) { - maxScreenId = screenId; - } - } - } - Collections.sort(sBgWorkspaceScreens); - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - maxScreenId: " + maxScreenId, true); - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); + sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); - LauncherAppState.getLauncherProvider().updateMaxScreenId(maxScreenId); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - - // Update the max item id after we load an old db - long maxItemId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - maxItemId = Math.max(maxItemId, item.id); + // Remove any empty screens + ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens); + for (ItemInfo item: sBgItemsIdMap) { + long screenId = item.screenId; + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + unusedScreens.contains(screenId)) { + unusedScreens.remove(screenId); } - LauncherAppState.getLauncherProvider().updateMaxItemId(maxItemId); - } else { - TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(mContext); - for (Integer i : orderedScreens.keySet()) { - sBgWorkspaceScreens.add(orderedScreens.get(i)); - } - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); - - // Remove any empty screens - ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens); - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - unusedScreens.contains(screenId)) { - unusedScreens.remove(screenId); - } - } - - // If there are any empty screens remove them, and update. - if (unusedScreens.size() != 0) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - unusedScreens (to be removed): " + - TextUtils.join(", ", unusedScreens), true); + } - sBgWorkspaceScreens.removeAll(unusedScreens); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - } + // If there are any empty screens remove them, and update. + if (unusedScreens.size() != 0) { + sBgWorkspaceScreens.removeAll(unusedScreens); + updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); } if (DEBUG_LOADERS) { @@ -2408,14 +2319,13 @@ public class LauncherModel extends BroadcastReceiver for (int y = 0; y < countY; y++) { String line = ""; - Iterator<Long> iter = occupied.keySet().iterator(); - while (iter.hasNext()) { - long screenId = iter.next(); + for (int i = 0; i < nScreens; i++) { + long screenId = occupied.keyAt(i); if (screenId > 0) { line += " | "; } + ItemInfo[][] screen = occupied.valueAt(i); for (int x = 0; x < countX; x++) { - ItemInfo[][] screen = occupied.get(screenId); if (x < screen.length && y < screen[x].length) { line += (screen[x][y] != null) ? "#" : "."; } else { @@ -2427,7 +2337,17 @@ public class LauncherModel extends BroadcastReceiver } } } - return loadedOldDb; + } + + /** + * Partially updates the item without any notification. Must be called on the worker thread. + */ + private void updateItem(long itemId, ContentValues update) { + mContext.getContentResolver().update( + LauncherSettings.Favorites.CONTENT_URI, + update, + BaseColumns._ID + "= ?", + new String[]{Long.toString(itemId)}); } /** Filters the set of items who are directly or indirectly (via another container) on the @@ -2496,14 +2416,17 @@ public class LauncherModel extends BroadcastReceiver /** Filters the set of folders which are on the specified screen. */ private void filterCurrentFolders(long currentScreenId, - HashMap<Long, ItemInfo> itemsIdMap, - HashMap<Long, FolderInfo> folders, - HashMap<Long, FolderInfo> currentScreenFolders, - HashMap<Long, FolderInfo> otherScreenFolders) { + LongArrayMap<ItemInfo> itemsIdMap, + LongArrayMap<FolderInfo> folders, + LongArrayMap<FolderInfo> currentScreenFolders, + LongArrayMap<FolderInfo> otherScreenFolders) { + + int total = folders.size(); + for (int i = 0; i < total; i++) { + long id = folders.keyAt(i); + FolderInfo folder = folders.valueAt(i); - for (long id : folders.keySet()) { ItemInfo info = itemsIdMap.get(id); - FolderInfo folder = folders.get(id); if (info == null || folder == null) continue; if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && info.screenId == currentScreenId) { @@ -2518,13 +2441,13 @@ public class LauncherModel extends BroadcastReceiver * right) */ private void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) { final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); // XXX: review this Collections.sort(workspaceItems, new Comparator<ItemInfo>() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { - int cellCountX = (int) grid.numColumns; - int cellCountY = (int) grid.numRows; + int cellCountX = (int) profile.numColumns; + int cellCountY = (int) profile.numRows; int screenOffset = cellCountX * cellCountY; int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat long lr = (lhs.container * containerOffset + lhs.screenId * screenOffset + @@ -2547,13 +2470,13 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } private void bindWorkspaceItems(final Callbacks oldCallbacks, final ArrayList<ItemInfo> workspaceItems, final ArrayList<LauncherAppWidgetInfo> appWidgets, - final HashMap<Long, FolderInfo> folders, + final LongArrayMap<FolderInfo> folders, ArrayList<Runnable> deferredBindRunnables) { final boolean postOnMainThread = (deferredBindRunnables != null); @@ -2578,7 +2501,7 @@ public class LauncherModel extends BroadcastReceiver deferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2597,7 +2520,7 @@ public class LauncherModel extends BroadcastReceiver deferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2616,7 +2539,7 @@ public class LauncherModel extends BroadcastReceiver if (postOnMainThread) { deferredBindRunnables.add(r); } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } } @@ -2624,7 +2547,7 @@ public class LauncherModel extends BroadcastReceiver /** * Binds all loaded data to actual views on the main thread. */ - private void bindWorkspace(int synchronizeBindPage, final boolean isUpgradePath) { + private void bindWorkspace(int synchronizeBindPage) { final long t = SystemClock.uptimeMillis(); Runnable r; @@ -2641,15 +2564,18 @@ public class LauncherModel extends BroadcastReceiver ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>(); ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<LauncherAppWidgetInfo>(); - HashMap<Long, FolderInfo> folders = new HashMap<Long, FolderInfo>(); - HashMap<Long, ItemInfo> itemsIdMap = new HashMap<Long, ItemInfo>(); ArrayList<Long> orderedScreenIds = new ArrayList<Long>(); + + final LongArrayMap<FolderInfo> folders; + final LongArrayMap<ItemInfo> itemsIdMap; + synchronized (sBgLock) { workspaceItems.addAll(sBgWorkspaceItems); appWidgets.addAll(sBgAppWidgets); - folders.putAll(sBgFolders); - itemsIdMap.putAll(sBgItemsIdMap); orderedScreenIds.addAll(sBgWorkspaceScreens); + + folders = sBgFolders.clone(); + itemsIdMap = sBgItemsIdMap.clone(); } final boolean isLoadingSynchronously = @@ -2675,8 +2601,8 @@ public class LauncherModel extends BroadcastReceiver new ArrayList<LauncherAppWidgetInfo>(); ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<LauncherAppWidgetInfo>(); - HashMap<Long, FolderInfo> currentFolders = new HashMap<Long, FolderInfo>(); - HashMap<Long, FolderInfo> otherFolders = new HashMap<Long, FolderInfo>(); + LongArrayMap<FolderInfo> currentFolders = new LongArrayMap<>(); + LongArrayMap<FolderInfo> otherFolders = new LongArrayMap<>(); filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems, otherWorkspaceItems); @@ -2696,7 +2622,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); bindWorkspaceScreens(oldCallbacks, orderedScreenIds); @@ -2712,7 +2638,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } // Load all the remaining pages (if we are loading synchronously, we want to defer this @@ -2728,7 +2654,7 @@ public class LauncherModel extends BroadcastReceiver public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { - callbacks.finishBindingItems(isUpgradePath); + callbacks.finishBindingItems(); } // If we're profiling, ensure this is the last thing in the queue. @@ -2745,7 +2671,7 @@ public class LauncherModel extends BroadcastReceiver mDeferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2759,6 +2685,12 @@ public class LauncherModel extends BroadcastReceiver if (mStopped) { return; } + } + updateIconCache(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } mAllAppsLoaded = true; } } else { @@ -2766,6 +2698,27 @@ public class LauncherModel extends BroadcastReceiver } } + private void updateIconCache() { + // Ignore packages which have a promise icon. + HashSet<String> packagesToIgnore = new HashSet<>(); + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + if (si.isPromise() && si.getTargetComponent() != null) { + packagesToIgnore.add(si.getTargetComponent().getPackageName()); + } + } else if (info instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo lawi = (LauncherAppWidgetInfo) info; + if (lawi.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) { + packagesToIgnore.add(lawi.providerName.getPackageName()); + } + } + } + } + mIconCache.updateDbIcons(packagesToIgnore); + } + private void onlyBindAllApps() { final Callbacks oldCallbacks = mCallbacks.get(); if (oldCallbacks == null) { @@ -2778,12 +2731,14 @@ public class LauncherModel extends BroadcastReceiver @SuppressWarnings("unchecked") final ArrayList<AppInfo> list = (ArrayList<AppInfo>) mBgAllAppsList.data.clone(); + final WidgetsModel widgetList = mBgWidgetsModel.clone(); Runnable r = new Runnable() { public void run() { final long t = SystemClock.uptimeMillis(); final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAllApplications(list); + callbacks.bindAllPackages(widgetList); } if (DEBUG_LOADERS) { Log.d(TAG, "bound all " + list.size() + " apps from cache in " @@ -2809,19 +2764,14 @@ public class LauncherModel extends BroadcastReceiver return; } - final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); - mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); - final List<UserHandleCompat> profiles = mUserManager.getUserProfiles(); // Clear the list of apps mBgAllAppsList.clear(); - SharedPreferences prefs = mContext.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); for (UserHandleCompat user : profiles) { // Query for the set of apps final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); + final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); if (DEBUG_LOADERS) { Log.d(TAG, "getActivityList took " + (SystemClock.uptimeMillis()-qiaTime) + "ms for user " + user); @@ -2832,39 +2782,23 @@ public class LauncherModel extends BroadcastReceiver if (apps == null || apps.isEmpty()) { return; } - // Sort the applications by name - final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - Collections.sort(apps, - new LauncherModel.ShortcutNameComparator(mLabelCache)); - if (DEBUG_LOADERS) { - Log.d(TAG, "sort took " - + (SystemClock.uptimeMillis()-sortTime) + "ms"); - } // Create the ApplicationInfos for (int i = 0; i < apps.size(); i++) { LauncherActivityInfoCompat app = apps.get(i); // This builds the icon bitmaps. - mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, mLabelCache)); + mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache)); } - if (ADD_MANAGED_PROFILE_SHORTCUTS && !user.equals(UserHandleCompat.myUserHandle())) { - // Add shortcuts for packages which were installed while launcher was dead. - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(user); - Set<String> packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET); - HashSet<String> newPackageSet = new HashSet<String>(); + final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user); + if (heuristic != null) { + runAfterBindCompletes(new Runnable() { - for (LauncherActivityInfoCompat info : apps) { - String packageName = info.getComponentName().getPackageName(); - if (!packagesAdded.contains(packageName) - && !newPackageSet.contains(packageName)) { - InstallShortcutReceiver.queueInstallShortcut(info, mContext); + @Override + public void run() { + heuristic.processUserApps(apps); } - newPackageSet.add(packageName); - } - - prefs.edit().putStringSet(shortcutsSetKey, newPackageSet).commit(); + }); } } // Huh? Shouldn't this be inside the Runnable below? @@ -2874,6 +2808,7 @@ public class LauncherModel extends BroadcastReceiver // Post callback on main thread mHandler.post(new Runnable() { public void run() { + final long bindTime = SystemClock.uptimeMillis(); final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { @@ -2887,7 +2822,11 @@ public class LauncherModel extends BroadcastReceiver } } }); + // Cleanup any data stored for a deleted user. + ManagedProfileHeuristic.processAllUsers(profiles, mContext); + loadAndBindWidgetsAndShortcuts(mApp.getContext(), tryGetCallbacks(oldCallbacks), + true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " + (SystemClock.uptimeMillis() - loadTime) + "ms"); @@ -2897,7 +2836,6 @@ public class LauncherModel extends BroadcastReceiver public void dumpState() { synchronized (sBgLock) { Log.d(TAG, "mLoaderTask.mContext=" + mContext); - Log.d(TAG, "mLoaderTask.mIsLaunching=" + mIsLaunching); Log.d(TAG, "mLoaderTask.mStopped=" + mStopped); Log.d(TAG, "mLoaderTask.mLoadAndBindStepFinished=" + mLoadAndBindStepFinished); Log.d(TAG, "mItems size=" + sBgWorkspaceItems.size()); @@ -2905,11 +2843,66 @@ public class LauncherModel extends BroadcastReceiver } } + /** + * Called when the icons for packages have been updated in the icon cache. + */ + public void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandleCompat user) { + final Callbacks callbacks = getCallback(); + final ArrayList<AppInfo> updatedApps = new ArrayList<>(); + final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>(); + + // If any package icon has changed (app was updated while launcher was dead), + // update the corresponding shortcuts. + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo && user.equals(info.user) + && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (cn != null && updatedPackages.contains(cn.getPackageName())) { + si.updateIcon(mIconCache); + updatedShortcuts.add(si); + } + } + } + mBgAllAppsList.updateIconsAndLabels(updatedPackages, user, updatedApps); + } + + if (!updatedShortcuts.isEmpty()) { + final UserHandleCompat userFinal = user; + mHandler.post(new Runnable() { + + public void run() { + Callbacks cb = getCallback(); + if (cb != null && callbacks == cb) { + cb.bindShortcutsChanged(updatedShortcuts, + new ArrayList<ShortcutInfo>(), userFinal); + } + } + }); + } + + if (!updatedApps.isEmpty()) { + mHandler.post(new Runnable() { + + public void run() { + Callbacks cb = getCallback(); + if (cb != null && callbacks == cb) { + cb.bindAppsUpdated(updatedApps); + } + } + }); + } + + // Reload widget list. No need to refresh, as we only want to update the icons and labels. + loadAndBindWidgetsAndShortcuts(mApp.getContext(), callbacks, false); + } + void enqueuePackageUpdated(PackageUpdatedTask task) { sWorker.post(task); } - private class AppsAvailabilityCheck extends BroadcastReceiver { + @Thunk class AppsAvailabilityCheck extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -2969,73 +2962,52 @@ public class LauncherModel extends BroadcastReceiver } public void run() { + if (!mHasLoaderCompletedOnce) { + // Loader has not yet run. + return; + } final Context context = mApp.getContext(); final String[] packages = mPackages; final int N = packages.length; switch (mOp) { - case OP_ADD: + case OP_ADD: { for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); - mIconCache.remove(packages[i], mUser); + mIconCache.updateIconsForPkg(packages[i], mUser); mBgAllAppsList.addPackage(context, packages[i], mUser); } - // Auto add shortcuts for added packages. - if (ADD_MANAGED_PROFILE_SHORTCUTS - && !UserHandleCompat.myUserHandle().equals(mUser)) { - SharedPreferences prefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(mUser); - Set<String> shortcutSet = new HashSet<String>( - prefs.getStringSet(shortcutsSetKey,Collections.EMPTY_SET)); - - for (int i=0; i<N; i++) { - if (!shortcutSet.contains(packages[i])) { - shortcutSet.add(packages[i]); - List<LauncherActivityInfoCompat> activities = - mLauncherApps.getActivityList(packages[i], mUser); - if (activities != null && !activities.isEmpty()) { - InstallShortcutReceiver.queueInstallShortcut( - activities.get(0), context); - } - } - } - - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageAdd(mPackages); } break; + } case OP_UPDATE: for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); + mIconCache.updateIconsForPkg(packages[i], mUser); mBgAllAppsList.updatePackage(context, packages[i], mUser); - WidgetPreviewLoader.removePackageFromDb( - mApp.getWidgetPreviewCacheDb(), packages[i]); + mApp.getWidgetCache().removePackage(packages[i], mUser); } break; - case OP_REMOVE: - // Remove the packageName for the set of auto-installed shortcuts. This - // will ensure that the shortcut when the app is installed again. - if (ADD_MANAGED_PROFILE_SHORTCUTS - && !UserHandleCompat.myUserHandle().equals(mUser)) { - SharedPreferences prefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(mUser); - HashSet<String> shortcutSet = new HashSet<String>( - prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET)); - shortcutSet.removeAll(Arrays.asList(mPackages)); - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + case OP_REMOVE: { + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageRemoved(mPackages); + } + for (int i=0; i<N; i++) { + if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); + mIconCache.removeIconsForPkg(packages[i], mUser); } // Fall through + } case OP_UNAVAILABLE: - boolean clearCache = mOp == OP_REMOVE; for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); - mBgAllAppsList.removePackage(packages[i], mUser, clearCache); - WidgetPreviewLoader.removePackageFromDb( - mApp.getWidgetPreviewCacheDb(), packages[i]); + mBgAllAppsList.removePackage(packages[i], mUser); + mApp.getWidgetCache().removePackage(packages[i], mUser); } break; } @@ -3067,13 +3039,7 @@ public class LauncherModel extends BroadcastReceiver new HashMap<ComponentName, AppInfo>(); if (added != null) { - // Ensure that we add all the workspace applications to the db - if (LauncherAppState.isDisableAllApps()) { - final ArrayList<ItemInfo> addedInfos = new ArrayList<ItemInfo>(added); - addAndBindAddedWorkspaceApps(context, addedInfos); - } else { - addAppsToAllApps(context, added); - } + addAppsToAllApps(context, added); for (AppInfo ai : added) { addedOrUpdatedApps.put(ai.componentName, ai); } @@ -3103,7 +3069,7 @@ public class LauncherModel extends BroadcastReceiver HashSet<String> packageSet = new HashSet<String>(Arrays.asList(packages)); synchronized (sBgLock) { - for (ItemInfo info : sBgItemsIdMap.values()) { + for (ItemInfo info : sBgItemsIdMap) { if (info instanceof ShortcutInfo && mUser.equals(info.user)) { ShortcutInfo si = (ShortcutInfo) info; boolean infoUpdated = false; @@ -3112,8 +3078,9 @@ public class LauncherModel extends BroadcastReceiver // Update shortcuts which use iconResource. if ((si.iconResource != null) && packageSet.contains(si.iconResource.packageName)) { - Bitmap icon = Utilities.createIconBitmap(si.iconResource.packageName, - si.iconResource.resourceName, mIconCache, context); + Bitmap icon = Utilities.createIconBitmap( + si.iconResource.packageName, + si.iconResource.resourceName, context); if (icon != null) { si.setIcon(icon); si.usingFallbackIcon = false; @@ -3126,7 +3093,6 @@ public class LauncherModel extends BroadcastReceiver AppInfo appInfo = addedOrUpdatedApps.get(cn); if (si.isPromise()) { - mIconCache.deletePreloadedIcon(cn, mUser); if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { // Auto install icon PackageManager pm = context.getPackageManager(); @@ -3152,12 +3118,13 @@ public class LauncherModel extends BroadcastReceiver } // Restore the shortcut. + if (appInfo != null) { + si.flags = appInfo.flags; + } + si.intent = si.promisedIntent; si.promisedIntent = null; - si.status &= ~ShortcutInfo.FLAG_RESTORED_ICON - & ~ShortcutInfo.FLAG_AUTOINTALL_ICON - & ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; - + si.status = ShortcutInfo.DEFAULT; infoUpdated = true; si.updateIcon(mIconCache); } @@ -3165,7 +3132,7 @@ public class LauncherModel extends BroadcastReceiver if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction()) && si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { si.updateIcon(mIconCache); - si.title = appInfo.title.toString(); + si.title = Utilities.trim(appInfo.title); si.contentDescription = appInfo.contentDescription; infoUpdated = true; } @@ -3268,17 +3235,8 @@ public class LauncherModel extends BroadcastReceiver }); } - final ArrayList<Object> widgetsAndShortcuts = - getSortedWidgetsAndShortcuts(context); - mHandler.post(new Runnable() { - @Override - public void run() { - Callbacks cb = getCallback(); - if (callbacks == cb && cb != null) { - callbacks.bindPackagesUpdated(widgetsAndShortcuts); - } - } - }); + // onProvidersChanged method (API >= 17) already refreshed the widget list + loadAndBindWidgetsAndShortcuts(context, callbacks, Build.VERSION.SDK_INT < 17); // Write all the logs to disk mHandler.post(new Runnable() { @@ -3292,19 +3250,106 @@ public class LauncherModel extends BroadcastReceiver } } - // Returns a list of ResolveInfos/AppWindowInfos in sorted order - public static ArrayList<Object> getSortedWidgetsAndShortcuts(Context context) { + public static List<LauncherAppWidgetProviderInfo> getWidgetProviders(Context context, + boolean refresh) { + ArrayList<LauncherAppWidgetProviderInfo> results = + new ArrayList<LauncherAppWidgetProviderInfo>(); + try { + synchronized (sBgLock) { + if (sBgWidgetProviders == null || refresh) { + HashMap<ComponentKey, LauncherAppWidgetProviderInfo> tmpWidgetProviders + = new HashMap<>(); + AppWidgetManagerCompat wm = AppWidgetManagerCompat.getInstance(context); + LauncherAppWidgetProviderInfo info; + + List<AppWidgetProviderInfo> widgets = wm.getAllProviders(); + for (AppWidgetProviderInfo pInfo : widgets) { + info = LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo); + UserHandleCompat user = wm.getUser(info); + tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); + } + + Collection<CustomAppWidget> customWidgets = Launcher.getCustomAppWidgets().values(); + for (CustomAppWidget widget : customWidgets) { + info = new LauncherAppWidgetProviderInfo(context, widget); + UserHandleCompat user = wm.getUser(info); + tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); + } + // Replace the global list at the very end, so that if there is an exception, + // previously loaded provider list is used. + sBgWidgetProviders = tmpWidgetProviders; + } + results.addAll(sBgWidgetProviders.values()); + return results; + } + } catch (Exception e) { + if (e.getCause() instanceof TransactionTooLargeException) { + // the returned value may be incomplete and will not be refreshed until the next + // time Launcher starts. + // TODO: after figuring out a repro step, introduce a dirty bit to check when + // onResume is called to refresh the widget provider list. + synchronized (sBgLock) { + if (sBgWidgetProviders != null) { + results.addAll(sBgWidgetProviders.values()); + } + return results; + } + } else { + throw e; + } + } + } + + public static LauncherAppWidgetProviderInfo getProviderInfo(Context ctx, ComponentName name, + UserHandleCompat user) { + synchronized (sBgLock) { + if (sBgWidgetProviders == null) { + getWidgetProviders(ctx, false /* refresh */); + } + return sBgWidgetProviders.get(new ComponentKey(name, user)); + } + } + + public void loadAndBindWidgetsAndShortcuts(final Context context, final Callbacks callbacks, + final boolean refresh) { + + runOnWorkerThread(new Runnable() { + @Override + public void run() { + updateWidgetsModel(context, refresh); + final WidgetsModel model = mBgWidgetsModel.clone(); + + mHandler.post(new Runnable() { + @Override + public void run() { + Callbacks cb = getCallback(); + if (callbacks == cb && cb != null) { + callbacks.bindAllPackages(model); + } + } + }); + // update the Widget entries inside DB on the worker thread. + LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews( + model.getRawList()); + } + }); + } + + /** + * Returns a list of ResolveInfos/AppWidgetInfos. + * + * @see #loadAndBindWidgetsAndShortcuts + */ + @Thunk void updateWidgetsModel(Context context, boolean refresh) { PackageManager packageManager = context.getPackageManager(); final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>(); - widgetsAndShortcuts.addAll(AppWidgetManagerCompat.getInstance(context).getAllProviders()); - + widgetsAndShortcuts.addAll(getWidgetProviders(context, refresh)); Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); - Collections.sort(widgetsAndShortcuts, new WidgetAndShortcutNameComparator(context)); - return widgetsAndShortcuts; + mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts); } - private static boolean isPackageDisabled(Context context, String packageName, + @Thunk static boolean isPackageDisabled(Context context, String packageName, UserHandleCompat user) { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); return !launcherApps.isPackageEnabledForProfile(packageName, user); @@ -3335,31 +3380,36 @@ public class LauncherModel extends BroadcastReceiver * Make an ShortcutInfo object for a restored application or shortcut item that points * to a package that is not yet installed on the system. */ - public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent, - int promiseType) { + public ShortcutInfo getRestoredItemInfo(Cursor c, int titleIndex, Intent intent, + int promiseType, int itemType, CursorIconInfo iconInfo, Context context) { final ShortcutInfo info = new ShortcutInfo(); info.user = UserHandleCompat.myUserHandle(); - mIconCache.getTitleAndIcon(info, intent, info.user, true); + + Bitmap icon = iconInfo.loadIcon(c, info, context); + // the fallback icon + if (icon == null) { + mIconCache.getTitleAndIcon(info, intent, info.user, false /* useLowResIcon */); + } else { + info.setIcon(icon); + } if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { - String title = (cursor != null) ? cursor.getString(titleIndex) : null; + String title = (c != null) ? c.getString(titleIndex) : null; if (!TextUtils.isEmpty(title)) { - info.title = title; + info.title = Utilities.trim(title); } - info.status = ShortcutInfo.FLAG_RESTORED_ICON; } else if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { if (TextUtils.isEmpty(info.title)) { - info.title = (cursor != null) ? cursor.getString(titleIndex) : ""; + info.title = (c != null) ? Utilities.trim(c.getString(titleIndex)) : ""; } - info.status = ShortcutInfo.FLAG_AUTOINTALL_ICON; } else { throw new InvalidParameterException("Invalid restoreType " + promiseType); } - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); - info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); + info.itemType = itemType; info.promisedIntent = intent; + info.status = promiseType; return info; } @@ -3367,7 +3417,7 @@ public class LauncherModel extends BroadcastReceiver * Make an Intent object for a restored application or shortcut item that points * to the market page for the item. */ - private Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { + @Thunk Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { ComponentName componentName = intent.getComponent(); return getMarketIntent(componentName.getPackageName()); } @@ -3382,22 +3432,13 @@ public class LauncherModel extends BroadcastReceiver } /** - * This is called from the code that adds shortcuts from the intent receiver. This - * doesn't have a Cursor, but - */ - public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, - UserHandleCompat user, Context context) { - return getShortcutInfo(manager, intent, user, context, null, -1, -1, null, false); - } - - /** * Make an ShortcutInfo object for a shortcut that is an application. * * If c is not null, then it will be used to fill in missing data like the title and icon. */ - public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, + public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent, UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex, - HashMap<Object, CharSequence> labelCache, boolean allowMissingTarget) { + boolean allowMissingTarget, boolean useLowResIcon) { if (user == null) { Log.d(TAG, "Null user found in getShortcutInfo"); return null; @@ -3419,56 +3460,32 @@ public class LauncherModel extends BroadcastReceiver } final ShortcutInfo info = new ShortcutInfo(); - - // the resource -- This may implicitly give us back the fallback icon, - // but don't worry about that. All we're doing with usingFallbackIcon is - // to avoid saving lots of copies of that in the database, and most apps - // have icons anyway. - Bitmap icon = mIconCache.getIcon(componentName, lai, labelCache); - - // the db - if (icon == null) { - if (c != null) { - icon = getIconFromCursor(c, iconIndex, context); - } + mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon); + if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) { + Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context); + info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon); } - // the fallback icon - if (icon == null) { - icon = mIconCache.getDefaultIcon(user); - info.usingFallbackIcon = true; - } - info.setIcon(icon); - // From the cache. - if (labelCache != null) { - info.title = labelCache.get(componentName); - } - - // from the resource - if (info.title == null && lai != null) { - info.title = lai.getLabel(); - if (labelCache != null) { - labelCache.put(componentName, info.title); - } - } // from the db - if (info.title == null) { - if (c != null) { - info.title = c.getString(titleIndex); - } + if (TextUtils.isEmpty(info.title) && c != null) { + info.title = Utilities.trim(c.getString(titleIndex)); } + // fall back to the class name of the activity if (info.title == null) { info.title = componentName.getClassName(); } + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; info.user = user; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); + if (lai != null) { + info.flags = AppInfo.initFlags(lai); + } return info; } - static ArrayList<ItemInfo> filterItemInfos(Collection<ItemInfo> infos, + static ArrayList<ItemInfo> filterItemInfos(Iterable<ItemInfo> infos, ItemInfoFilter f) { HashSet<ItemInfo> filtered = new HashSet<ItemInfo>(); for (ItemInfo i : infos) { @@ -3497,7 +3514,7 @@ public class LauncherModel extends BroadcastReceiver return new ArrayList<ItemInfo>(filtered); } - private ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, + @Thunk ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, final UserHandleCompat user) { ItemInfoFilter filter = new ItemInfoFilter() { @Override @@ -3509,17 +3526,14 @@ public class LauncherModel extends BroadcastReceiver } } }; - return filterItemInfos(sBgItemsIdMap.values(), filter); + return filterItemInfos(sBgItemsIdMap, filter); } /** * Make an ShortcutInfo object for a shortcut that isn't an application. */ - private ShortcutInfo getShortcutInfo(Cursor c, Context context, - int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, - int titleIndex) { - - Bitmap icon = null; + @Thunk ShortcutInfo getShortcutInfo(Cursor c, Context context, + int titleIndex, CursorIconInfo iconInfo) { final ShortcutInfo info = new ShortcutInfo(); // Non-app shortcuts are only supported for current user. info.user = UserHandleCompat.myUserHandle(); @@ -3527,77 +3541,18 @@ public class LauncherModel extends BroadcastReceiver // TODO: If there's an explicit component and we can't install that, delete it. - info.title = c.getString(titleIndex); - - int iconType = c.getInt(iconTypeIndex); - switch (iconType) { - case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: - String packageName = c.getString(iconPackageIndex); - String resourceName = c.getString(iconResourceIndex); - info.customIcon = false; - // the resource - icon = Utilities.createIconBitmap(packageName, resourceName, mIconCache, context); - // the db - if (icon == null) { - icon = getIconFromCursor(c, iconIndex, context); - } - // the fallback icon - if (icon == null) { - icon = mIconCache.getDefaultIcon(info.user); - info.usingFallbackIcon = true; - } - break; - case LauncherSettings.Favorites.ICON_TYPE_BITMAP: - icon = getIconFromCursor(c, iconIndex, context); - if (icon == null) { - icon = mIconCache.getDefaultIcon(info.user); - info.customIcon = false; - info.usingFallbackIcon = true; - } else { - info.customIcon = true; - } - break; - default: + info.title = Utilities.trim(c.getString(titleIndex)); + + Bitmap icon = iconInfo.loadIcon(c, info, context); + // the fallback icon + if (icon == null) { icon = mIconCache.getDefaultIcon(info.user); info.usingFallbackIcon = true; - info.customIcon = false; - break; } info.setIcon(icon); return info; } - Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context) { - @SuppressWarnings("all") // suppress dead code warning - final boolean debug = false; - if (debug) { - Log.d(TAG, "getIconFromCursor app=" - + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE))); - } - byte[] data = c.getBlob(iconIndex); - try { - return Utilities.createIconBitmap( - BitmapFactory.decodeByteArray(data, 0, data.length), context); - } catch (Exception e) { - return null; - } - } - - /** - * Attempts to find an AppWidgetProviderInfo that matches the given component. - */ - static AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, - ComponentName component) { - List<AppWidgetProviderInfo> widgets = - AppWidgetManager.getInstance(context).getInstalledProviders(); - for (AppWidgetProviderInfo info : widgets) { - if (info.provider.equals(component)) { - return info; - } - } - return null; - } - ShortcutInfo infoFromShortcutIntent(Context context, Intent data) { Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); @@ -3621,7 +3576,7 @@ public class LauncherModel extends BroadcastReceiver if (extra instanceof ShortcutIconResource) { iconResource = (ShortcutIconResource) extra; icon = Utilities.createIconBitmap(iconResource.packageName, - iconResource.resourceName, mIconCache, context); + iconResource.resourceName, context); } } @@ -3636,9 +3591,8 @@ public class LauncherModel extends BroadcastReceiver } info.setIcon(icon); - info.title = name; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.title = Utilities.trim(name); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); info.intent = intent; info.customIcon = customIcon; info.iconResource = iconResource; @@ -3646,50 +3600,11 @@ public class LauncherModel extends BroadcastReceiver return info; } - boolean queueIconToBeChecked(HashMap<Object, byte[]> cache, ShortcutInfo info, Cursor c, - int iconIndex) { - // If apps can't be on SD, don't even bother. - if (!mAppsCanBeOnRemoveableStorage) { - return false; - } - // If this icon doesn't have a custom icon, check to see - // what's stored in the DB, and if it doesn't match what - // we're going to show, store what we are going to show back - // into the DB. We do this so when we're loading, if the - // package manager can't find an icon (for example because - // the app is on SD) then we can use that instead. - if (!info.customIcon && !info.usingFallbackIcon) { - cache.put(info, c.getBlob(iconIndex)); - return true; - } - return false; - } - void updateSavedIcon(Context context, ShortcutInfo info, byte[] data) { - boolean needSave = false; - try { - if (data != null) { - Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length); - Bitmap loaded = info.getIcon(mIconCache); - needSave = !saved.sameAs(loaded); - } else { - needSave = true; - } - } catch (Exception e) { - needSave = true; - } - if (needSave) { - Log.d(TAG, "going to save icon bitmap for info=" + info); - // This is slower than is ideal, but this only happens once - // or when the app is updated with a new icon. - updateItemInDatabase(context, info); - } - } - /** * Return an existing FolderInfo object if we have encountered this ID previously, * or make a new one. */ - private static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) { + @Thunk static FolderInfo findOrMakeFolder(LongArrayMap<FolderInfo> folders, long id) { // See if a placeholder was created for us already FolderInfo folderInfo = folders.get(id); if (folderInfo == null) { @@ -3700,105 +3615,6 @@ public class LauncherModel extends BroadcastReceiver return folderInfo; } - public static final Comparator<AppInfo> getAppNameComparator() { - final Collator collator = Collator.getInstance(); - return new Comparator<AppInfo>() { - public final int compare(AppInfo a, AppInfo b) { - if (a.user.equals(b.user)) { - int result = collator.compare(a.title.toString().trim(), - b.title.toString().trim()); - if (result == 0) { - result = a.componentName.compareTo(b.componentName); - } - return result; - } else { - // TODO Need to figure out rules for sorting - // profiles, this puts work second. - return a.user.toString().compareTo(b.user.toString()); - } - } - }; - } - public static final Comparator<AppInfo> APP_INSTALL_TIME_COMPARATOR - = new Comparator<AppInfo>() { - public final int compare(AppInfo a, AppInfo b) { - if (a.firstInstallTime < b.firstInstallTime) return 1; - if (a.firstInstallTime > b.firstInstallTime) return -1; - return 0; - } - }; - static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) { - if (info.activityInfo != null) { - return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); - } else { - return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); - } - } - public static class ShortcutNameComparator implements Comparator<LauncherActivityInfoCompat> { - private Collator mCollator; - private HashMap<Object, CharSequence> mLabelCache; - ShortcutNameComparator(PackageManager pm) { - mLabelCache = new HashMap<Object, CharSequence>(); - mCollator = Collator.getInstance(); - } - ShortcutNameComparator(HashMap<Object, CharSequence> labelCache) { - mLabelCache = labelCache; - mCollator = Collator.getInstance(); - } - public final int compare(LauncherActivityInfoCompat a, LauncherActivityInfoCompat b) { - String labelA, labelB; - ComponentName keyA = a.getComponentName(); - ComponentName keyB = b.getComponentName(); - if (mLabelCache.containsKey(keyA)) { - labelA = mLabelCache.get(keyA).toString(); - } else { - labelA = a.getLabel().toString().trim(); - - mLabelCache.put(keyA, labelA); - } - if (mLabelCache.containsKey(keyB)) { - labelB = mLabelCache.get(keyB).toString(); - } else { - labelB = b.getLabel().toString().trim(); - - mLabelCache.put(keyB, labelB); - } - return mCollator.compare(labelA, labelB); - } - }; - public static class WidgetAndShortcutNameComparator implements Comparator<Object> { - private final AppWidgetManagerCompat mManager; - private final PackageManager mPackageManager; - private final HashMap<Object, String> mLabelCache; - private final Collator mCollator; - - WidgetAndShortcutNameComparator(Context context) { - mManager = AppWidgetManagerCompat.getInstance(context); - mPackageManager = context.getPackageManager(); - mLabelCache = new HashMap<Object, String>(); - mCollator = Collator.getInstance(); - } - public final int compare(Object a, Object b) { - String labelA, labelB; - if (mLabelCache.containsKey(a)) { - labelA = mLabelCache.get(a); - } else { - labelA = (a instanceof AppWidgetProviderInfo) - ? mManager.loadLabel((AppWidgetProviderInfo) a) - : ((ResolveInfo) a).loadLabel(mPackageManager).toString().trim(); - mLabelCache.put(a, labelA); - } - if (mLabelCache.containsKey(b)) { - labelB = mLabelCache.get(b); - } else { - labelB = (b instanceof AppWidgetProviderInfo) - ? mManager.loadLabel((AppWidgetProviderInfo) b) - : ((ResolveInfo) b).loadLabel(mPackageManager).toString().trim(); - mLabelCache.put(b, labelB); - } - return mCollator.compare(labelA, labelB); - } - }; static boolean isValidProvider(AppWidgetProviderInfo provider) { return (provider != null) && (provider.provider != null) @@ -3821,4 +3637,20 @@ public class LauncherModel extends BroadcastReceiver public Callbacks getCallback() { return mCallbacks != null ? mCallbacks.get() : null; } + + /** + * @return {@link FolderInfo} if its already loaded. + */ + public FolderInfo findFolderById(Long folderId) { + synchronized (sBgLock) { + return sBgFolders.get(folderId); + } + } + + /** + * @return the looper for the worker thread which can be used to start background tasks. + */ + public static Looper getWorkerLooper() { + return sWorkerThread.getLooper(); + } } diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 1715b02bf..cc5e18bc1 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -16,9 +16,9 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentProviderOperation; @@ -30,6 +30,7 @@ import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; @@ -37,10 +38,13 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; -import android.provider.Settings; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.os.StrictMode; +import android.os.UserManager; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; @@ -50,52 +54,42 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.Thunk; import java.io.File; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; public class LauncherProvider extends ContentProvider { private static final String TAG = "Launcher.LauncherProvider"; private static final boolean LOGD = false; - private static final int DATABASE_VERSION = 20; + private static final int DATABASE_VERSION = 26; static final String OLD_AUTHORITY = "com.android.launcher2.settings"; static final String AUTHORITY = ProviderConfig.AUTHORITY; - // Should we attempt to load anything from the com.android.launcher2 provider? - static final boolean IMPORT_LAUNCHER2_DATABASE = false; - - static final String TABLE_FAVORITES = "favorites"; - static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens"; - static final String PARAMETER_NOTIFY = "notify"; - static final String UPGRADED_FROM_OLD_DATABASE = - "UPGRADED_FROM_OLD_DATABASE"; - static final String EMPTY_DATABASE_CREATED = - "EMPTY_DATABASE_CREATED"; + static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME; + static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME; + static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; - private LauncherProviderChangeListener mListener; + private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; - /** - * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when - * {@link AppWidgetHost#deleteHost()} is called during database creation. - * Use this to recall {@link AppWidgetHost#startListening()} if needed. - */ - static final Uri CONTENT_APPWIDGET_RESET_URI = - Uri.parse("content://" + AUTHORITY + "/appWidgetReset"); - - private DatabaseHelper mOpenHelper; - private static boolean sJustLoadedFromOldDb; + @Thunk LauncherProviderChangeListener mListener; + @Thunk DatabaseHelper mOpenHelper; @Override public boolean onCreate() { final Context context = getContext(); + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); mOpenHelper = new DatabaseHelper(context); + StrictMode.setThreadPolicy(oldPolicy); LauncherAppState.setLauncherProvider(this); return true; } @@ -106,6 +100,7 @@ public class LauncherProvider extends ContentProvider { public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) { mListener = listener; + mOpenHelper.mListener = mListener; } @Override @@ -133,7 +128,7 @@ public class LauncherProvider extends ContentProvider { return result; } - private static long dbInsertAndCheck(DatabaseHelper helper, + @Thunk static long dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { if (values == null) { throw new RuntimeException("Error: attempting to insert null values"); @@ -151,7 +146,8 @@ public class LauncherProvider extends ContentProvider { // In very limited cases, we support system|signature permission apps to add to the db String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD); - if (externalAdd != null && "true".equals(externalAdd)) { + final boolean isExternalAll = externalAdd != null && "true".equals(externalAdd); + if (isExternalAll) { if (!mOpenHelper.initializeExternalAdd(initialValues)) { return null; } @@ -160,10 +156,17 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); addModifiedTime(initialValues); final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); - if (rowId <= 0) return null; + if (rowId < 0) return null; uri = ContentUris.withAppendedId(uri, rowId); - sendNotify(uri); + notifyListeners(); + + if (isExternalAll) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } + } return uri; } @@ -188,7 +191,7 @@ public class LauncherProvider extends ContentProvider { db.endTransaction(); } - sendNotify(uri); + notifyListeners(); return values.length; } @@ -212,7 +215,7 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.delete(args.table, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } @@ -224,17 +227,80 @@ public class LauncherProvider extends ContentProvider { addModifiedTime(values); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.update(args.table, values, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } - private void sendNotify(Uri uri) { - String notify = uri.getQueryParameter(PARAMETER_NOTIFY); - if (notify == null || "true".equals(notify)) { - getContext().getContentResolver().notifyChange(uri, null); + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (Binder.getCallingUid() != Process.myUid()) { + return null; + } + + switch (method) { + case LauncherSettings.Settings.METHOD_GET_BOOLEAN: { + Bundle result = new Bundle(); + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, + getContext().getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) + .getBoolean(arg, extras.getBoolean( + LauncherSettings.Settings.EXTRA_DEFAULT_VALUE))); + return result; + } + case LauncherSettings.Settings.METHOD_SET_BOOLEAN: { + boolean value = extras.getBoolean(LauncherSettings.Settings.EXTRA_VALUE); + getContext().getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) + .edit().putBoolean(arg, value).apply(); + if (mListener != null) { + mListener.onSettingsChanged(arg, value); + } + Bundle result = new Bundle(); + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, value); + return result; + } + } + return null; + } + + /** + * Deletes any empty folder from the DB. + * @return Ids of deleted folders. + */ + public List<Long> deleteEmptyFolders() { + ArrayList<Long> folderIds = new ArrayList<Long>(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + // Select folders whose id do not match any container value. + String selection = LauncherSettings.Favorites.ITEM_TYPE + " = " + + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND " + + LauncherSettings.Favorites._ID + " NOT IN (SELECT " + + LauncherSettings.Favorites.CONTAINER + " FROM " + + TABLE_FAVORITES + ")"; + Cursor c = db.query(TABLE_FAVORITES, + new String[] {LauncherSettings.Favorites._ID}, + selection, null, null, null, null); + while (c.moveToNext()) { + folderIds.add(c.getLong(0)); + } + c.close(); + if (folderIds.size() > 0) { + db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, folderIds), null); + } + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + folderIds.clear(); + } finally { + db.endTransaction(); } + return folderIds; + } + private void notifyListeners() { // always notify the backup agent LauncherBackupAgentHelper.dataChanged(getContext()); if (mListener != null) { @@ -242,7 +308,7 @@ public class LauncherProvider extends ContentProvider { } } - private void addModifiedTime(ContentValues values) { + @Thunk static void addModifiedTime(ContentValues values) { values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); } @@ -258,32 +324,6 @@ public class LauncherProvider extends ContentProvider { return mOpenHelper.generateNewScreenId(); } - // This is only required one time while loading the workspace during the - // upgrade path, and should never be called from anywhere else. - public void updateMaxScreenId(long maxScreenId) { - mOpenHelper.updateMaxScreenId(maxScreenId); - } - - /** - * @param Should we load the old db for upgrade? first run only. - */ - synchronized public boolean justLoadedOldDb() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); - - boolean loadedOldDb = false || sJustLoadedFromOldDb; - - sJustLoadedFromOldDb = false; - if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) { - - SharedPreferences.Editor editor = sp.edit(); - editor.remove(UPGRADED_FROM_OLD_DATABASE); - editor.commit(); - loadedOldDb = true; - } - return loadedOldDb; - } - /** * Clears all the data for a fresh start. */ @@ -301,9 +341,10 @@ public class LauncherProvider extends ContentProvider { /** * Loads the default workspace based on the following priority scheme: - * 1) From a package provided by play store - * 2) From a partner configuration APK, already in the system image - * 3) The default configuration for the particular device + * 1) From the app restrictions + * 2) From a package provided by play store + * 3) From a partner configuration APK, already in the system image + * 4) The default configuration for the particular device */ synchronized public void loadDefaultFavoritesIfNecessary() { String spKey = LauncherAppState.getSharedPreferencesKey(); @@ -312,9 +353,11 @@ public class LauncherProvider extends ContentProvider { if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { Log.d(TAG, "loading default workspace"); - AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(), - mOpenHelper.mAppWidgetHost, mOpenHelper); - + AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(); + if (loader == null) { + loader = AutoInstallsLayout.get(getContext(), + mOpenHelper.mAppWidgetHost, mOpenHelper); + } if (loader == null) { final Partner partner = Partner.get(getContext().getPackageManager()); if (partner != null && partner.hasDefaultLayout()) { @@ -332,6 +375,10 @@ public class LauncherProvider extends ContentProvider { if (loader == null) { loader = getDefaultLayoutParser(); } + + // There might be some partially restored DB items, due to buggy restore logic in + // previous versions of launcher. + createEmptyDB(); // Populate favorites table with initial favorites if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) && usingExternallyProvidedLayout) { @@ -344,9 +391,43 @@ public class LauncherProvider extends ContentProvider { } } + /** + * Creates workspace loader from an XML resource listed in the app restrictions. + * + * @return the loader if the restrictions are set and the resource exists; null otherwise. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() { + // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return null; + } + + Context ctx = getContext(); + UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); + Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName()); + if (bundle == null) { + return null; + } + + String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME); + if (packageName != null) { + try { + Resources targetResources = ctx.getPackageManager() + .getResourcesForApplication(packageName); + return AutoInstallsLayout.get(ctx, packageName, targetResources, + mOpenHelper.mAppWidgetHost, mOpenHelper); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target package for restricted profile not found", e); + return null; + } + } + return null; + } + private DefaultLayoutParser getDefaultLayoutParser() { int defaultLayout = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().defaultLayoutId; + .getInvariantDeviceProfile().defaultLayoutId; return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost, mOpenHelper, getContext().getResources(), defaultLayout); } @@ -356,17 +437,15 @@ public class LauncherProvider extends ContentProvider { Uri.parse(getContext().getString(R.string.old_launcher_provider_uri))); } - private static interface ContentValuesCallback { - public void onRow(ContentValues values); + public void updateFolderItemsRank() { + mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false); } - private static boolean shouldImportLauncher2Database(Context context) { - boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet); - - // We don't import the old databse for tablets, as the grid size has changed. - return !isTablet && IMPORT_LAUNCHER2_DATABASE; + public void convertShortcutsToLauncherActivities() { + mOpenHelper.convertShortcutsToLauncherActivities(mOpenHelper.getWritableDatabase()); } + public void deleteDatabase() { // Are you sure? (y/n) final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -376,16 +455,19 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase.deleteDatabase(dbFile); } mOpenHelper = new DatabaseHelper(getContext()); + mOpenHelper.mListener = mListener; } private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { private final Context mContext; - private final AppWidgetHost mAppWidgetHost; + @Thunk final AppWidgetHost mAppWidgetHost; private long mMaxItemId = -1; private long mMaxScreenId = -1; private boolean mNewDbCreated = false; + @Thunk LauncherProviderChangeListener mListener; + DatabaseHelper(Context context) { super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION); mContext = context; @@ -405,17 +487,6 @@ public class LauncherProvider extends ContentProvider { return mNewDbCreated; } - /** - * Send notification that we've deleted the {@link AppWidgetHost}, - * probably as part of the initial database creation. The receiver may - * want to re-call {@link AppWidgetHost#startListening()} to ensure - * callbacks are correctly set. - */ - private void sendAppWidgetResetNotify() { - final ContentResolver resolver = mContext.getContentResolver(); - resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null); - } - @Override public void onCreate(SQLiteDatabase db) { if (LOGD) Log.d(TAG, "creating new launcher database"); @@ -450,53 +521,44 @@ public class LauncherProvider extends ContentProvider { "appWidgetProvider TEXT," + "modified INTEGER NOT NULL DEFAULT 0," + "restored INTEGER NOT NULL DEFAULT 0," + - "profileId INTEGER DEFAULT " + userSerialNumber + + "profileId INTEGER DEFAULT " + userSerialNumber + "," + + "rank INTEGER NOT NULL DEFAULT 0," + + "options INTEGER NOT NULL DEFAULT 0" + ");"); addWorkspacesTable(db); // Database was just created, so wipe any previous widgets if (mAppWidgetHost != null) { mAppWidgetHost.deleteHost(); - sendAppWidgetResetNotify(); - } - if (shouldImportLauncher2Database(mContext)) { - // Try converting the old database - ContentValuesCallback permuteScreensCb = new ContentValuesCallback() { - public void onRow(ContentValues values) { - int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER); - if (container == Favorites.CONTAINER_DESKTOP) { - int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN); - screen = (int) upgradeLauncherDb_permuteScreens(screen); - values.put(LauncherSettings.Favorites.SCREEN, screen); + /** + * Send notification that we've deleted the {@link AppWidgetHost}, + * probably as part of the initial database creation. The receiver may + * want to re-call {@link AppWidgetHost#startListening()} to ensure + * callbacks are correctly set. + */ + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + if (mListener != null) { + mListener.onAppWidgetHostReset(); } } - }; - Uri uri = Uri.parse("content://" + Settings.AUTHORITY + - "/old_favorites?notify=true"); - if (!convertDatabase(db, uri, permuteScreensCb, true)) { - // Try and upgrade from the Launcher2 db - uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri)); - if (!convertDatabase(db, uri, permuteScreensCb, false)) { - // If we fail, then set a flag to load the default workspace - setFlagEmptyDbCreated(); - return; - } - } - // Right now, in non-default workspace cases, we want to run the final - // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so - // set that flag too. - setFlagJustLoadedOldDb(); - } else { - // Fresh and clean launcher DB. - mMaxItemId = initializeMaxItemId(db); - setFlagEmptyDbCreated(); + }); } + + // Fresh and clean launcher DB. + mMaxItemId = initializeMaxItemId(db); + setFlagEmptyDbCreated(); + + // When a new DB is created, remove all previously stored managed profile information. + ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext); } private void addWorkspacesTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" + - LauncherSettings.WorkspaceScreens._ID + " INTEGER," + + LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," + LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + ");"); @@ -536,333 +598,120 @@ public class LauncherProvider extends ContentProvider { private void setFlagJustLoadedOldDb() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true); - editor.putBoolean(EMPTY_DATABASE_CREATED, false); - editor.commit(); + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); } private void setFlagEmptyDbCreated() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(EMPTY_DATABASE_CREATED, true); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false); - editor.commit(); - } - - // We rearrange the screens from the old launcher - // 12345 -> 34512 - private long upgradeLauncherDb_permuteScreens(long screen) { - if (screen >= 2) { - return screen - 2; - } else { - return screen + 3; - } - } - - private boolean convertDatabase(SQLiteDatabase db, Uri uri, - ContentValuesCallback cb, boolean deleteRows) { - if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade"); - boolean converted = false; - - final ContentResolver resolver = mContext.getContentResolver(); - Cursor cursor = null; - - try { - cursor = resolver.query(uri, null, null, null, null); - } catch (Exception e) { - // Ignore - } - - // We already have a favorites database in the old provider - if (cursor != null) { - try { - if (cursor.getCount() > 0) { - converted = copyFromCursor(db, cursor, cb) > 0; - if (converted && deleteRows) { - resolver.delete(uri, null, null); - } - } - } finally { - cursor.close(); - } - } - - if (converted) { - // Convert widgets from this import into widgets - if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade"); - convertWidgets(db); - - // Update max item id - mMaxItemId = initializeMaxItemId(db); - if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId); - } - - return converted; - } - - private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) { - final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); - final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); - final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); - final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); - final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); - final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); - final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); - final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); - final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); - final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); - final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); - - ContentValues[] rows = new ContentValues[c.getCount()]; - int i = 0; - while (c.moveToNext()) { - ContentValues values = new ContentValues(c.getColumnCount()); - values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex)); - values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex)); - values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); - values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex)); - values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); - values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex)); - values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex)); - values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex)); - values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex)); - values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); - values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex)); - values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex)); - values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex)); - values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); - values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex)); - if (cb != null) { - cb.onRow(values); - } - rows[i++] = values; - } - - int total = 0; - if (i > 0) { - db.beginTransaction(); - try { - int numValues = rows.length; - for (i = 0; i < numValues; i++) { - if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) { - return 0; - } else { - total++; - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - return total; + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); - - int version = oldVersion; - if (version < 3) { - // upgrade 1,2 -> 3 added appWidgetId column - db.beginTransaction(); - try { - // Insert new column for holding appWidgetIds - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;"); - db.setTransactionSuccessful(); - version = 3; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + switch (oldVersion) { + // The version cannot be lower that 12, as Launcher3 never supported a lower + // version of the DB. + case 12: { + // With the new shrink-wrapped and re-orderable workspaces, it makes sense + // to persist workspace screens and their relative order. + mMaxScreenId = 0; + addWorkspacesTable(db); } - - // Convert existing widgets only if table upgrade was successful - if (version == 3) { - convertWidgets(db); + case 13: { + db.beginTransaction(); + try { + // Insert new column for holding widget provider name + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN appWidgetProvider TEXT;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } } - } - - if (version < 4) { - version = 4; - } - - // Where's version 5? - // - Donut and sholes on 2.0 shipped with version 4 of launcher1. - // - Passion shipped on 2.1 with version 6 of launcher3 - // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1 - // but version 5 on there was the updateContactsShortcuts change - // which was version 6 in launcher 2 (first shipped on passion 2.1r1). - // The updateContactsShortcuts change is idempotent, so running it twice - // is okay so we'll do that when upgrading the devices that shipped with it. - if (version < 6) { - // We went from 3 to 5 screens. Move everything 1 to the right - db.beginTransaction(); - try { - db.execSQL("UPDATE favorites SET screen=(screen + 1);"); - db.setTransactionSuccessful(); - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 14: { + db.beginTransaction(); + try { + // Insert new column for holding update timestamp + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.execSQL("ALTER TABLE workspaceScreens " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } } - - // We added the fast track. - if (updateContactsShortcuts(db)) { - version = 6; + case 15: { + if (!addIntegerColumn(db, Favorites.RESTORED, 0)) { + // Old version remains, which means we wipe old data + break; + } } - } - - if (version < 7) { - // Version 7 gets rid of the special search widget. - convertWidgets(db); - version = 7; - } - - if (version < 8) { - // Version 8 (froyo) has the icons all normalized. This should - // already be the case in practice, but we now rely on it and don't - // resample the images each time. - normalizeIcons(db); - version = 8; - } - - if (version < 9) { - // The max id is not yet set at this point (onUpgrade is triggered in the ctor - // before it gets a change to get set, so we need to read it here when we use it) - if (mMaxItemId == -1) { - mMaxItemId = initializeMaxItemId(db); + case 16: { + // We use the db version upgrade here to identify users who may not have seen + // clings yet (because they weren't available), but for whom the clings are now + // available (tablet users). Because one of the possible cling flows (migration) + // is very destructive (wipes out workspaces), we want to prevent this from showing + // until clear data. We do so by marking that the clings have been shown. + LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); } - - // Add default hotseat icons - loadFavorites(db, new DefaultLayoutParser(mContext, mAppWidgetHost, this, - mContext.getResources(), R.xml.update_workspace)); - version = 9; - } - - // We bumped the version three time during JB, once to update the launch flags, once to - // update the override for the default launch animation and once to set the mimetype - // to improve startup performance - if (version < 12) { - // Contact shortcuts need a different set of flags to be launched now - // The updateContactsShortcuts change is idempotent, so we can keep using it like - // back in the Donut days - updateContactsShortcuts(db); - version = 12; - } - - if (version < 13) { - // With the new shrink-wrapped and re-orderable workspaces, it makes sense - // to persist workspace screens and their relative order. - mMaxScreenId = 0; - - // This will never happen in the wild, but when we switch to using workspace - // screen ids, redo the import from old launcher. - sJustLoadedFromOldDb = true; - - addWorkspacesTable(db); - version = 13; - } - - if (version < 14) { - db.beginTransaction(); - try { - // Insert new column for holding widget provider name - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN appWidgetProvider TEXT;"); - db.setTransactionSuccessful(); - version = 14; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 17: { + // No-op } - } - - if (version < 15) { - db.beginTransaction(); - try { - // Insert new column for holding update timestamp - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.execSQL("ALTER TABLE workspaceScreens " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 15; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 18: { + // Due to a data loss bug, some users may have items associated with screen ids + // which no longer exist. Since this can cause other problems, and since the user + // will never see these items anyway, we use database upgrade as an opportunity to + // clean things up. + removeOrphanedItems(db); } - } - - - if (version < 16) { - db.beginTransaction(); - try { - // Insert new column for holding restore status - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 16; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 19: { + // Add userId column + if (!addProfileColumn(db)) { + // Old version remains, which means we wipe old data + break; + } } - } - - if (version < 17) { - // We use the db version upgrade here to identify users who may not have seen - // clings yet (because they weren't available), but for whom the clings are now - // available (tablet users). Because one of the possible cling flows (migration) - // is very destructive (wipes out workspaces), we want to prevent this from showing - // until clear data. We do so by marking that the clings have been shown. - LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); - version = 17; - } - - if (version < 18) { - // No-op - version = 18; - } - - if (version < 19) { - // Due to a data loss bug, some users may have items associated with screen ids - // which no longer exist. Since this can cause other problems, and since the user - // will never see these items anyway, we use database upgrade as an opportunity to - // clean things up. - removeOrphanedItems(db); - version = 19; - } - - if (version < 20) { - // Add userId column - if (addProfileColumn(db)) { - version = 20; + case 20: + if (!updateFolderItemsRank(db, true)) { + break; + } + case 21: + // Recreate workspace table with screen id a primary key + if (!recreateWorkspaceTable(db)) { + break; + } + case 22: { + if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) { + // Old version remains, which means we wipe old data + break; + } + } + case 23: + // No-op + case 24: + ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext); + case 25: + convertShortcutsToLauncherActivities(db); + case 26: { + // DB Upgraded successfully + return; } - // else old version remains, which means we wipe old data } - if (version != DATABASE_VERSION) { - Log.w(TAG, "Destroying all old data."); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); - - onCreate(db); - } + // DB was not upgraded + Log.w(TAG, "Destroying all old data."); + createEmptyDB(db); } @Override @@ -873,7 +722,6 @@ public class LauncherProvider extends ContentProvider { createEmptyDB(db); } - /** * Clears all the data for a fresh start. */ @@ -883,20 +731,102 @@ public class LauncherProvider extends ContentProvider { onCreate(db); } - private boolean addProfileColumn(SQLiteDatabase db) { + /** + * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid + * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. + */ + @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) { db.beginTransaction(); + Cursor c = null; + SQLiteStatement updateStmt = null; + try { - UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); - // Default to the serial number of this user, for older - // shortcuts. - long userSerialNumber = userManager.getSerialNumberForUser( - UserHandleCompat.myUserHandle()); - // Insert new column for holding user serial number - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN profileId INTEGER DEFAULT " - + userSerialNumber + ";"); + // Only consider the primary user as other users can't have a shortcut. + long userSerial = UserManagerCompat.getInstance(mContext) + .getSerialNumberForUser(UserHandleCompat.myUserHandle()); + c = db.query(TABLE_FAVORITES, new String[] { + Favorites._ID, + Favorites.INTENT, + }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial, + null, null, null, null); + + updateStmt = db.compileStatement("UPDATE favorites SET itemType=" + + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?"); + + final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); + + while (c.moveToNext()) { + String intentDescription = c.getString(intentIndex); + Intent intent; + try { + intent = Intent.parseUri(intentDescription, 0); + } catch (URISyntaxException e) { + Log.e(TAG, "Unable to parse intent", e); + continue; + } + + if (!Utilities.isLauncherAppTarget(intent)) { + continue; + } + + long id = c.getLong(idIndex); + updateStmt.bindLong(1, id); + updateStmt.executeUpdateDelete(); + } db.setTransactionSuccessful(); } catch (SQLException ex) { + Log.w(TAG, "Error deduping shortcuts", ex); + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + if (updateStmt != null) { + updateStmt.close(); + } + } + } + + /** + * Recreates workspace table and migrates data to the new table. + */ + public boolean recreateWorkspaceTable(SQLiteDatabase db) { + db.beginTransaction(); + try { + Cursor c = db.query(TABLE_WORKSPACE_SCREENS, + new String[] {LauncherSettings.WorkspaceScreens._ID}, + null, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK); + ArrayList<Long> sortedIDs = new ArrayList<Long>(); + long maxId = 0; + try { + while (c.moveToNext()) { + Long id = c.getLong(0); + if (!sortedIDs.contains(id)) { + sortedIDs.add(id); + maxId = Math.max(maxId, id); + } + } + } finally { + c.close(); + } + + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); + addWorkspacesTable(db); + + // Add all screen ids back + int total = sortedIDs.size(); + for (int i = 0; i < total; i++) { + ContentValues values = new ContentValues(); + values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i)); + values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + addModifiedTime(values); + db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values); + } + db.setTransactionSuccessful(); + mMaxScreenId = maxId; + } catch (SQLException ex) { // Old version remains, which means we wipe old data Log.e(TAG, ex.getMessage(), ex); return false; @@ -906,140 +836,60 @@ public class LauncherProvider extends ContentProvider { return true; } - private boolean updateContactsShortcuts(SQLiteDatabase db) { - final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, - new int[] { Favorites.ITEM_TYPE_SHORTCUT }); - - Cursor c = null; - final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT"; + @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { db.beginTransaction(); try { - // Select and iterate through each matching widget - c = db.query(TABLE_FAVORITES, - new String[] { Favorites._ID, Favorites.INTENT }, - selectWhere, null, null, null, null); - if (c == null) return false; - - if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); + if (addRankColumn) { + // Insert new column for holding rank + db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;"); + } - final int idIndex = c.getColumnIndex(Favorites._ID); - final int intentIndex = c.getColumnIndex(Favorites.INTENT); + // Get a map for folder ID to folder width + Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" + + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" + + " GROUP BY container;", + new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); while (c.moveToNext()) { - long favoriteId = c.getLong(idIndex); - final String intentUri = c.getString(intentIndex); - if (intentUri != null) { - try { - final Intent intent = Intent.parseUri(intentUri, 0); - android.util.Log.d("Home", intent.toString()); - final Uri uri = intent.getData(); - if (uri != null) { - final String data = uri.toString(); - if ((Intent.ACTION_VIEW.equals(intent.getAction()) || - actionQuickContact.equals(intent.getAction())) && - (data.startsWith("content://contacts/people/") || - data.startsWith("content://com.android.contacts/" + - "contacts/lookup/"))) { - - final Intent newIntent = new Intent(actionQuickContact); - // When starting from the launcher, start in a new, cleared task - // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we - // clear the whole thing preemptively here since - // QuickContactActivity will finish itself when launching other - // detail activities. - newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_CLEAR_TASK); - newIntent.putExtra( - Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true); - newIntent.setData(uri); - // Determine the type and also put that in the shortcut - // (that can speed up launch a bit) - newIntent.setDataAndType(uri, newIntent.resolveType(mContext)); - - final ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.INTENT, - newIntent.toUri(0)); - - String updateWhere = Favorites._ID + "=" + favoriteId; - db.update(TABLE_FAVORITES, values, updateWhere, null); - } - } - } catch (RuntimeException ex) { - Log.e(TAG, "Problem upgrading shortcut", ex); - } catch (URISyntaxException e) { - Log.e(TAG, "Problem upgrading shortcut", e); - } - } + db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " + + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", + new Object[] {c.getLong(1) + 1, c.getLong(0)}); } + c.close(); db.setTransactionSuccessful(); } catch (SQLException ex) { - Log.w(TAG, "Problem while upgrading contacts", ex); + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); return false; } finally { db.endTransaction(); - if (c != null) { - c.close(); - } } - return true; } - private void normalizeIcons(SQLiteDatabase db) { - Log.d(TAG, "normalizing icons"); + private boolean addProfileColumn(SQLiteDatabase db) { + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + // Default to the serial number of this user, for older + // shortcuts. + long userSerialNumber = userManager.getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber); + } + private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { db.beginTransaction(); - Cursor c = null; - SQLiteStatement update = null; try { - boolean logged = false; - update = db.compileStatement("UPDATE favorites " - + "SET icon=? WHERE _id=?"); - - c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" + - Favorites.ICON_TYPE_BITMAP, null); - - final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); - final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON); - - while (c.moveToNext()) { - long id = c.getLong(idIndex); - byte[] data = c.getBlob(iconIndex); - try { - Bitmap bitmap = Utilities.createIconBitmap( - BitmapFactory.decodeByteArray(data, 0, data.length), - mContext); - if (bitmap != null) { - update.bindLong(1, id); - data = ItemInfo.flattenBitmap(bitmap); - if (data != null) { - update.bindBlob(2, data); - update.execute(); - } - bitmap.recycle(); - } - } catch (Exception e) { - if (!logged) { - Log.e(TAG, "Failed normalizing icon " + id, e); - } else { - Log.e(TAG, "Also failed normalizing icon " + id); - } - logged = true; - } - } + db.execSQL("ALTER TABLE favorites ADD COLUMN " + + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";"); db.setTransactionSuccessful(); } catch (SQLException ex) { - Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); + Log.e(TAG, ex.getMessage(), ex); + return false; } finally { db.endTransaction(); - if (update != null) { - update.close(); - } - if (c != null) { - c.close(); - } } + return true; } // Generates a new ID to use for an object in your database. This method should be only @@ -1075,23 +925,7 @@ public class LauncherProvider extends ContentProvider { } private long initializeMaxItemId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max item id"); - } - - return id; + return getMaxId(db, TABLE_FAVORITES); } // Generates a new ID to use for an workspace screen in your database. This method @@ -1104,128 +938,14 @@ public class LauncherProvider extends ContentProvider { throw new RuntimeException("Error: max screen id was not initialized"); } mMaxScreenId += 1; - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true); return mMaxScreenId; } - public void updateMaxScreenId(long maxScreenId) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true); - mMaxScreenId = maxScreenId; - } - private long initializeMaxScreenId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max screen id"); - } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true); - return id; + return getMaxId(db, TABLE_WORKSPACE_SCREENS); } - /** - * Upgrade existing clock and photo frame widgets into their new widget - * equivalents. - */ - private void convertWidgets(SQLiteDatabase db) { - final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); - final int[] bindSources = new int[] { - Favorites.ITEM_TYPE_WIDGET_CLOCK, - Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME, - Favorites.ITEM_TYPE_WIDGET_SEARCH, - }; - - final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources); - - Cursor c = null; - - db.beginTransaction(); - try { - // Select and iterate through each matching widget - c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE }, - selectWhere, null, null, null, null); - - if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); - - final ContentValues values = new ContentValues(); - while (c != null && c.moveToNext()) { - long favoriteId = c.getLong(0); - int favoriteType = c.getInt(1); - - // Allocate and update database with new appWidgetId - try { - int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); - - if (LOGD) { - Log.d(TAG, "allocated appWidgetId=" + appWidgetId - + " for favoriteId=" + favoriteId); - } - values.clear(); - values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); - values.put(Favorites.APPWIDGET_ID, appWidgetId); - - // Original widgets might not have valid spans when upgrading - if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { - values.put(LauncherSettings.Favorites.SPANX, 4); - values.put(LauncherSettings.Favorites.SPANY, 1); - } else { - values.put(LauncherSettings.Favorites.SPANX, 2); - values.put(LauncherSettings.Favorites.SPANY, 2); - } - - String updateWhere = Favorites._ID + "=" + favoriteId; - db.update(TABLE_FAVORITES, values, updateWhere, null); - - if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - new ComponentName("com.android.alarmclock", - "com.android.alarmclock.AnalogAppWidgetProvider")); - } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - new ComponentName("com.android.camera", - "com.android.camera.PhotoAppWidgetProvider")); - } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - getSearchWidgetProvider()); - } - } catch (RuntimeException ex) { - Log.e(TAG, "Problem allocating appWidgetId", ex); - } - } - - db.setTransactionSuccessful(); - } catch (SQLException ex) { - Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); - } finally { - db.endTransaction(); - if (c != null) { - c.close(); - } - } - - // Update max item id - mMaxItemId = initializeMaxItemId(db); - if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId); - } - - private boolean initializeExternalAdd(ContentValues values) { + @Thunk boolean initializeExternalAdd(ContentValues values) { // 1. Ensure that externally added items have a valid item id long id = generateNewItemId(); values.put(LauncherSettings.Favorites._ID, id); @@ -1312,7 +1032,7 @@ public class LauncherProvider extends ContentProvider { return rank; } - private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { + @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { ArrayList<Long> screenIds = new ArrayList<Long>(); // TODO: Use multiple loaders with fall-back and transaction. int count = loader.loadLayout(db, screenIds); @@ -1339,12 +1059,7 @@ public class LauncherProvider extends ContentProvider { return count; } - private ComponentName getSearchWidgetProvider() { - AppWidgetProviderInfo searchProvider = Utilities.getSearchWidgetProvider(mContext); - return (searchProvider == null) ? null : searchProvider.provider; - } - - private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { + @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { final ContentResolver resolver = mContext.getContentResolver(); Cursor c = null; int count = 0; @@ -1395,10 +1110,10 @@ public class LauncherProvider extends ContentProvider { int curY = 0; final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int width = (int) grid.numColumns; - final int height = (int) grid.numRows; - final int hotseatWidth = (int) grid.numHotseatIcons; + final InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int width = (int) profile.numColumns; + final int height = (int) profile.numRows; + final int hotseatWidth = (int) profile.numHotseatIcons; final HashSet<String> seenIntents = new HashSet<String>(c.getCount()); @@ -1540,7 +1255,7 @@ public class LauncherProvider extends ContentProvider { int hotseatX = hotseat.keyAt(idx); ContentValues values = hotseat.valueAt(idx); - if (hotseatX == grid.hotseatAllAppsRank) { + if (hotseatX == profile.hotseatAllAppsRank) { // let's drop this in the next available hole in the hotseat while (++hotseatX < hotseatWidth) { if (hotseat.get(hotseatX) == null) { @@ -1619,6 +1334,8 @@ public class LauncherProvider extends ContentProvider { } finally { db.endTransaction(); } + + updateFolderItemsRank(db, false); } } finally { c.close(); @@ -1639,18 +1356,24 @@ public class LauncherProvider extends ContentProvider { } /** - * Build a query string that will match any row where the column matches - * anything in the values list. + * @return the max _id in the provided table. */ - private static String buildOrWhereString(String column, int[] values) { - StringBuilder selectWhere = new StringBuilder(); - for (int i = values.length - 1; i >= 0; i--) { - selectWhere.append(column).append("=").append(values[i]); - if (i > 0) { - selectWhere.append(" OR "); - } + @Thunk static long getMaxId(SQLiteDatabase db, String table) { + Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); + // get the result + long id = -1; + if (c != null && c.moveToNext()) { + id = c.getLong(0); } - return selectWhere.toString(); + if (c != null) { + c.close(); + } + + if (id == -1) { + throw new RuntimeException("Error: could not query max id in " + table); + } + + return id; } static class SqlArguments { diff --git a/src/com/android/launcher3/LauncherProviderChangeListener.java b/src/com/android/launcher3/LauncherProviderChangeListener.java index 0de96fbc4..1b78e9c18 100644 --- a/src/com/android/launcher3/LauncherProviderChangeListener.java +++ b/src/com/android/launcher3/LauncherProviderChangeListener.java @@ -8,4 +8,8 @@ package com.android.launcher3; public interface LauncherProviderChangeListener { public void onLauncherProviderChange(); + + public void onSettingsChanged(String settings, boolean value); + + public void onAppWidgetHostReset(); } diff --git a/src/com/android/launcher3/LauncherScroller.java b/src/com/android/launcher3/LauncherScroller.java index 3bd0a78c4..a9b49556b 100644 --- a/src/com/android/launcher3/LauncherScroller.java +++ b/src/com/android/launcher3/LauncherScroller.java @@ -20,7 +20,6 @@ import android.animation.TimeInterpolator; import android.content.Context; import android.hardware.SensorManager; import android.os.Build; -import android.util.FloatMath; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -409,7 +408,7 @@ public class LauncherScroller { float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); - float hyp = FloatMath.sqrt(dx * dx + dy * dy); + float hyp = (float) Math.hypot(dx, dy); float ndx = dx / hyp; float ndy = dy / hyp; @@ -426,7 +425,7 @@ public class LauncherScroller { mMode = FLING_MODE; mFinished = false; - float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); + float velocity = (float) Math.hypot(velocityX, velocityY); mVelocity = velocity; mDuration = getSplineFlingDuration(velocity); diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 355370283..f2c85a195 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -19,17 +19,19 @@ package com.android.launcher3; import android.net.Uri; import android.provider.BaseColumns; +import com.android.launcher3.config.ProviderConfig; + /** * Settings related utilities. */ -class LauncherSettings { +public class LauncherSettings { /** Columns required on table staht will be subject to backup and restore. */ static interface ChangeLogColumns extends BaseColumns { /** * The time of the last update to this row. * <P>Type: INTEGER</P> */ - static final String MODIFIED = "modified"; + public static final String MODIFIED = "modified"; } static interface BaseLauncherColumns extends ChangeLogColumns { @@ -37,7 +39,7 @@ class LauncherSettings { * Descriptive name of the gesture that can be displayed to the user. * <P>Type: TEXT</P> */ - static final String TITLE = "title"; + public static final String TITLE = "title"; /** * The Intent URL of the gesture, describing what it points to. This @@ -45,58 +47,58 @@ class LauncherSettings { * an Intent that can be launched. * <P>Type: TEXT</P> */ - static final String INTENT = "intent"; + public static final String INTENT = "intent"; /** * The type of the gesture * * <P>Type: INTEGER</P> */ - static final String ITEM_TYPE = "itemType"; + public static final String ITEM_TYPE = "itemType"; /** * The gesture is an application */ - static final int ITEM_TYPE_APPLICATION = 0; + public static final int ITEM_TYPE_APPLICATION = 0; /** * The gesture is an application created shortcut */ - static final int ITEM_TYPE_SHORTCUT = 1; + public static final int ITEM_TYPE_SHORTCUT = 1; /** * The icon type. * <P>Type: INTEGER</P> */ - static final String ICON_TYPE = "iconType"; + public static final String ICON_TYPE = "iconType"; /** * The icon is a resource identified by a package name and an integer id. */ - static final int ICON_TYPE_RESOURCE = 0; + public static final int ICON_TYPE_RESOURCE = 0; /** * The icon is a bitmap. */ - static final int ICON_TYPE_BITMAP = 1; + public static final int ICON_TYPE_BITMAP = 1; /** * The icon package name, if icon type is ICON_TYPE_RESOURCE. * <P>Type: TEXT</P> */ - static final String ICON_PACKAGE = "iconPackage"; + public static final String ICON_PACKAGE = "iconPackage"; /** * The icon resource id, if icon type is ICON_TYPE_RESOURCE. * <P>Type: TEXT</P> */ - static final String ICON_RESOURCE = "iconResource"; + public static final String ICON_RESOURCE = "iconResource"; /** * The custom icon bitmap, if icon type is ICON_TYPE_BITMAP. * <P>Type: BLOB</P> */ - static final String ICON = "icon"; + public static final String ICON = "icon"; } /** @@ -104,72 +106,59 @@ class LauncherSettings { * * Tracks the order of workspace screens. */ - static final class WorkspaceScreens implements ChangeLogColumns { + public static final class WorkspaceScreens implements ChangeLogColumns { + + public static final String TABLE_NAME = "workspaceScreens"; + /** * The content:// style URL for this table */ static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_WORKSPACE_SCREENS + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The rank of this screen -- ie. how it is ordered relative to the other screens. * <P>Type: INTEGER</P> */ - static final String SCREEN_RANK = "screenRank"; + public static final String SCREEN_RANK = "screenRank"; } /** * Favorites. */ - static final class Favorites implements BaseLauncherColumns { - /** - * The content:// style URL for this table - */ - static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + public static final class Favorites implements BaseLauncherColumns { - /** - * The content:// style URL for this table - */ - static final Uri OLD_CONTENT_URI = Uri.parse("content://" + - LauncherProvider.OLD_AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + public static final String TABLE_NAME = "favorites"; /** - * The content:// style URL for this table. When this Uri is used, no notification is - * sent if the content changes. + * The content:// style URL for this table */ - static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=false"); + public static final Uri CONTENT_URI = Uri.parse("content://" + + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The content:// style URL for a given row, identified by its id. * * @param id The row id. - * @param notify True to send a notification is the content changes. * * @return The unique content URL for the specified row. */ - static Uri getContentUri(long id, boolean notify) { - return Uri.parse("content://" + LauncherProvider.AUTHORITY + - "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" + - LauncherProvider.PARAMETER_NOTIFY + "=" + notify); + static Uri getContentUri(long id) { + return Uri.parse("content://" + ProviderConfig.AUTHORITY + + "/" + TABLE_NAME + "/" + id); } /** * The container holding the favorite * <P>Type: INTEGER</P> */ - static final String CONTAINER = "container"; + public static final String CONTAINER = "container"; /** * The icon is a resource identified by a package name and an integer id. */ - static final int CONTAINER_DESKTOP = -100; - static final int CONTAINER_HOTSEAT = -101; + public static final int CONTAINER_DESKTOP = -100; + public static final int CONTAINER_HOTSEAT = -101; static final String containerToString(int container) { switch (container) { @@ -183,33 +172,33 @@ class LauncherSettings { * The screen holding the favorite (if container is CONTAINER_DESKTOP) * <P>Type: INTEGER</P> */ - static final String SCREEN = "screen"; + public static final String SCREEN = "screen"; /** * The X coordinate of the cell holding the favorite * (if container is CONTAINER_HOTSEAT or CONTAINER_HOTSEAT) * <P>Type: INTEGER</P> */ - static final String CELLX = "cellX"; + public static final String CELLX = "cellX"; /** * The Y coordinate of the cell holding the favorite * (if container is CONTAINER_DESKTOP) * <P>Type: INTEGER</P> */ - static final String CELLY = "cellY"; + public static final String CELLY = "cellY"; /** * The X span of the cell holding the favorite * <P>Type: INTEGER</P> */ - static final String SPANX = "spanX"; + public static final String SPANX = "spanX"; /** * The Y span of the cell holding the favorite * <P>Type: INTEGER</P> */ - static final String SPANY = "spanY"; + public static final String SPANY = "spanY"; /** * The profile id of the item in the cell. @@ -217,12 +206,12 @@ class LauncherSettings { * Type: INTEGER * </P> */ - static final String PROFILE_ID = "profileId"; + public static final String PROFILE_ID = "profileId"; /** * The favorite is a user created folder */ - static final int ITEM_TYPE_FOLDER = 2; + public static final int ITEM_TYPE_FOLDER = 2; /** * The favorite is a live folder @@ -231,26 +220,35 @@ class LauncherSettings { * exist within the launcher database will be ignored when loading. That said, these * entries in the database may still exist, and are not automatically stripped. */ + @Deprecated static final int ITEM_TYPE_LIVE_FOLDER = 3; /** * The favorite is a widget */ - static final int ITEM_TYPE_APPWIDGET = 4; + public static final int ITEM_TYPE_APPWIDGET = 4; + + /** + * The favorite is a custom widget provided by the launcher + */ + public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5; /** * The favorite is a clock */ + @Deprecated static final int ITEM_TYPE_WIDGET_CLOCK = 1000; /** * The favorite is a search widget */ + @Deprecated static final int ITEM_TYPE_WIDGET_SEARCH = 1001; /** * The favorite is a photo frame */ + @Deprecated static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; /** @@ -258,7 +256,7 @@ class LauncherSettings { * * <P>Type: INTEGER</P> */ - static final String APPWIDGET_ID = "appWidgetId"; + public static final String APPWIDGET_ID = "appWidgetId"; /** * The ComponentName of the widget provider @@ -281,6 +279,7 @@ class LauncherSettings { * live folders to find the content provider. * <P>Type: TEXT</P> */ + @Deprecated static final String URI = "uri"; /** @@ -290,12 +289,40 @@ class LauncherSettings { * @see android.provider.LiveFolders#DISPLAY_MODE_GRID * @see android.provider.LiveFolders#DISPLAY_MODE_LIST */ + @Deprecated static final String DISPLAY_MODE = "displayMode"; /** * Boolean indicating that his item was restored and not yet successfully bound. * <P>Type: INTEGER</P> */ - static final String RESTORED = "restored"; + public static final String RESTORED = "restored"; + + /** + * Indicates the position of the item inside an auto-arranged view like folder or hotseat. + * <p>Type: INTEGER</p> + */ + public static final String RANK = "rank"; + + /** + * Stores general flag based options for {@link ItemInfo}s. + * <p>Type: INTEGER</p> + */ + public static final String OPTIONS = "options"; + } + + /** + * Launcher settings + */ + public static final class Settings { + + public static final Uri CONTENT_URI = Uri.parse("content://" + + ProviderConfig.AUTHORITY + "/settings"); + + public static final String METHOD_GET_BOOLEAN = "get_boolean_setting"; + public static final String METHOD_SET_BOOLEAN = "set_boolean_setting"; + + public static final String EXTRA_VALUE = "value"; + public static final String EXTRA_DEFAULT_VALUE = "default_value"; } } diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java new file mode 100644 index 000000000..d69b7432d --- /dev/null +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -0,0 +1,762 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.util.Log; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.util.UiThreadCircularReveal; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetsContainerView; + +import java.util.HashMap; + +/** + * TODO: figure out what kind of tests we can write for this + * + * Things to test when changing the following class. + * - Home from workspace + * - from center screen + * - from other screens + * - Home from all apps + * - from center screen + * - from other screens + * - Back from all apps + * - from center screen + * - from other screens + * - Launch app from workspace and quit + * - with back + * - with home + * - Launch app from all apps and quit + * - with back + * - with home + * - Go to a screen that's not the default, then all + * apps, and launch and app, and go back + * - with back + * -with home + * - On workspace, long press power and go back + * - with back + * - with home + * - On all apps, long press power and go back + * - with back + * - with home + * - On workspace, power off + * - On all apps, power off + * - Launch an app and turn off the screen while in that app + * - Go back with home key + * - Go back with back key TODO: make this not go to workspace + * - From all apps + * - From workspace + * - Enter and exit car mode (becuase it causes an extra configuration changed) + * - From all apps + * - From the center workspace + * - From another workspace + */ +public class LauncherStateTransitionAnimation { + + /** + * Callbacks made during the state transition + */ + interface Callbacks { + public void onStateTransitionHideSearchBar(); + } + + /** + * Private callbacks made during transition setup. + */ + static abstract class PrivateTransitionCallbacks { + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0; + } + float getMaterialRevealViewStartFinalRadius() { + return 0; + } + AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView, + View buttonView) { + return null; + } + void onTransitionComplete() {} + } + + public static final String TAG = "LauncherStateTransitionAnimation"; + + // Flags to determine how to set the layers on views before the transition animation + public static final int BUILD_LAYER = 0; + public static final int BUILD_AND_SET_LAYER = 1; + public static final int SINGLE_FRAME_DELAY = 16; + + @Thunk Launcher mLauncher; + @Thunk Callbacks mCb; + @Thunk AnimatorSet mStateAnimation; + + public LauncherStateTransitionAnimation(Launcher l, Callbacks cb) { + mLauncher = l; + mCb = cb; + } + + /** + * Starts an animation to the apps view. + * + * @param startSearchAfterTransition Immediately starts app search after the transition to + * All Apps is completed. + */ + public void startAnimationToAllApps(final boolean animated, + final boolean startSearchAfterTransition) { + final AllAppsContainerView toView = mLauncher.getAppsView(); + final View buttonView = mLauncher.getAllAppsButton(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 1f; + } + @Override + public float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + allAppsButtonView.setVisibility(View.INVISIBLE); + } + public void onAnimationEnd(Animator animation) { + allAppsButtonView.setVisibility(View.VISIBLE); + } + }; + } + @Override + void onTransitionComplete() { + if (startSearchAfterTransition) { + toView.startAppsSearch(); + } + } + }; + // Only animate the search bar if animating from spring loaded mode back to all apps + startAnimationToOverlay(Workspace.State.NORMAL_HIDDEN, buttonView, toView, + toView.getContentView(), toView.getRevealView(), toView.getSearchBarView(), + animated, true /* hideSearchBar */, cb); + } + + /** + * Starts an animation to the widgets view. + */ + public void startAnimationToWidgets(final boolean animated) { + final WidgetsContainerView toView = mLauncher.getWidgetsView(); + final View buttonView = mLauncher.getWidgetsButton(); + + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.3f; + } + }; + startAnimationToOverlay(Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, + toView.getContentView(), toView.getRevealView(), null, animated, + true /* hideSearchBar */, cb); + } + + /** + * Starts and animation to the workspace from the current overlay view. + */ + public void startAnimationToWorkspace(final Launcher.State fromState, + final Workspace.State toWorkspaceState, final int toWorkspacePage, + final boolean animated, final Runnable onCompleteRunnable) { + if (toWorkspaceState != Workspace.State.NORMAL && + toWorkspaceState != Workspace.State.SPRING_LOADED && + toWorkspaceState != Workspace.State.OVERVIEW) { + Log.e(TAG, "Unexpected call to startAnimationToWorkspace"); + } + + if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) { + startAnimationToWorkspaceFromAllApps(toWorkspaceState, toWorkspacePage, + animated, onCompleteRunnable); + } else { + startAnimationToWorkspaceFromWidgets(toWorkspaceState, toWorkspacePage, + animated, onCompleteRunnable); + } + } + + /** + * Creates and starts a new animation to a particular overlay view. + */ + @SuppressLint("NewApi") + private void startAnimationToOverlay(final Workspace.State toWorkspaceState, + final View buttonView, final View toView, final View contentView, final View revealView, + final View overlaySearchBarView, final boolean animated, final boolean hideSearchBar, + final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_overlayItemsAlphaStagger); + + final View fromView = mLauncher.getWorkspace(); + + final HashMap<View, Integer> layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = buttonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, -1, + animated, overlaySearchBarView != null /* hasOverlaySearchBar */, layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Setup the reveal view animation + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.hypot(width / 2, height / 2); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(0f); + revealView.setTranslationY(0f); + revealView.setTranslationX(0f); + + // Calculate the final animation values + final float revealViewToAlpha; + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + buttonView, null); + revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView); + revealViewToYDrift = buttonViewToPanelDelta[1]; + revealViewToXDrift = buttonViewToPanelDelta[0]; + } else { + revealViewToAlpha = 0f; + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 0; + } + + // Create the animators + PropertyValuesHolder panelAlpha = + PropertyValuesHolder.ofFloat("alpha", revealViewToAlpha, 1f); + PropertyValuesHolder panelDriftY = + PropertyValuesHolder.ofFloat("translationY", revealViewToYDrift, 0); + PropertyValuesHolder panelDriftX = + PropertyValuesHolder.ofFloat("translationX", revealViewToXDrift, 0); + ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, + panelAlpha, panelDriftY, panelDriftX); + panelAlphaAndDrift.setDuration(revealDuration); + panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + // Play the animation + layerViews.put(revealView, BUILD_AND_SET_LAYER); + mStateAnimation.play(panelAlphaAndDrift); + + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(0f); + ObjectAnimator searchBarAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 0f, 1f); + searchBarAlpha.setDuration(100); + searchBarAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); + mStateAnimation.play(searchBarAlpha); + } + + // Setup the animation for the content view + contentView.setVisibility(View.VISIBLE); + contentView.setAlpha(0f); + contentView.setTranslationY(revealViewToYDrift); + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", + revealViewToYDrift, 0); + pageDrift.setDuration(revealDuration); + pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + pageDrift.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(pageDrift); + + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f); + itemsAlpha.setDuration(revealDuration); + itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + itemsAlpha.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(itemsAlpha); + + if (material) { + float startRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener( + revealView, buttonView); + Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2, + height / 2, startRadius, revealRadius); + reveal.setDuration(revealDuration); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + pCb.onTransitionComplete(); + } + + }); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // Dispatch the prepare transition signal + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + + + final AnimatorSet stateAnimation = mStateAnimation; + final Runnable startAnimRunnable = new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout/draw pass + if (mStateAnimation != stateAnimation) + return; + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + v.buildLayer(); + } + } + + // Focus the new view + toView.requestFocus(); + + mStateAnimation.start(); + } + }; + toView.bringToFront(); + toView.setVisibility(View.VISIBLE); + toView.post(startAnimRunnable); + } else { + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + + // Show the content view + contentView.setVisibility(View.VISIBLE); + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + pCb.onTransitionComplete(); + } + } + + /** + * Starts and animation to the workspace from the apps view. + */ + private void startAnimationToWorkspaceFromAllApps(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + AllAppsContainerView appsView = mLauncher.getAppsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + int[] mAllAppsToPanelDelta; + + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + // No alpha anim from all apps + return 1f; + } + @Override + float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + // We set the alpha instead of visibility to ensure that the focus does not + // get taken from the all apps view + allAppsButtonView.setVisibility(View.VISIBLE); + allAppsButtonView.setAlpha(0f); + } + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + + // Show the all apps button, and focus it + allAppsButtonView.setAlpha(1f); + } + }; + } + }; + // Only animate the search bar if animating to spring loaded mode from all apps + startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, + mLauncher.getAllAppsButton(), appsView, appsView.getContentView(), + appsView.getRevealView(), appsView.getSearchBarView(), animated, + onCompleteRunnable, cb); + } + + /** + * Starts and animation to the workspace from the widgets view. + */ + private void startAnimationToWorkspaceFromWidgets(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.3f; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View widgetsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + } + }; + } + }; + startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, + mLauncher.getWidgetsButton(), widgetsView, widgetsView.getContentView(), + widgetsView.getRevealView(), null, animated, onCompleteRunnable, cb); + } + + /** + * Creates and starts a new animation to the workspace. + */ + private void startAnimationToWorkspaceFromOverlay(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final View buttonView, final View fromView, + final View contentView, final View revealView, final View overlaySearchBarView, + final boolean animated, final Runnable onCompleteRunnable, + final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_overlayItemsAlphaStagger); + + final View toView = mLauncher.getWorkspace(); + + final HashMap<View, Integer> layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = buttonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, + toWorkspacePage, animated, overlaySearchBarView != null /* hasOverlaySearchBar */, + layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // hideAppsCustomizeHelper is called in some cases when it is already hidden + // don't perform all these no-op animations. In particularly, this was causing + // the all-apps button to pop in and out. + if (fromView.getVisibility() == View.VISIBLE) { + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.hypot(width / 2, height / 2); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(1f); + revealView.setTranslationY(0); + layerViews.put(revealView, BUILD_AND_SET_LAYER); + + // Calculate the final animation values + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + buttonView, null); + revealViewToYDrift = buttonViewToPanelDelta[1]; + revealViewToXDrift = buttonViewToPanelDelta[0]; + } else { + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 0; + } + + // The vertical motion of the apps panel should be delayed by one frame + // from the conceal animation in order to give the right feel. We correspondingly + // shorten the duration so that the slide and conceal end at the same time. + TimeInterpolator decelerateInterpolator = material ? + new LogDecelerateInterpolator(100, 0) : + new DecelerateInterpolator(1f); + ObjectAnimator panelDriftY = ObjectAnimator.ofFloat(revealView, "translationY", + 0, revealViewToYDrift); + panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftY.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftY); + + ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX", + 0, revealViewToXDrift); + panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftX.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftX); + + // Setup animation for the reveal panel alpha + final float revealViewToAlpha = !material ? 0f : + pCb.getMaterialRevealViewFinalAlpha(revealView); + if (revealViewToAlpha != 1f) { + ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha", + 1f, revealViewToAlpha); + panelAlpha.setDuration(material ? revealDuration : 150); + panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelAlpha); + } + + // Setup the animation for the content view + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", + 0, revealViewToYDrift); + contentView.setTranslationY(0); + pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); + pageDrift.setInterpolator(decelerateInterpolator); + pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + mStateAnimation.play(pageDrift); + + contentView.setAlpha(1f); + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f); + itemsAlpha.setDuration(100); + itemsAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(itemsAlpha); + + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(1f); + ObjectAnimator searchAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 1f, 0f); + searchAlpha.setDuration(material ? 100 : 150); + searchAlpha.setInterpolator(decelerateInterpolator); + searchAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); + layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); + mStateAnimation.play(searchAlpha); + } + + if (material) { + // Animate the all apps button + float finalRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = + pCb.getMaterialRevealViewAnimatorListener(revealView, buttonView); + Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2, + height / 2, revealRadius, finalRadius); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + reveal.setDuration(revealDuration); + reveal.setStartDelay(itemsAlphaStagger); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + // Reset page transforms + if (contentView != null) { + contentView.setTranslationX(0); + contentView.setTranslationY(0); + contentView.setAlpha(1); + } + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(1f); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + pCb.onTransitionComplete(); + } + }); + + final AnimatorSet stateAnimation = mStateAnimation; + final Runnable startAnimRunnable = new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout/draw pass + if (mStateAnimation != stateAnimation) + return; + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + v.buildLayer(); + } + } + mStateAnimation.start(); + } + }; + fromView.post(startAnimRunnable); + } else { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + pCb.onTransitionComplete(); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + } + + + /** + * Dispatches the prepare-transition event to suitable views. + */ + void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionPrepare(mLauncher, animated, + toWorkspace); + } + } + + /** + * Dispatches the start-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStart(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 0f); + } + + /** + * Dispatches the step-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStep(View v, float t) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStep(mLauncher, t); + } + } + + /** + * Dispatches the end-transition event to suitable views. + */ + void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionEnd(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 1f); + } + + /** + * Cancels the current animation. + */ + private void cancelAnimation() { + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + } +} diff --git a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java b/src/com/android/launcher3/LauncherTransitionable.java index 10fe013ee..49af6928a 100644 --- a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java +++ b/src/com/android/launcher3/LauncherTransitionable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * 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. @@ -16,15 +16,12 @@ package com.android.launcher3; -import android.content.Intent; - -public class LauncherWallpaperPickerActivity extends WallpaperPickerActivity { - @Override - public void startActivityForResultSafely(Intent intent, int requestCode) { - Utilities.startActivityForResultSafely(this, intent, requestCode); - } - @Override - public boolean enableRotation() { - return Utilities.isRotationEnabled(this); - } +/** + * An interface to get callbacks during a launcher transition. + */ +public interface LauncherTransitionable { + void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStep(Launcher l, float t); + void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); } diff --git a/src/com/android/launcher3/MemoryTracker.java b/src/com/android/launcher3/MemoryTracker.java index 2d37c809e..067a50f97 100644 --- a/src/com/android/launcher3/MemoryTracker.java +++ b/src/com/android/launcher3/MemoryTracker.java @@ -101,7 +101,7 @@ public class MemoryTracker extends Service { public void startTrackingProcess(int pid, String name, long start) { synchronized (mLock) { - final Long lpid = new Long(pid); + final Long lpid = Long.valueOf(pid); if (mPids.contains(lpid)) return; diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java deleted file mode 100644 index e59f6d81d..000000000 --- a/src/com/android/launcher3/PackageChangedReceiver.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.launcher3; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class PackageChangedReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, Intent intent) { - final String packageName = intent.getData().getSchemeSpecificPart(); - - if (packageName == null || packageName.length() == 0) { - // they sent us a bad intent - return; - } - // in rare cases the receiver races with the application to set up LauncherAppState - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - WidgetPreviewLoader.removePackageFromDb(app.getWidgetPreviewCacheDb(), packageName); - } -} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 7d65f4686..218c1a36f 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -19,16 +19,16 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.PointF; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -46,20 +46,12 @@ import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; -import java.util.ArrayList; +import com.android.launcher3.util.LauncherEdgeEffect; +import com.android.launcher3.util.Thunk; -interface Page { - public int getPageChildCount(); - public View getChildOnPageAt(int i); - public void removeAllViewsOnPage(); - public void removeViewOnPageAt(int i); - public int indexOfChildOnPage(View v); -} +import java.util.ArrayList; /** * An abstraction of the original Workspace which supports browsing through a @@ -74,28 +66,21 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private static final int MIN_LENGTH_FOR_FLING = 25; protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; - protected static final int OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION = 350; protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; protected static final float NANOTIME_DIV = 1000000000.0f; - private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; - private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; - private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; // The page is moved more than halfway, automatically move to the next page on touch up. private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; + private static final float MAX_SCROLL_PROGRESS = 1.0f; + // The following constants need to be scaled based on density. The scaled versions will be // assigned to the corresponding member variables below. private static final int FLING_THRESHOLD_VELOCITY = 500; private static final int MIN_SNAP_VELOCITY = 1500; private static final int MIN_FLING_VELOCITY = 250; - // We are disabling touch interaction of the widget region for factory ROM. - private static final boolean DISABLE_TOUCH_INTERACTION = false; - private static final boolean DISABLE_TOUCH_SIDE_PAGES = true; - private static final boolean DISABLE_FLING_TO_DELETE = true; - public static final int INVALID_RESTORE_PAGE = -1001; private boolean mFreeScroll = false; @@ -124,7 +109,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected LauncherScroller mScroller; private Interpolator mDefaultInterpolator; private VelocityTracker mVelocityTracker; - private int mPageSpacing = 0; + @Thunk int mPageSpacing = 0; private float mParentDownMotionX; private float mParentDownMotionY; @@ -156,7 +141,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected OnLongClickListener mLongClickListener; protected int mTouchSlop; - private int mPagingTouchSlop; private int mMaximumVelocity; protected int mPageLayoutWidthGap; protected int mPageLayoutHeightGap; @@ -164,15 +148,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected int mCellCountY = 0; protected boolean mCenterPagesVertically; protected boolean mAllowOverScroll = true; - protected int mUnboundedScrollX; protected int[] mTempVisiblePagesRange = new int[2]; protected boolean mForceDrawAllChildrenNextFrame; - private boolean mSpacePagesAutomatically = false; - - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise - // it is equal to the scaled overscroll position. We use a separate value so as to prevent - // the screens from continuing to translate beyond the normal bounds. - protected int mOverScrollX; protected static final int INVALID_POINTER = -1; @@ -180,37 +157,16 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private PageSwitchListener mPageSwitchListener; - protected ArrayList<Boolean> mDirtyPageContent; - - // If true, syncPages and syncPageItems will be called to refresh pages - protected boolean mContentIsRefreshable = true; - // If true, modify alpha of neighboring pages as user scrolls left/right protected boolean mFadeInAdjacentScreens = false; - // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding - // to switch to a new page - protected boolean mUsePagingTouchSlop = true; - - // If true, the subclass should directly update scrollX itself in its computeScroll method - // (SmoothPagedView does this) - protected boolean mDeferScrollUpdate = false; - protected boolean mDeferLoadAssociatedPagesUntilScrollCompletes = false; - protected boolean mIsPageMoving = false; - // All syncs and layout passes are deferred until data is ready. - protected boolean mIsDataReady = false; - - protected boolean mAllowLongPress = true; - private boolean mWasInOverscroll = false; // Page Indicator - private int mPageIndicatorViewId; - private PageIndicator mPageIndicator; - private boolean mAllowPagedViewAnimations = true; - + @Thunk int mPageIndicatorViewId; + @Thunk PageIndicator mPageIndicator; // The viewport whether the pages are to be contained (the actual view may be larger than the // viewport) private Rect mViewport = new Rect(); @@ -218,16 +174,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Reordering // 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 int REORDERING_DROP_REPOSITION_DURATION = 200; - protected int REORDERING_REORDER_REPOSITION_DURATION = 300; - protected int REORDERING_ZOOM_IN_OUT_DURATION = 250; - private int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; + private static int REORDERING_DROP_REPOSITION_DURATION = 200; + @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300; + private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; + private float mMinScale = 1f; private boolean mUseMinScale = false; protected View mDragView; - protected AnimatorSet mZoomInOutAnim; private Runnable mSidePageHoverRunnable; - private int mSidePageHoverIndex = -1; + @Thunk int mSidePageHoverIndex = -1; // This variable's scope is only for the duration of startReordering() and endReordering() private boolean mReorderingStarted = false; // This variable's scope is for the duration of startReordering() and after the zoomIn() @@ -239,30 +194,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private Runnable mPostReorderingPreZoomInRunnable; // Convenience/caching - private Matrix mTmpInvMatrix = new Matrix(); - private float[] mTmpPoint = new float[2]; - private int[] mTmpIntPoint = new int[2]; - private Rect mTmpRect = new Rect(); - private Rect mAltTmpRect = new Rect(); - - // Fling to delete - private int FLING_TO_DELETE_FADE_OUT_DURATION = 350; - private float FLING_TO_DELETE_FRICTION = 0.035f; - // The degrees specifies how much deviation from the up vector to still consider a fling "up" - private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f; - protected int mFlingToDeleteThresholdVelocity = -1400; - // Drag to delete - private boolean mDeferringForDelete = false; - private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; - private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350; - - // Drop to delete - private View mDeleteDropTarget; - - // Bouncer - private boolean mTopAlignPageWhenShrinkingForBouncer = false; + private static final Matrix sTmpInvMatrix = new Matrix(); + private static final float[] sTmpPoint = new float[2]; + private static final int[] sTmpIntPoint = new int[2]; + private static final Rect sTmpRect = new Rect(); protected final Rect mInsets = new Rect(); + protected final boolean mIsRtl; + + // Edge effect + private final LauncherEdgeEffect mEdgeGlowLeft = new LauncherEdgeEffect(); + private final LauncherEdgeEffect mEdgeGlowRight = new LauncherEdgeEffect(); public interface PageSwitchListener { void onPageSwitch(View newPage, int newPageIndex); @@ -290,6 +232,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc a.recycle(); setHapticFeedbackEnabled(false); + mIsRtl = Utilities.isRtl(getResources()); init(); } @@ -297,8 +240,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Initializes various states for this workspace. */ protected void init() { - mDirtyPageContent = new ArrayList<Boolean>(); - mDirtyPageContent.ensureCapacity(32); mScroller = new LauncherScroller(getContext()); setDefaultInterpolator(new ScrollInterpolator()); mCurrentPage = 0; @@ -306,18 +247,19 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledPagingTouchSlop(); - mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mDensity = getResources().getDisplayMetrics().density; - // Scale the fling-to-delete threshold by the density - mFlingToDeleteThresholdVelocity = - (int) (mFlingToDeleteThresholdVelocity * mDensity); - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); setOnHierarchyChangeListener(this); + setWillNotDraw(false); + } + + protected void setEdgeGlowColor(int color) { + mEdgeGlowLeft.setColor(color); + mEdgeGlowRight.setColor(color); } protected void setDefaultInterpolator(Interpolator interpolator) { @@ -333,7 +275,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc ViewGroup grandParent = (ViewGroup) parent.getParent(); if (mPageIndicator == null && mPageIndicatorViewId > -1) { mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId); - mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); + mPageIndicator.removeAllMarkers(true); ArrayList<PageIndicator.PageMarkerResources> markers = new ArrayList<PageIndicator.PageMarkerResources>(); @@ -341,7 +283,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc markers.add(getPageIndicatorMarker(i)); } - mPageIndicator.addMarkers(markers, mAllowPagedViewAnimations); + mPageIndicator.addMarkers(markers, true); OnClickListener listener = getPageIndicatorClickListener(); if (listener != null) { @@ -359,33 +301,31 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return null; } + @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); // Unhook the page indicator mPageIndicator = null; } - void setDeleteDropTarget(View v) { - mDeleteDropTarget = v; - } - // Convenience methods to map points from self to parent and vice versa - float[] mapPointFromViewToParent(View v, float x, float y) { - mTmpPoint[0] = x; - mTmpPoint[1] = y; - v.getMatrix().mapPoints(mTmpPoint); - mTmpPoint[0] += v.getLeft(); - mTmpPoint[1] += v.getTop(); - return mTmpPoint; - } - float[] mapPointFromParentToView(View v, float x, float y) { - mTmpPoint[0] = x - v.getLeft(); - mTmpPoint[1] = y - v.getTop(); - v.getMatrix().invert(mTmpInvMatrix); - mTmpInvMatrix.mapPoints(mTmpPoint); - return mTmpPoint; - } - - void updateDragViewTranslationDuringDrag() { + private float[] mapPointFromViewToParent(View v, float x, float y) { + sTmpPoint[0] = x; + sTmpPoint[1] = y; + v.getMatrix().mapPoints(sTmpPoint); + sTmpPoint[0] += v.getLeft(); + sTmpPoint[1] += v.getTop(); + return sTmpPoint; + } + private float[] mapPointFromParentToView(View v, float x, float y) { + sTmpPoint[0] = x - v.getLeft(); + sTmpPoint[1] = y - v.getTop(); + v.getMatrix().invert(sTmpInvMatrix); + sTmpInvMatrix.mapPoints(sTmpPoint); + return sTmpPoint; + } + + private void updateDragViewTranslationDuringDrag() { if (mDragView != null) { float x = (mLastMotionX - mDownMotionX) + (getScrollX() - mDownScrollX) + (mDragViewBaselineLeft - mDragView.getLeft()); @@ -453,33 +393,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } /** - * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. - */ - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); - } - - /** - * Called by subclasses to mark that data is ready, and that we can begin loading and laying - * out pages. - */ - protected void setDataIsReady() { - mIsDataReady = true; - } - - protected boolean isDataReady() { - return mIsDataReady; - } - - /** * Returns the index of the currently displayed page. - * - * @return The index of the currently displayed page. */ - int getCurrentPage() { + public int getCurrentPage() { return mCurrentPage; } + /** + * Returns the index of page to be shown immediately afterwards. + */ int getNextPage() { return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; } @@ -488,7 +410,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return getChildCount(); } - View getPageAt(int index) { + public View getPageAt(int index) { return getChildAt(index); } @@ -512,17 +434,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc forceFinishScroller(); } - /** - * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation - * {@link #updateCurrentPageScroll()} should be called, to correctly set the final state and - * re-enable scrolling. - */ - void stopScrolling() { - mCurrentPage = getNextPage(); - notifyPageSwitchListener(); - forceFinishScroller(); - } - private void abortScrollerAnimation(boolean resetNextPage) { mScroller.abortAnimation(); // We need to clean up the next page here to avoid computeScrollHelper from @@ -555,7 +466,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc /** * Sets the current page. */ - void setCurrentPage(int currentPage) { + public void setCurrentPage(int currentPage) { if (!mScroller.isFinished()) { abortScrollerAnimation(true); } @@ -647,37 +558,41 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public void scrollBy(int x, int y) { - scrollTo(mUnboundedScrollX + x, getScrollY() + y); + scrollTo(getScrollX() + x, getScrollY() + y); } @Override public void scrollTo(int x, int y) { // In free scroll mode, we clamp the scrollX if (mFreeScroll) { + // If the scroller is trying to move to a location beyond the maximum allowed + // in the free scroll mode, we make sure to end the scroll operation. + if (!mScroller.isFinished() && + (x > mFreeScrollMaxScrollX || x < mFreeScrollMinScrollX)) { + forceFinishScroller(); + } + x = Math.min(x, mFreeScrollMaxScrollX); x = Math.max(x, mFreeScrollMinScrollX); } - final boolean isRtl = isLayoutRtl(); - mUnboundedScrollX = x; - - boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0); - boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX); + boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); + boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); if (isXBeforeFirstPage) { - super.scrollTo(0, y); + super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); if (mAllowOverScroll) { mWasInOverscroll = true; - if (isRtl) { + if (mIsRtl) { overScroll(x - mMaxScrollX); } else { overScroll(x); } } } else if (isXAfterLastPage) { - super.scrollTo(mMaxScrollX, y); + super.scrollTo(mIsRtl ? 0 : mMaxScrollX, y); if (mAllowOverScroll) { mWasInOverscroll = true; - if (isRtl) { + if (mIsRtl) { overScroll(x); } else { overScroll(x - mMaxScrollX); @@ -688,7 +603,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc overScroll(0); mWasInOverscroll = false; } - mOverScrollX = x; super.scrollTo(x, y); } @@ -708,21 +622,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (am.isEnabled()) { - AccessibilityEvent ev = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); - ev.setItemCount(getChildCount()); - ev.setFromIndex(mCurrentPage); - ev.setToIndex(getNextPage()); - - final int action; - if (getNextPage() >= mCurrentPage) { - action = AccessibilityNodeInfo.ACTION_SCROLL_FORWARD; - } else { - action = AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD; + if (mCurrentPage != getNextPage()) { + AccessibilityEvent ev = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); + ev.setScrollable(true); + ev.setScrollX(getScrollX()); + ev.setScrollY(getScrollY()); + ev.setMaxScrollX(mMaxScrollX); + ev.setMaxScrollY(0); + + sendAccessibilityEventUnchecked(ev); } - - ev.setAction(action); - sendAccessibilityEventUnchecked(ev); } } @@ -731,8 +641,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (mScroller.computeScrollOffset()) { // Don't bother scrolling if the page does not need to be moved if (getScrollX() != mScroller.getCurrX() - || getScrollY() != mScroller.getCurrY() - || mOverScrollX != mScroller.getCurrX()) { + || getScrollY() != mScroller.getCurrY()) { float scaleX = mFreeScroll ? getScaleX() : 1f; int scrollX = (int) (mScroller.getCurrX() * (1 / scaleX)); scrollTo(scrollX, mScroller.getCurrY()); @@ -746,12 +655,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mNextPage = INVALID_PAGE; notifyPageSwitchListener(); - // Load the associated pages if necessary - if (mDeferLoadAssociatedPagesUntilScrollCompletes) { - loadAssociatedPages(mCurrentPage); - mDeferLoadAssociatedPagesUntilScrollCompletes = false; - } - // We don't want to trigger a page end moving unless the page has settled // and the user has stopped scrolling if (mTouchState == TOUCH_STATE_REST) { @@ -775,10 +678,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc computeScrollHelper(); } - protected boolean shouldSetTopAlignedPivotForWidget(int childIndex) { - return mTopAlignPageWhenShrinkingForBouncer; - } - public static class LayoutParams extends ViewGroup.LayoutParams { public boolean isFullScreenPage = false; @@ -789,15 +688,35 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc super(width, height); } + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + } + public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + public void addFullScreenPage(View page) { LayoutParams lp = generateDefaultLayoutParams(); lp.isFullScreenPage = true; @@ -810,7 +729,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (!mIsDataReady || getChildCount() == 0) { + if (getChildCount() == 0) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } @@ -914,27 +833,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } - if (mSpacePagesAutomatically) { - int spacing = (getViewportWidth() - mInsets.left - mInsets.right - - referenceChildWidth) / 2; - if (spacing >= 0) { - setPageSpacing(spacing); - } - mSpacePagesAutomatically = false; - } setMeasuredDimension(scaledWidthSize, scaledHeightSize); } - /** - * This method should be called once before first layout / measure pass. - */ - protected void setSinglePageInViewport() { - mSpacePagesAutomatically = true; - } - @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (!mIsDataReady || getChildCount() == 0) { + if (getChildCount() == 0) { return; } @@ -945,13 +849,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int offsetY = getViewportOffsetY(); // Update the viewport offsets - mViewport.offset(offsetX, offsetY); - - final boolean isRtl = isLayoutRtl(); + mViewport.offset(offsetX, offsetY); - final int startIndex = isRtl ? childCount - 1 : 0; - final int endIndex = isRtl ? -1 : childCount; - final int delta = isRtl ? -1 : 1; + final int startIndex = mIsRtl ? childCount - 1 : 0; + final int endIndex = mIsRtl ? -1 : childCount; + final int delta = mIsRtl ? -1 : 1; int verticalPadding = getPaddingTop() + getPaddingBottom(); @@ -959,8 +861,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc LayoutParams nextLp; int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft()); - if (mPageScrolls == null || getChildCount() != mChildCountOnLastLayout) { - mPageScrolls = new int[getChildCount()]; + if (mPageScrolls == null || childCount != mChildCountOnLastLayout) { + mPageScrolls = new int[childCount]; } for (int i = startIndex; i != endIndex; i += delta) { @@ -1003,24 +905,40 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc pageGap = getPaddingRight(); } - childLeft += childWidth + pageGap; + childLeft += childWidth + pageGap + getChildGap(); } } - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { - updateCurrentPageScroll(); - mFirstLayout = false; - } + final LayoutTransition transition = getLayoutTransition(); + // If the transition is running defer updating max scroll, as some empty pages could + // still be present, and a max scroll change could cause sudden jumps in scroll. + if (transition != null && transition.isRunning()) { + transition.addTransitionListener(new LayoutTransition.TransitionListener() { - if (childCount > 0) { - final int index = isLayoutRtl() ? 0 : childCount - 1; - mMaxScrollX = getScrollForPage(index); + @Override + public void startTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { } + + @Override + public void endTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { + // Wait until all transitions are complete. + if (!transition.isRunning()) { + transition.removeTransitionListener(this); + updateMaxScrollX(); + } + } + }); } else { - mMaxScrollX = 0; + updateMaxScrollX(); } - if (mScroller.isFinished() && mChildCountOnLastLayout != getChildCount() && - !mDeferringForDelete) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { + updateCurrentPageScroll(); + mFirstLayout = false; + } + + if (mScroller.isFinished() && mChildCountOnLastLayout != childCount) { if (mRestorePage != INVALID_RESTORE_PAGE) { setCurrentPage(mRestorePage); mRestorePage = INVALID_RESTORE_PAGE; @@ -1028,42 +946,37 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setCurrentPage(getNextPage()); } } - mChildCountOnLastLayout = getChildCount(); + mChildCountOnLastLayout = childCount; if (isReordering(true)) { updateDragViewTranslationDuringDrag(); } } - public void setPageSpacing(int pageSpacing) { - mPageSpacing = pageSpacing; - requestLayout(); + protected int getChildGap() { + return 0; } - protected void screenScrolled(int screenCenter) { - boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; - - if (mFadeInAdjacentScreens && !isInOverscroll) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (child != null) { - float scrollProgress = getScrollProgress(screenCenter, child, i); - float alpha = 1 - Math.abs(scrollProgress); - child.setAlpha(alpha); - } - } - invalidate(); + @Thunk void updateMaxScrollX() { + int childCount = getChildCount(); + if (childCount > 0) { + final int index = mIsRtl ? 0 : childCount - 1; + mMaxScrollX = getScrollForPage(index); + } else { + mMaxScrollX = 0; } } - protected void enablePagedViewAnimations() { - mAllowPagedViewAnimations = true; - - } - protected void disablePagedViewAnimations() { - mAllowPagedViewAnimations = false; + public void setPageSpacing(int pageSpacing) { + mPageSpacing = pageSpacing; + requestLayout(); } + /** + * Called when the center screen changes during scrolling. + */ + protected void screenScrolled(int screenCenter) { } + @Override public void onChildViewAdded(View parent, View child) { // Update the page indicator, we don't update the page indicator as we @@ -1072,7 +985,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int pageIndex = indexOfChild(child); mPageIndicator.addMarker(pageIndex, getPageIndicatorMarker(pageIndex), - mAllowPagedViewAnimations); + true); } // This ensures that when children are added, they get the correct transforms / alphas @@ -1093,7 +1006,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Update the page indicator, we don't update the page indicator as we // add/remove pages if (mPageIndicator != null && !isReordering(false)) { - mPageIndicator.removeMarker(index, mAllowPagedViewAnimations); + mPageIndicator.removeMarker(index, true); } } @@ -1115,7 +1028,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc public void removeViewAt(int index) { // XXX: We should find a better way to hook into this before the view // gets removed form its parent... - removeViewAt(index); + removeMarkerForView(index); super.removeViewAt(index); } @Override @@ -1123,7 +1036,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Update the page indicator, we don't update the page indicator as we // add/remove pages if (mPageIndicator != null) { - mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); + mPageIndicator.removeAllMarkers(true); } super.removeAllViewsInLayout(); @@ -1144,7 +1057,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected void getVisiblePages(int[] range) { final int pageCount = getChildCount(); - mTmpIntPoint[0] = mTmpIntPoint[1] = 0; + sTmpIntPoint[0] = sTmpIntPoint[1] = 0; range[0] = -1; range[1] = -1; @@ -1157,9 +1070,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc for (int i = 0; i < count; i++) { View currPage = getPageAt(i); - mTmpIntPoint[0] = 0; - Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); - if (mTmpIntPoint[0] > viewportWidth) { + sTmpIntPoint[0] = 0; + Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); + if (sTmpIntPoint[0] > viewportWidth) { if (range[0] == -1) { continue; } else { @@ -1167,9 +1080,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - mTmpIntPoint[0] = currPage.getMeasuredWidth(); - Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); - if (mTmpIntPoint[0] < 0) { + sTmpIntPoint[0] = currPage.getMeasuredWidth(); + Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); + if (sTmpIntPoint[0] < 0) { if (range[0] == -1) { continue; } else { @@ -1199,9 +1112,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final int pageCount = getChildCount(); if (pageCount > 0) { int halfScreenSize = getViewportWidth() / 2; - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. - // Otherwise it is equal to the scaled overscroll position. - int screenCenter = mOverScrollX + halfScreenSize; + int screenCenter = getScrollX() + halfScreenSize; if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can @@ -1242,6 +1153,46 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (getPageCount() > 0) { + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + Rect display = mViewport; + canvas.translate(display.left, display.top); + canvas.rotate(270); + + getEdgeVerticalPostion(sTmpIntPoint); + canvas.translate(display.top - sTmpIntPoint[1], 0); + mEdgeGlowLeft.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); + if (mEdgeGlowLeft.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + Rect display = mViewport; + canvas.translate(display.left + mPageScrolls[mIsRtl ? 0 : (getPageCount() - 1)], display.top); + canvas.rotate(90); + + getEdgeVerticalPostion(sTmpIntPoint); + canvas.translate(sTmpIntPoint[0] - display.top, -display.width()); + mEdgeGlowRight.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); + if (mEdgeGlowRight.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + } + } + + /** + * Returns the top and bottom position for the edge effect. + */ + protected abstract void getEdgeVerticalPostion(int[] pos); + + @Override public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { int page = indexToPage(indexOfChild(child)); if (page != mCurrentPage || !mScroller.isFinished()) { @@ -1346,7 +1297,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Return true if a tap at (x, y) should trigger a flip to the previous page. */ protected boolean hitsPreviousPage(float x, float y) { - if (isLayoutRtl()) { + if (mIsRtl) { return (x > (getViewportOffsetX() + getViewportWidth() - getPaddingRight() - mPageSpacing)); } @@ -1357,7 +1308,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Return true if a tap at (x, y) should trigger a flip to the next page. */ protected boolean hitsNextPage(float x, float y) { - if (isLayoutRtl()) { + if (mIsRtl) { return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); } return (x > (getViewportOffsetX() + getViewportWidth() - @@ -1366,17 +1317,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc /** Returns whether x and y originated within the buffered viewport */ private boolean isTouchPointInViewportWithBuffer(int x, int y) { - mTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, + sTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, mViewport.right + mViewport.width() / 2, mViewport.bottom); - return mTmpRect.contains(x, y); + return sTmpRect.contains(x, y); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DISABLE_TOUCH_INTERACTION) { - return false; - } - /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual @@ -1453,19 +1400,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - // check if this can be the beginning of a tap on the side of the pages - // to scroll the current page - if (!DISABLE_TOUCH_SIDE_PAGES) { - if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { - if (getChildCount() > 0) { - if (hitsPreviousPage(x, y)) { - mTouchState = TOUCH_STATE_PREV_PAGE; - } else if (hitsNextPage(x, y)) { - mTouchState = TOUCH_STATE_NEXT_PAGE; - } - } - } - } break; } @@ -1506,54 +1440,33 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); final int touchSlop = Math.round(touchSlopScale * mTouchSlop); - boolean xPaged = xDiff > mPagingTouchSlop; boolean xMoved = xDiff > touchSlop; - boolean yMoved = yDiff > touchSlop; - if (xMoved || xPaged || yMoved) { - if (mUsePagingTouchSlop ? xPaged : xMoved) { - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mTotalMotionX += Math.abs(mLastMotionX - x); - mLastMotionX = x; - mLastMotionXRemainder = 0; - mTouchX = getViewportOffsetX() + getScrollX(); - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - onScrollInteractionBegin(); - pageBeginMoving(); - } + if (xMoved) { + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mTotalMotionX += Math.abs(mLastMotionX - x); + mLastMotionX = x; + mLastMotionXRemainder = 0; + mTouchX = getViewportOffsetX() + getScrollX(); + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + onScrollInteractionBegin(); + pageBeginMoving(); } } - protected float getMaxScrollProgress() { - return 1.0f; - } - protected void cancelCurrentPageLongPress() { - if (mAllowLongPress) { - //mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); } } - protected float getBoundedScrollProgress(int screenCenter, View v, int page) { - final int halfScreenSize = getViewportWidth() / 2; - - screenCenter = Math.min(getScrollX() + halfScreenSize, screenCenter); - screenCenter = Math.max(halfScreenSize, screenCenter); - - return getScrollProgress(screenCenter, v, page); - } - protected float getScrollProgress(int screenCenter, View v, int page) { final int halfScreenSize = getViewportWidth() / 2; @@ -1563,7 +1476,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final int totalDistance; int adjacentPage = page + 1; - if ((delta < 0 && !isLayoutRtl()) || (delta > 0 && isLayoutRtl())) { + if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { adjacentPage = page - 1; } @@ -1574,8 +1487,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } float scrollProgress = delta / (totalDistance * 1.0f); - scrollProgress = Math.min(scrollProgress, getMaxScrollProgress()); - scrollProgress = Math.max(scrollProgress, - getMaxScrollProgress()); + scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); + scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS); return scrollProgress; } @@ -1598,7 +1511,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int scrollOffset = 0; LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isFullScreenPage) { - scrollOffset = isLayoutRtl() ? getPaddingRight() : getPaddingLeft(); + scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); } int baselineX = mPageScrolls[index] + scrollOffset + getViewportOffsetX(); @@ -1606,49 +1519,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - // This curve determines how the effect of scrolling over the limits of the page dimishes - // as the user pulls further and further from the bounds - private float overScrollInfluenceCurve(float f) { - f -= 1.0f; - return f * f * f + 1.0f; - } - - protected float acceleratedOverFactor(float amount) { - int screenSize = getViewportWidth(); - - // We want to reach the max over scroll effect when the user has - // over scrolled half the size of the screen - float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); - - if (f == 0) return 0; - - // Clamp this factor, f, to -1 < f < 1 - if (Math.abs(f) >= 1) { - f /= Math.abs(f); - } - return f; - } - protected void dampedOverScroll(float amount) { int screenSize = getViewportWidth(); - float f = (amount / screenSize); - - if (f == 0) return; - f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); - - // Clamp this factor, f, to -1 < f < 1 - if (Math.abs(f) >= 1) { - f /= Math.abs(f); - } - - int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); - if (amount < 0) { - mOverScrollX = overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); + if (f < 0) { + mEdgeGlowLeft.onPull(-f); + } else if (f > 0) { + mEdgeGlowRight.onPull(f); } else { - mOverScrollX = mMaxScrollX + overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); + return; } invalidate(); } @@ -1657,25 +1536,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc dampedOverScroll(amount); } - protected float maxOverScroll() { - // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not - // exceed). Used to find out how much extra wallpaper we need for the over scroll effect - float f = 1.0f; - f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); - return OVERSCROLL_DAMP_FACTOR * f; - } - - protected void enableFreeScroll() { + public void enableFreeScroll() { setEnableFreeScroll(true); } - protected void disableFreeScroll() { + public void disableFreeScroll() { setEnableFreeScroll(false); } void updateFreescrollBounds() { getFreeScrollPageRange(mTempVisiblePagesRange); - if (isLayoutRtl()) { + if (mIsRtl) { mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[1]); mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[0]); } else { @@ -1700,11 +1571,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setEnableOverscroll(!freeScroll); } - private void setEnableOverscroll(boolean enable) { + protected void setEnableOverscroll(boolean enable) { mAllowOverScroll = enable; } - int getNearestHoverOverPageIndex() { + private int getNearestHoverOverPageIndex() { if (mDragView != null) { int dragX = (int) (mDragView.getLeft() + (mDragView.getMeasuredWidth() / 2) + mDragView.getTranslationX()); @@ -1727,10 +1598,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public boolean onTouchEvent(MotionEvent ev) { - if (DISABLE_TOUCH_INTERACTION) { - return false; - } - super.onTouchEvent(ev); // Skip touch handling if there are no pages to swipe @@ -1785,12 +1652,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (Math.abs(deltaX) >= 1.0f) { mTouchX += deltaX; mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - if (!mDeferScrollUpdate) { - scrollBy((int) deltaX, 0); - if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); - } else { - invalidate(); - } + scrollBy((int) deltaX, 0); mLastMotionX = x; mLastMotionXRemainder = deltaX - (int) deltaX; } else { @@ -1811,19 +1673,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Find the closest page to the touch point final int dragViewIndex = indexOfChild(mDragView); - // Change the drag view if we are hovering over the drop target - boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget( - (int) mParentDownMotionX, (int) mParentDownMotionY); - setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); - if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); final int pageUnderPointIndex = getNearestHoverOverPageIndex(); - if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView) && - !isHoveringOverDelete) { + if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView)) { mTempVisiblePagesRange[0] = 0; mTempVisiblePagesRange[1] = getPageCount() - 1; getFreeScrollPageRange(mTempVisiblePagesRange); @@ -1870,9 +1726,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } removeView(mDragView); - onRemoveView(mDragView, false); addView(mDragView, pageUnderPointIndex); - onAddView(mDragView, pageUnderPointIndex); mSidePageHoverIndex = -1; if (mPageIndicator != null) { mPageIndicator.setActiveMarker(getNextPage()); @@ -1922,9 +1776,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // We give flings precedence over large moves, which is why we short-circuit our // test for a large move if a fling has been registered. That is, a large // move to the left and fling to the right will register as a fling to the right. - final boolean isRtl = isLayoutRtl(); - boolean isDeltaXLeft = isRtl ? deltaX > 0 : deltaX < 0; - boolean isVelocityXLeft = isRtl ? velocityX > 0 : velocityX < 0; + boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0; + boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0; if (((isSignificantMove && !isDeltaXLeft && !isFling) || (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; @@ -1983,19 +1836,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mParentDownMotionX = pt[0]; mParentDownMotionY = pt[1]; updateDragViewTranslationDuringDrag(); - boolean handledFling = false; - if (!DISABLE_FLING_TO_DELETE) { - // Check the velocity and see if we are flinging-to-delete - PointF flingToDeleteVector = isFlingingToDelete(); - if (flingToDeleteVector != null) { - onFlingToDelete(flingToDeleteVector); - handledFling = true; - } - } - if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX, - (int) mParentDownMotionY)) { - onDropToDelete(); - } } else { if (!mCancelTap) { onUnhandledTap(ev); @@ -2024,17 +1864,14 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return true; } - public void onFlingToDelete(View v) {} - public void onRemoveView(View v, boolean deletePermanently) {} - public void onRemoveViewAnimationCompleted() {} - public void onAddView(View v, int index) {} - private void resetTouchState() { releaseVelocityTracker(); endReordering(); mCancelTap = false; mTouchState = TOUCH_STATE_REST; mActivePointerId = INVALID_POINTER; + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); } /** @@ -2066,7 +1903,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); } if (hscroll != 0 || vscroll != 0) { - boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0) + boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) : (hscroll > 0 || vscroll > 0); if (isForwardScroll) { scrollRight(); @@ -2124,22 +1961,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected int getChildWidth(int index) { - return getPageAt(index).getMeasuredWidth(); - } - - int getPageNearestToPoint(float x) { - int index = 0; - for (int i = 0; i < getChildCount(); ++i) { - if (x < getChildAt(i).getRight() - getScrollX()) { - return index; - } else { - index++; - } - } - return Math.min(index, getChildCount() - 1); - } - int getPageNearestToCenterOfScreen() { int minDistanceFromScreenCenter = Integer.MAX_VALUE; int minDistanceFromScreenCenterIndex = -1; @@ -2159,20 +1980,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return minDistanceFromScreenCenterIndex; } - protected boolean isInOverScroll() { - return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); - } - - protected int getPageSnapDuration() { - if (isInOverScroll()) { - return OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION; - } - return PAGE_SNAP_ANIMATION_DURATION; - - } - protected void snapToDestination() { - snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()); + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); } private static class ScrollInterpolator implements Interpolator { @@ -2189,7 +1998,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // the screen has to travel, however, we don't want this duration to be effected in a // purely linear fashion. Instead, we use this method to moderate the effect that the distance // of travel has on the overall snap duration. - float distanceInfluenceForSnapDuration(float f) { + private float distanceInfluenceForSnapDuration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * Math.PI / 2.0f; return (float) Math.sin(f); @@ -2200,13 +2009,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int halfScreenSize = getViewportWidth() / 2; final int newX = getScrollForPage(whichPage); - int delta = newX - mUnboundedScrollX; + int delta = newX - getScrollX(); int duration = 0; - if (Math.abs(velocity) < mMinFlingVelocity || isInOverScroll()) { + if (Math.abs(velocity) < mMinFlingVelocity) { // If the velocity is low enough, then treat this more as an automatic page advance // as opposed to an apparent physical response to flinging - snapToPage(whichPage, getPageSnapDuration()); + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); return; } @@ -2229,12 +2038,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc snapToPage(whichPage, delta, duration); } - protected void snapToPage(int whichPage) { - snapToPage(whichPage, getPageSnapDuration()); + public void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); } protected void snapToPageImmediately(int whichPage) { - snapToPage(whichPage, getPageSnapDuration(), true, null); + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); } protected void snapToPage(int whichPage, int duration) { @@ -2250,8 +2059,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc whichPage = validateNewPage(whichPage); int newX = getScrollForPage(whichPage); - final int sX = mUnboundedScrollX; - final int delta = newX - sX; + final int delta = newX - getScrollX(); snapToPage(whichPage, delta, duration, immediate, interpolator); } @@ -2270,8 +2078,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc focusedChild.clearFocus(); } - sendScrollAccessibilityEvent(); - pageBeginMoving(); awakenScrollBars(duration); if (immediate) { @@ -2290,7 +2096,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mScroller.setInterpolator(mDefaultInterpolator); } - mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + mScroller.startScroll(getScrollX(), 0, delta, 0, duration); updatePageIndicator(); @@ -2299,9 +2105,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc computeScroll(); } - // Defer loading associated pages until the scroll settles - mDeferLoadAssociatedPagesUntilScrollCompletes = true; - mForceScreenScrolled = true; invalidate(); } @@ -2328,27 +2131,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return result; } - /** - * @return True is long presses are still allowed for the current touch - */ - public boolean allowLongPress() { - return mAllowLongPress; - } - @Override public boolean performLongClick() { mCancelTap = true; return super.performLongClick(); } - /** - * Set true to allow long-press events to be triggered, usually checked by - * {@link Launcher} to accept or block dpad-initiated long-presses. - */ - public void setAllowLongPress(boolean allowLongPress) { - mAllowLongPress = allowLongPress; - } - public static class SavedState extends BaseSavedState { int currentPage = -1; @@ -2356,7 +2144,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc super(superState); } - private SavedState(Parcel in) { + @Thunk SavedState(Parcel in) { super(in); currentPage = in.readInt(); } @@ -2379,113 +2167,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc }; } - protected void loadAssociatedPages(int page) { - loadAssociatedPages(page, false); - } - protected void loadAssociatedPages(int page, boolean immediateAndOnly) { - if (mContentIsRefreshable) { - final int count = getChildCount(); - if (page < count) { - int lowerPageBound = getAssociatedLowerPageBound(page); - int upperPageBound = getAssociatedUpperPageBound(page); - if (DEBUG) Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/" - + upperPageBound); - // First, clear any pages that should no longer be loaded - for (int i = 0; i < count; ++i) { - Page layout = (Page) getPageAt(i); - if ((i < lowerPageBound) || (i > upperPageBound)) { - if (layout.getPageChildCount() > 0) { - layout.removeAllViewsOnPage(); - } - mDirtyPageContent.set(i, true); - } - } - // Next, load any new pages - for (int i = 0; i < count; ++i) { - if ((i != page) && immediateAndOnly) { - continue; - } - if (lowerPageBound <= i && i <= upperPageBound) { - if (mDirtyPageContent.get(i)) { - syncPageItems(i, (i == page) && immediateAndOnly); - mDirtyPageContent.set(i, false); - } - } - } - } - } - } - - protected int getAssociatedLowerPageBound(int page) { - return Math.max(0, page - 1); - } - protected int getAssociatedUpperPageBound(int page) { - final int count = getChildCount(); - return Math.min(page + 1, count - 1); - } - - /** - * This method is called ONLY to synchronize the number of pages that the paged view has. - * To actually fill the pages with information, implement syncPageItems() below. It is - * guaranteed that syncPageItems() will be called for a particular page before it is shown, - * and therefore, individual page items do not need to be updated in this method. - */ - public abstract void syncPages(); - - /** - * This method is called to synchronize the items that are on a particular page. If views on - * the page can be reused, then they should be updated within this method. - */ - public abstract void syncPageItems(int page, boolean immediate); - - protected void invalidatePageData() { - invalidatePageData(-1, false); - } - protected void invalidatePageData(int currentPage) { - invalidatePageData(currentPage, false); - } - protected void invalidatePageData(int currentPage, boolean immediateAndOnly) { - if (!mIsDataReady) { - return; - } - - if (mContentIsRefreshable) { - // Force all scrolling-related behavior to end - forceFinishScroller(); - - // Update all the pages - syncPages(); - - // We must force a measure after we've loaded the pages to update the content width and - // to determine the full scroll width - measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); - - // Set a new page as the current page if necessary - if (currentPage > -1) { - setCurrentPage(Math.min(getPageCount() - 1, currentPage)); - } - - // Mark each of the pages as dirty - final int count = getChildCount(); - mDirtyPageContent.clear(); - for (int i = 0; i < count; ++i) { - mDirtyPageContent.add(true); - } - - // Load any pages that are necessary for the current window of views - loadAssociatedPages(mCurrentPage, immediateAndOnly); - requestLayout(); - } - if (isPageMoving()) { - // If the page is moving, then snap it to the final position to ensure we don't get - // stuck between pages - snapToDestination(); - } - } - // Animate the drag view back to the original position - void animateDragViewToOriginalPosition() { + private void animateDragViewToOriginalPosition() { if (mDragView != null) { AnimatorSet anim = new AnimatorSet(); anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); @@ -2504,7 +2187,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected void onStartReordering() { + public void onStartReordering() { // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) mTouchState = TOUCH_STATE_REORDERING; mIsReordering = true; @@ -2514,7 +2197,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc invalidate(); } - private void onPostReorderingAnimationCompleted() { + @Thunk void onPostReorderingAnimationCompleted() { // Trigger the callback when reordering has settled --mPostReorderingPreZoomInRemainingAnimationCount; if (mPostReorderingPreZoomInRunnable != null && @@ -2524,7 +2207,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected void onEndReordering() { + public void onEndReordering() { mIsReordering = false; } @@ -2574,281 +2257,26 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc onEndReordering(); } }; - if (!mDeferringForDelete) { - mPostReorderingPreZoomInRunnable = new Runnable() { - public void run() { - onCompleteRunnable.run(); - enableFreeScroll(); - }; - }; - - mPostReorderingPreZoomInRemainingAnimationCount = - NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; - // Snap to the current page - snapToPage(indexOfChild(mDragView), 0); - // Animate the drag view back to the front position - animateDragViewToOriginalPosition(); - } else { - // Handled in post-delete-animation-callbacks - } - } - - /* - * Flinging to delete - IN PROGRESS - */ - private PointF isFlingingToDelete() { - ViewConfiguration config = ViewConfiguration.get(getContext()); - mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); - - if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { - // Do a quick dot product test to ensure that we are flinging upwards - PointF vel = new PointF(mVelocityTracker.getXVelocity(), - mVelocityTracker.getYVelocity()); - PointF upVec = new PointF(0f, -1f); - float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / - (vel.length() * upVec.length())); - if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) { - return vel; - } - } - return null; - } - /** - * Creates an animation from the current drag view along its current velocity vector. - * For this animation, the alpha runs for a fixed duration and we update the position - * progressively. - */ - private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { - private View mDragView; - private PointF mVelocity; - private Rect mFrom; - private long mPrevTime; - private float mFriction; - - private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); - - public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, - long startTime, float friction) { - mDragView = dragView; - mVelocity = vel; - mFrom = from; - mPrevTime = startTime; - mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction); - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - float t = ((Float) animation.getAnimatedValue()).floatValue(); - long curTime = AnimationUtils.currentAnimationTimeMillis(); - - mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); - mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); - - mDragView.setTranslationX(mFrom.left); - mDragView.setTranslationY(mFrom.top); - mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); - - mVelocity.x *= mFriction; - mVelocity.y *= mFriction; - mPrevTime = curTime; - } - }; - - private static final int ANIM_TAG_KEY = 100; - - private Runnable createPostDeleteAnimationRunnable(final View dragView) { - return new Runnable() { - @Override + mPostReorderingPreZoomInRunnable = new Runnable() { public void run() { - int dragViewIndex = indexOfChild(dragView); - - // For each of the pages around the drag view, animate them from the previous - // position to the new position in the layout (as a result of the drag view moving - // in the layout) - // NOTE: We can make an assumption here because we have side-bound pages that we - // will always have pages to animate in from the left - getFreeScrollPageRange(mTempVisiblePagesRange); - boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); - boolean slideFromLeft = (isLastWidgetPage || - dragViewIndex > mTempVisiblePagesRange[0]); - - // Setup the scroll to the correct page before we swap the views - if (slideFromLeft) { - snapToPageImmediately(dragViewIndex - 1); - } - - int firstIndex = (isLastWidgetPage ? 0 : mTempVisiblePagesRange[0]); - int lastIndex = Math.min(mTempVisiblePagesRange[1], getPageCount() - 1); - int lowerIndex = (slideFromLeft ? firstIndex : dragViewIndex + 1 ); - int upperIndex = (slideFromLeft ? dragViewIndex - 1 : lastIndex); - ArrayList<Animator> animations = new ArrayList<Animator>(); - for (int i = lowerIndex; i <= upperIndex; ++i) { - View v = getChildAt(i); - // dragViewIndex < pageUnderPointIndex, so after we remove the - // drag view all subsequent views to pageUnderPointIndex will - // shift down. - int oldX = 0; - int newX = 0; - if (slideFromLeft) { - if (i == 0) { - // Simulate the page being offscreen with the page spacing - oldX = getViewportOffsetX() + getChildOffset(i) - getChildWidth(i) - - mPageSpacing; - } else { - oldX = getViewportOffsetX() + getChildOffset(i - 1); - } - newX = getViewportOffsetX() + getChildOffset(i); - } else { - oldX = getChildOffset(i) - getChildOffset(i - 1); - newX = 0; - } - - // Animate the view translation from its old position to its new - // position - AnimatorSet anim = (AnimatorSet) v.getTag(); - if (anim != null) { - anim.cancel(); - } - - // Note: Hacky, but we want to skip any optimizations to not draw completely - // hidden views - v.setAlpha(Math.max(v.getAlpha(), 0.01f)); - v.setTranslationX(oldX - newX); - anim = new AnimatorSet(); - anim.playTogether( - ObjectAnimator.ofFloat(v, "translationX", 0f), - ObjectAnimator.ofFloat(v, "alpha", 1f)); - animations.add(anim); - v.setTag(ANIM_TAG_KEY, anim); - } - - AnimatorSet slideAnimations = new AnimatorSet(); - slideAnimations.playTogether(animations); - slideAnimations.setDuration(DELETE_SLIDE_IN_SIDE_PAGE_DURATION); - slideAnimations.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mDeferringForDelete = false; - onEndReordering(); - onRemoveViewAnimationCompleted(); - } - }); - slideAnimations.start(); - - removeView(dragView); - onRemoveView(dragView, true); - } - }; - } - - public void onFlingToDelete(PointF vel) { - final long startTime = AnimationUtils.currentAnimationTimeMillis(); - - // NOTE: Because it takes time for the first frame of animation to actually be - // called and we expect the animation to be a continuation of the fling, we have - // to account for the time that has elapsed since the fling finished. And since - // we don't have a startDelay, we will always get call to update when we call - // start() (which we want to ignore). - final TimeInterpolator tInterpolator = new TimeInterpolator() { - private int mCount = -1; - private long mStartTime; - private float mOffset; - /* Anonymous inner class ctor */ { - mStartTime = startTime; - } - - @Override - public float getInterpolation(float t) { - if (mCount < 0) { - mCount++; - } else if (mCount == 0) { - mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - - mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION); - mCount++; - } - return Math.min(1f, mOffset + t); - } + onCompleteRunnable.run(); + enableFreeScroll(); + }; }; - final Rect from = new Rect(); - final View dragView = mDragView; - from.left = (int) dragView.getTranslationX(); - from.top = (int) dragView.getTranslationY(); - AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, - from, startTime, FLING_TO_DELETE_FRICTION); - - final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); - - // Create and start the animation - ValueAnimator mDropAnim = new ValueAnimator(); - mDropAnim.setInterpolator(tInterpolator); - mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION); - mDropAnim.setFloatValues(0f, 1f); - mDropAnim.addUpdateListener(updateCb); - mDropAnim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - onAnimationEndRunnable.run(); - } - }); - mDropAnim.start(); - mDeferringForDelete = true; - } - - /* Drag to delete */ - private boolean isHoveringOverDeleteDropTarget(int x, int y) { - if (mDeleteDropTarget != null) { - mAltTmpRect.set(0, 0, 0, 0); - View parent = (View) mDeleteDropTarget.getParent(); - if (parent != null) { - parent.getGlobalVisibleRect(mAltTmpRect); - } - mDeleteDropTarget.getGlobalVisibleRect(mTmpRect); - mTmpRect.offset(-mAltTmpRect.left, -mAltTmpRect.top); - return mTmpRect.contains(x, y); - } - return false; + mPostReorderingPreZoomInRemainingAnimationCount = + NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; + // Snap to the current page + snapToPage(indexOfChild(mDragView), 0); + // Animate the drag view back to the front position + animateDragViewToOriginalPosition(); } - protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) {} - - private void onDropToDelete() { - final View dragView = mDragView; - - final float toScale = 0f; - final float toAlpha = 0f; - - // Create and start the complex animation - ArrayList<Animator> animations = new ArrayList<Animator>(); - AnimatorSet motionAnim = new AnimatorSet(); - motionAnim.setInterpolator(new DecelerateInterpolator(2)); - motionAnim.playTogether( - ObjectAnimator.ofFloat(dragView, "scaleX", toScale), - ObjectAnimator.ofFloat(dragView, "scaleY", toScale)); - animations.add(motionAnim); - - AnimatorSet alphaAnim = new AnimatorSet(); - alphaAnim.setInterpolator(new LinearInterpolator()); - alphaAnim.playTogether( - ObjectAnimator.ofFloat(dragView, "alpha", toAlpha)); - animations.add(alphaAnim); - - final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); - - AnimatorSet anim = new AnimatorSet(); - anim.playTogether(animations); - anim.setDuration(DRAG_TO_DELETE_FADE_OUT_DURATION); - anim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - onAnimationEndRunnable.run(); - } - }); - anim.start(); - - mDeferringForDelete = true; - } + private static final int ANIM_TAG_KEY = 100; /* Accessibility */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); @@ -2859,6 +2287,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (getCurrentPage() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } + info.setClassName(getClass().getName()); + + // Accessibility-wise, PagedView doesn't support long click, so disabling it. + // Besides disabling the accessibility long-click, this also prevents this view from getting + // accessibility focus. + info.setLongClickable(false); + if (Utilities.isLmpOrAbove()) { + info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); + } } @Override @@ -2872,7 +2309,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setScrollable(true); + event.setScrollable(getPageCount() > 1); } @Override diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java deleted file mode 100644 index 2d9e10b9d..000000000 --- a/src/com/android/launcher3/PagedViewCellLayout.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (C) 2010 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.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayout extends ViewGroup implements Page { - static final String TAG = "PagedViewCellLayout"; - - private int mCellCountX; - private int mCellCountY; - private int mOriginalCellWidth; - private int mOriginalCellHeight; - private int mCellWidth; - private int mCellHeight; - private int mOriginalWidthGap; - private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; - protected PagedViewCellLayoutChildren mChildren; - - public PagedViewCellLayout(Context context) { - this(context, null); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setAlwaysDrawnWithCacheEnabled(false); - - // setup default cell parameters - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mOriginalCellWidth = mCellWidth = grid.cellWidthPx; - mOriginalCellHeight = mCellHeight = grid.cellHeightPx; - mCellCountX = (int) grid.numColumns; - mCellCountY = (int) grid.numRows; - mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1; - - mChildren = new PagedViewCellLayoutChildren(context); - mChildren.setCellDimensions(mCellWidth, mCellHeight); - mChildren.setGap(mWidthGap, mHeightGap); - - addView(mChildren); - } - - public int getCellWidth() { - return mCellWidth; - } - - public int getCellHeight() { - return mCellHeight; - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public boolean addViewToCellLayout(View child, int index, int childId, - PagedViewCellLayout.LayoutParams params) { - final PagedViewCellLayout.LayoutParams lp = params; - - // Generate an id for each view, this assumes we have at most 256x256 cells - // per workspace screen - if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) && - lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) { - // If the horizontal or vertical span is set to -1, it is taken to - // mean that it spans the extent of the CellLayout - if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX; - if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY; - - child.setId(childId); - mChildren.addView(child, index, lp); - - return true; - } - return false; - } - - @Override - public void removeAllViewsOnPage() { - mChildren.removeAllViews(); - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - mChildren.removeViewAt(index); - } - - /** - * Clears all the key listeners for the individual icons. - */ - public void resetChildrenOnKeyListeners() { - int childCount = mChildren.getChildCount(); - for (int j = 0; j < childCount; ++j) { - mChildren.getChildAt(j).setOnKeyListener(null); - } - } - - @Override - public int getPageChildCount() { - return mChildren.getChildCount(); - } - - public PagedViewCellLayoutChildren getChildrenLayout() { - return mChildren; - } - - @Override - public View getChildOnPageAt(int i) { - return mChildren.getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return mChildren.indexOfChild(v); - } - - public int getCellCountX() { - return mCellCountX; - } - - public int getCellCountY() { - return mCellCountY; - } - - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - int numWidthGaps = mCellCountX - 1; - int numHeightGaps = mCellCountY - 1; - - if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { - int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); - int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); - int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth); - int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight); - mWidthGap = numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0; - mHeightGap = numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0; - - mChildren.setGap(mWidthGap, mHeightGap); - } else { - mWidthGap = mOriginalWidthGap; - mHeightGap = mOriginalHeightGap; - } - - // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY - int newWidth = widthSpecSize; - int newHeight = heightSpecSize; - if (widthSpecMode == MeasureSpec.AT_MOST) { - newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) + - ((mCellCountX - 1) * mWidthGap); - newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) + - ((mCellCountY - 1) * mHeightGap); - setMeasuredDimension(newWidth, newHeight); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - int childWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() - - getPaddingRight(), MeasureSpec.EXACTLY); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() - - getPaddingBottom(), MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(newWidth, newHeight); - } - - int getContentWidth() { - return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight(); - } - - int getContentHeight() { - if (mCellCountY > 0) { - return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap); - } - return 0; - } - - int getWidthBeforeFirstLayout() { - if (mCellCountX > 0) { - return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap); - } - return 0; - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - child.layout(getPaddingLeft(), getPaddingTop(), - r - l - getPaddingRight(), b - t - getPaddingBottom()); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX()); - if (numRows < getCellCountY()) { - // Add a little bit of buffer if there is room for another row - bottom += mCellHeight / 2; - } - result = result || (event.getY() < bottom); - } - return result; - } - - public void enableCenteredContent(boolean enabled) { - mChildren.enableCenteredContent(enabled); - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - mChildren.setChildrenDrawingCacheEnabled(enabled); - } - - public void setCellCount(int xCount, int yCount) { - mCellCountX = xCount; - mCellCountY = yCount; - requestLayout(); - } - - public void setGap(int widthGap, int heightGap) { - mOriginalWidthGap = mWidthGap = widthGap; - mOriginalHeightGap = mHeightGap = heightGap; - mChildren.setGap(widthGap, heightGap); - } - - public int[] getCellCountForDimensions(int width, int height) { - // Always assume we're working with the smallest span to make sure we - // reserve enough space in both orientations - int smallerSize = Math.min(mCellWidth, mCellHeight); - - // Always round up to next largest cell - int spanX = (width + smallerSize) / smallerSize; - int spanY = (height + smallerSize) / smallerSize; - - return new int[] { spanX, spanY }; - } - - /** - * Start dragging the specified child - * - * @param child The child that is being dragged - */ - void onDragChild(View child) { - PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.isDragging = true; - } - - /** - * Estimates the number of cells that the specified width would take up. - */ - public int estimateCellHSpan(int width) { - // We don't show the next/previous pages any more, so we use the full width, minus the - // padding - int availWidth = width - (getPaddingLeft() + getPaddingRight()); - - // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N - int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** - * Estimates the number of cells that the specified height would take up. - */ - public int estimateCellVSpan(int height) { - // The space for a page is the height - top padding (current page) - bottom padding (current - // page) - int availHeight = height - (getPaddingTop() + getPaddingBottom()); - - // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N - int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** Returns an estimated center position of the cell at the specified index */ - public int[] estimateCellPosition(int x, int y) { - return new int[] { - getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2), - getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2) - }; - } - - public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) { - mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width)); - mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height)); - requestLayout(); - } - - /** - * Estimates the width that the number of hSpan cells will take up. - */ - public int estimateCellWidth(int hSpan) { - // TODO: we need to take widthGap into effect - return hSpan * mCellWidth; - } - - /** - * Estimates the height that the number of vSpan cells will take up. - */ - public int estimateCellHeight(int vSpan) { - // TODO: we need to take heightGap into effect - return vSpan * mCellHeight; - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new PagedViewCellLayout.LayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof PagedViewCellLayout.LayoutParams; - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new PagedViewCellLayout.LayoutParams(p); - } - - public static class LayoutParams extends ViewGroup.MarginLayoutParams { - /** - * Horizontal location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellX; - - /** - * Vertical location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellY; - - /** - * Number of cells spanned horizontally by the item. - */ - @ViewDebug.ExportedProperty - public int cellHSpan; - - /** - * Number of cells spanned vertically by the item. - */ - @ViewDebug.ExportedProperty - public int cellVSpan; - - /** - * Is this item currently being dragged - */ - public boolean isDragging; - - // a data object that you can bind to this layout params - private Object mTag; - - // X coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int x; - // Y coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int y; - - public LayoutParams() { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(LayoutParams source) { - super(source); - this.cellX = source.cellX; - this.cellY = source.cellY; - this.cellHSpan = source.cellHSpan; - this.cellVSpan = source.cellVSpan; - } - - public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - this.cellX = cellX; - this.cellY = cellY; - this.cellHSpan = cellHSpan; - this.cellVSpan = cellVSpan; - } - - public void setup(Context context, - int cellWidth, int cellHeight, int widthGap, int heightGap, - int hStartPadding, int vStartPadding) { - - final int myCellHSpan = cellHSpan; - final int myCellVSpan = cellVSpan; - final int myCellX = cellX; - final int myCellY = cellY; - - width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - - leftMargin - rightMargin; - height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - - topMargin - bottomMargin; - - if (LauncherAppState.getInstance().isScreenLarge()) { - x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; - y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; - } else { - x = myCellX * (cellWidth + widthGap) + leftMargin; - y = myCellY * (cellHeight + heightGap) + topMargin; - } - } - - public Object getTag() { - return mTag; - } - - public void setTag(Object tag) { - mTag = tag; - } - - public String toString() { - return "(" + this.cellX + ", " + this.cellY + ", " + - this.cellHSpan + ", " + this.cellVSpan + ")"; - } - } -}
\ No newline at end of file diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java deleted file mode 100644 index 84d2b1dd3..000000000 --- a/src/com/android/launcher3/PagedViewCellLayoutChildren.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2010 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.content.Context; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayoutChildren extends ViewGroup { - static final String TAG = "PagedViewCellLayout"; - - private boolean mCenterContent; - - private int mCellWidth; - private int mCellHeight; - private int mWidthGap; - private int mHeightGap; - - public PagedViewCellLayoutChildren(Context context) { - super(context); - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public void setGap(int widthGap, int heightGap) { - mWidthGap = widthGap; - mHeightGap = heightGap; - requestLayout(); - } - - public void setCellDimensions(int width, int height) { - mCellWidth = width; - mCellHeight = height; - requestLayout(); - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - if (child != null) { - Rect r = new Rect(); - child.getDrawingRect(r); - requestRectangleOnScreen(r); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.setup(getContext(), - mCellWidth, mCellHeight, mWidthGap, mHeightGap, - getPaddingLeft(), - getPaddingTop()); - - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, - MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, - MeasureSpec.EXACTLY); - - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(widthSpecSize, heightSpecSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - - int offsetX = 0; - if (mCenterContent && count > 0) { - // determine the max width of all the rows and center accordingly - int maxRowX = 0; - int minRowX = Integer.MAX_VALUE; - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - minRowX = Math.min(minRowX, lp.x); - maxRowX = Math.max(maxRowX, lp.x + lp.width); - } - } - int maxRowWidth = maxRowX - minRowX; - offsetX = (getMeasuredWidth() - maxRowWidth) / 2; - } - - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - - int childLeft = offsetX + lp.x; - int childTop = lp.y; - child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); - } - } - } - - public void enableCenteredContent(boolean enabled) { - mCenterContent = enabled; - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View view = getChildAt(i); - view.setDrawingCacheEnabled(enabled); - // Update the drawing caches - if (!view.isHardwareAccelerated()) { - view.buildDrawingCache(true); - } - } - } -} diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/PagedViewGridLayout.java deleted file mode 100644 index f69fa562d..000000000 --- a/src/com/android/launcher3/PagedViewGridLayout.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2011 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.content.Context; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.GridLayout; - -/** - * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane - */ -public class PagedViewGridLayout extends GridLayout implements Page { - static final String TAG = "PagedViewGridLayout"; - - private int mCellCountX; - private int mCellCountY; - private Runnable mOnLayoutListener; - - public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) { - super(context, null, 0); - mCellCountX = cellCountX; - mCellCountY = cellCountY; - } - - int getCellCountX() { - return mCellCountX; - } - - int getCellCountY() { - return mCellCountY; - } - - /** - * Clears all the key listeners for the individual widgets. - */ - public void resetChildrenOnKeyListeners() { - int childCount = getChildCount(); - for (int j = 0; j < childCount; ++j) { - getChildAt(j).setOnKeyListener(null); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mOnLayoutListener = null; - } - - public void setOnLayoutListener(Runnable r) { - mOnLayoutListener = r; - } - - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mOnLayoutListener != null) { - mOnLayoutListener.run(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - result = result || (event.getY() < bottom); - } - return result; - } - - @Override - public void removeAllViewsOnPage() { - removeAllViews(); - mOnLayoutListener = null; - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - removeViewAt(index); - } - - @Override - public int getPageChildCount() { - return getChildCount(); - } - - @Override - public View getChildOnPageAt(int i) { - return getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return indexOfChild(v); - } - - public static class LayoutParams extends FrameLayout.LayoutParams { - public LayoutParams(int width, int height) { - super(width, height); - } - } -} diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/PagedViewWidget.java deleted file mode 100644 index e6e11a312..000000000 --- a/src/com/android/launcher3/PagedViewWidget.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (C) 2010 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.appwidget.AppWidgetProviderInfo; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.launcher3.compat.AppWidgetManagerCompat; - -/** - * The linear layout used strictly for the widget/wallpaper tab of the customization tray - */ -public class PagedViewWidget extends LinearLayout { - static final String TAG = "PagedViewWidgetLayout"; - - private static boolean sDeletePreviewsWhenDetachedFromWindow = true; - private static boolean sRecyclePreviewsWhenDetachedFromWindow = true; - - private String mDimensionsFormatString; - CheckForShortPress mPendingCheckForShortPress = null; - ShortPressListener mShortPressListener = null; - boolean mShortPressTriggered = false; - static PagedViewWidget sShortpressTarget = null; - boolean mIsAppWidget; - private final Rect mOriginalImagePadding = new Rect(); - private Object mInfo; - private WidgetPreviewLoader mWidgetPreviewLoader; - - public PagedViewWidget(Context context) { - this(context, null); - } - - public PagedViewWidget(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final Resources r = context.getResources(); - mDimensionsFormatString = r.getString(R.string.widget_dims_format); - - setWillNotDraw(false); - setClipToPadding(false); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - mOriginalImagePadding.left = image.getPaddingLeft(); - mOriginalImagePadding.top = image.getPaddingTop(); - mOriginalImagePadding.right = image.getPaddingRight(); - mOriginalImagePadding.bottom = image.getPaddingBottom(); - - // Ensure we are using the right text size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - TextView name = (TextView) findViewById(R.id.widget_name); - if (name != null) { - name.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - dims.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - } - - public static void setDeletePreviewsWhenDetachedFromWindow(boolean value) { - sDeletePreviewsWhenDetachedFromWindow = value; - } - - public static void setRecyclePreviewsWhenDetachedFromWindow(boolean value) { - sRecyclePreviewsWhenDetachedFromWindow = value; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (sDeletePreviewsWhenDetachedFromWindow) { - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - if (image != null) { - FastBitmapDrawable preview = (FastBitmapDrawable) image.getDrawable(); - if (sRecyclePreviewsWhenDetachedFromWindow && - mInfo != null && preview != null && preview.getBitmap() != null) { - mWidgetPreviewLoader.recycleBitmap(mInfo, preview.getBitmap()); - } - image.setImageDrawable(null); - } - } - } - - public void applyFromAppWidgetProviderInfo(AppWidgetProviderInfo info, - int maxWidth, int[] cellSpan, WidgetPreviewLoader loader) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - mIsAppWidget = true; - mInfo = info; - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - if (maxWidth > -1) { - image.setMaxWidth(maxWidth); - } - final TextView name = (TextView) findViewById(R.id.widget_name); - name.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); - final TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - int hSpan = Math.min(cellSpan[0], (int) grid.numColumns); - int vSpan = Math.min(cellSpan[1], (int) grid.numRows); - dims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); - } - mWidgetPreviewLoader = loader; - } - - public void applyFromResolveInfo( - PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { - mIsAppWidget = false; - mInfo = info; - CharSequence label = info.loadLabel(pm); - final TextView name = (TextView) findViewById(R.id.widget_name); - name.setText(label); - final TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - dims.setText(String.format(mDimensionsFormatString, 1, 1)); - } - mWidgetPreviewLoader = loader; - } - - public int[] getPreviewSize() { - final ImageView i = (ImageView) findViewById(R.id.widget_preview); - int[] maxSize = new int[2]; - maxSize[0] = i.getWidth() - mOriginalImagePadding.left - mOriginalImagePadding.right; - maxSize[1] = i.getHeight() - mOriginalImagePadding.top; - return maxSize; - } - - void applyPreview(FastBitmapDrawable preview, int index) { - final PagedViewWidgetImageView image = - (PagedViewWidgetImageView) findViewById(R.id.widget_preview); - if (preview != null) { - image.mAllowRequestLayout = false; - image.setImageDrawable(preview); - if (mIsAppWidget) { - // center horizontally - int[] imageSize = getPreviewSize(); - int centerAmount = (imageSize[0] - preview.getIntrinsicWidth()) / 2; - image.setPadding(mOriginalImagePadding.left + centerAmount, - mOriginalImagePadding.top, - mOriginalImagePadding.right, - mOriginalImagePadding.bottom); - } - image.setAlpha(1f); - image.mAllowRequestLayout = true; - } - } - - void setShortPressListener(ShortPressListener listener) { - mShortPressListener = listener; - } - - interface ShortPressListener { - void onShortPress(View v); - void cleanUpShortPress(View v); - } - - class CheckForShortPress implements Runnable { - public void run() { - if (sShortpressTarget != null) return; - if (mShortPressListener != null) { - mShortPressListener.onShortPress(PagedViewWidget.this); - sShortpressTarget = PagedViewWidget.this; - } - mShortPressTriggered = true; - } - } - - private void checkForShortPress() { - if (sShortpressTarget != null) return; - if (mPendingCheckForShortPress == null) { - mPendingCheckForShortPress = new CheckForShortPress(); - } - postDelayed(mPendingCheckForShortPress, 120); - } - - /** - * Remove the longpress detection timer. - */ - private void removeShortPressCallback() { - if (mPendingCheckForShortPress != null) { - removeCallbacks(mPendingCheckForShortPress); - } - } - - private void cleanUpShortPress() { - removeShortPressCallback(); - if (mShortPressTriggered) { - if (mShortPressListener != null) { - mShortPressListener.cleanUpShortPress(PagedViewWidget.this); - } - mShortPressTriggered = false; - } - } - - static void resetShortPressTarget() { - sShortpressTarget = null; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - super.onTouchEvent(event); - - switch (event.getAction()) { - case MotionEvent.ACTION_UP: - cleanUpShortPress(); - break; - case MotionEvent.ACTION_DOWN: - checkForShortPress(); - break; - case MotionEvent.ACTION_CANCEL: - cleanUpShortPress(); - break; - case MotionEvent.ACTION_MOVE: - break; - } - - // We eat up the touch events here, since the PagedView (which uses the same swiping - // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when - // the user is scrolling between pages. This means that if the pages themselves don't - // handle touch events, it gets forwarded up to PagedView itself, and it's own - // onTouchEvent() handling will prevent further intercept touch events from being called - // (it's the same view in that case). This is not ideal, but to prevent more changes, - // we just always mark the touch event as handled. - return true; - } -} diff --git a/src/com/android/launcher3/PagedViewWidgetImageView.java b/src/com/android/launcher3/PagedViewWidgetImageView.java deleted file mode 100644 index 7d8279547..000000000 --- a/src/com/android/launcher3/PagedViewWidgetImageView.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2011 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.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.widget.ImageView; - -public class PagedViewWidgetImageView extends ImageView { - public boolean mAllowRequestLayout = true; - - public PagedViewWidgetImageView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void requestLayout() { - if (mAllowRequestLayout) { - super.requestLayout(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - canvas.save(); - canvas.clipRect(getScrollX() + getPaddingLeft(), - getScrollY() + getPaddingTop(), - getScrollX() + getRight() - getLeft() - getPaddingRight(), - getScrollY() + getBottom() - getTop() - getPaddingBottom()); - - super.onDraw(canvas); - canvas.restore(); - - } -} diff --git a/src/com/android/launcher3/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java deleted file mode 100644 index 0e593698d..000000000 --- a/src/com/android/launcher3/PagedViewWithDraggableItems.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2010 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.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - - -/* Class that does most of the work of enabling dragging items out of a PagedView by performing a - * vertical drag. Used by both CustomizePagedView and AllAppsPagedView. - * Subclasses must do the following: - * * call setDragSlopeThreshold after making an instance of the PagedViewWithDraggableItems - * * call child.setOnLongClickListener(this) and child.setOnTouchListener(this) on all children - * (good place to do it is in syncPageItems) - * * override beginDragging(View) (but be careful to call super.beginDragging(View) - * - */ -public abstract class PagedViewWithDraggableItems extends PagedView - implements View.OnLongClickListener, View.OnTouchListener { - private View mLastTouchedItem; - private boolean mIsDragging; - private boolean mIsDragEnabled; - private float mDragSlopeThreshold; - private Launcher mLauncher; - - public PagedViewWithDraggableItems(Context context) { - this(context, null); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mLauncher = (Launcher) context; - } - - protected boolean beginDragging(View v) { - boolean wasDragging = mIsDragging; - mIsDragging = true; - return !wasDragging; - } - - protected void cancelDragging() { - mIsDragging = false; - mLastTouchedItem = null; - mIsDragEnabled = false; - } - - private void handleTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - cancelDragging(); - mIsDragEnabled = true; - break; - case MotionEvent.ACTION_MOVE: - if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging && mIsDragEnabled) { - determineDraggingStart(ev); - } - break; - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onTouchEvent(ev); - } - - public void trimMemory() { - mLastTouchedItem = null; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - mLastTouchedItem = v; - mIsDragEnabled = true; - return false; - } - - @Override - public boolean onLongClick(View v) { - // Return early if this is not initiated from a touch - if (!v.isInTouchMode()) return false; - // Return early if we are still animating the pages - if (mNextPage != INVALID_PAGE) return false; - // When we have exited all apps or are in transition, disregard long clicks - if (!mLauncher.isAllAppsVisible() || - mLauncher.getWorkspace().isSwitchingState()) return false; - // Return if global dragging is not enabled - if (!mLauncher.isDraggingEnabled()) return false; - - return beginDragging(v); - } - - /* - * Determines if we should change the touch state to start scrolling after the - * user moves their touch point too far. - */ - protected void determineScrollingStart(MotionEvent ev) { - if (!mIsDragging) super.determineScrollingStart(ev); - } - - /* - * Determines if we should change the touch state to start dragging after the - * user moves their touch point far enough. - */ - protected void determineDraggingStart(MotionEvent ev) { - /* - * Locally do absolute value. mLastMotionX is set to the y value - * of the down event. - */ - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - boolean yMoved = yDiff > touchSlop; - boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold; - - if (isUpwardMotion && yMoved && mLastTouchedItem != null) { - // Drag if the user moved far enough along the Y axis - beginDragging(mLastTouchedItem); - - // Cancel any pending long press - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } - } - } - } - - public void setDragSlopeThreshold(float dragSlopeThreshold) { - mDragSlopeThreshold = dragSlopeThreshold; - } - - @Override - protected void onDetachedFromWindow() { - cancelDragging(); - super.onDetachedFromWindow(); - } -} diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java index e1913193b..380078b26 100644 --- a/src/com/android/launcher3/Partner.java +++ b/src/com/android/launcher3/Partner.java @@ -47,8 +47,6 @@ public class Partner { public static final String RES_REQUIRE_FIRST_RUN_FLOW = "requires_first_run_flow"; /** These resources are used to override the device profile */ - public static final String RES_GRID_AA_SHORT_EDGE_COUNT = "grid_aa_short_edge_count"; - public static final String RES_GRID_AA_LONG_EDGE_COUNT = "grid_aa_long_edge_count"; public static final String RES_GRID_NUM_ROWS = "grid_num_rows"; public static final String RES_GRID_NUM_COLUMNS = "grid_num_columns"; public static final String RES_GRID_ICON_SIZE_DP = "grid_icon_size_dp"; @@ -116,56 +114,42 @@ public class Partner { return resId != 0 && getResources().getBoolean(resId); } - public DeviceProfile getDeviceProfileOverride(DisplayMetrics dm) { - boolean containsProfileOverrides = false; - - DeviceProfile dp = new DeviceProfile(); - - // We initialize customizable fields to be invalid - dp.numRows = -1; - dp.numColumns = -1; - dp.allAppsShortEdgeCount = -1; - dp.allAppsLongEdgeCount = -1; + public void applyInvariantDeviceProfileOverrides(InvariantDeviceProfile inv, DisplayMetrics dm) { + int numRows = -1; + int numColumns = -1; + float iconSize = -1; try { int resId = getResources().getIdentifier(RES_GRID_NUM_ROWS, "integer", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; - dp.numRows = getResources().getInteger(resId); + numRows = getResources().getInteger(resId); } resId = getResources().getIdentifier(RES_GRID_NUM_COLUMNS, "integer", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; - dp.numColumns = getResources().getInteger(resId); - } - - resId = getResources().getIdentifier(RES_GRID_AA_SHORT_EDGE_COUNT, - "integer", getPackageName()); - if (resId > 0) { - containsProfileOverrides = true; - dp.allAppsShortEdgeCount = getResources().getInteger(resId); - } - - resId = getResources().getIdentifier(RES_GRID_AA_LONG_EDGE_COUNT, - "integer", getPackageName()); - if (resId > 0) { - containsProfileOverrides = true; - dp.allAppsLongEdgeCount = getResources().getInteger(resId); + numColumns = getResources().getInteger(resId); } resId = getResources().getIdentifier(RES_GRID_ICON_SIZE_DP, "dimen", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; int px = getResources().getDimensionPixelSize(resId); - dp.iconSize = DynamicGrid.dpiFromPx(px, dm); + iconSize = Utilities.dpiFromPx(px, dm); } } catch (Resources.NotFoundException ex) { Log.e(TAG, "Invalid Partner grid resource!", ex); + return; + } + + if (numRows > 0 && numColumns > 0) { + inv.numRows = numRows; + inv.numColumns = numColumns; + } + + if (iconSize > 0) { + inv.iconSize = iconSize; } - return containsProfileOverrides ? dp : null; } } diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java index 967cc928e..1aaf85bbd 100644 --- a/src/com/android/launcher3/PendingAddItemInfo.java +++ b/src/com/android/launcher3/PendingAddItemInfo.java @@ -16,92 +16,17 @@ package com.android.launcher3; -import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; -import android.content.pm.ActivityInfo; -import android.os.Bundle; -import android.os.Parcelable; /** - * We pass this object with a drag from the customization tray + * Meta data that is used for deferred binding. + * e.g., this object is used to pass information on dragable targets when they are dropped onto + * the workspace from another container. */ -class PendingAddItemInfo extends ItemInfo { +public class PendingAddItemInfo extends ItemInfo { + /** * The component that will be created. */ - ComponentName componentName; -} - -class PendingAddShortcutInfo extends PendingAddItemInfo { - - ActivityInfo shortcutActivityInfo; - - public PendingAddShortcutInfo(ActivityInfo activityInfo) { - shortcutActivityInfo = activityInfo; - } - - @Override - public String toString() { - return "Shortcut: " + shortcutActivityInfo.packageName; - } -} - -class PendingAddWidgetInfo extends PendingAddItemInfo { - int minWidth; - int minHeight; - int minResizeWidth; - int minResizeHeight; - int previewImage; - int icon; - AppWidgetProviderInfo info; - AppWidgetHostView boundWidget; - Bundle bindOptions = null; - - // Any configuration data that we want to pass to a configuration activity when - // starting up a widget - String mimeType; - Parcelable configurationData; - - public PendingAddWidgetInfo(AppWidgetProviderInfo i, String dataMimeType, Parcelable data) { - itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; - this.info = i; - componentName = i.provider; - minWidth = i.minWidth; - minHeight = i.minHeight; - minResizeWidth = i.minResizeWidth; - minResizeHeight = i.minResizeHeight; - previewImage = i.previewImage; - icon = i.icon; - if (dataMimeType != null && data != null) { - mimeType = dataMimeType; - configurationData = data; - } - } - - // Copy constructor - public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { - minWidth = copy.minWidth; - minHeight = copy.minHeight; - minResizeWidth = copy.minResizeWidth; - minResizeHeight = copy.minResizeHeight; - previewImage = copy.previewImage; - icon = copy.icon; - info = copy.info; - boundWidget = copy.boundWidget; - mimeType = copy.mimeType; - configurationData = copy.configurationData; - componentName = copy.componentName; - itemType = copy.itemType; - spanX = copy.spanX; - spanY = copy.spanY; - minSpanX = copy.minSpanX; - minSpanY = copy.minSpanY; - bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone(); - } - - @Override - public String toString() { - return "Widget: " + componentName.toShortString(); - } + public ComponentName componentName; } diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java index 179c60a98..08f8e5601 100644 --- a/src/com/android/launcher3/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -42,6 +42,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen private final int mStartState; private final Intent mIconLookupIntent; private final boolean mDisabledForSafeMode; + private Launcher mLauncher; private Bitmap mIcon; @@ -56,6 +57,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info, boolean disabledForSafeMode) { super(context); + + mLauncher = (Launcher) context; mInfo = info; mStartState = info.restoreStatus; mIconLookupIntent = new Intent().setComponent(info.providerName); @@ -64,7 +67,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen mPaint = new TextPaint(); mPaint.setColor(0xFFFFFFFF); mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, - getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); + mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); setBackgroundResource(R.drawable.quantum_panel_dark); setWillNotDraw(false); } @@ -118,7 +121,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen // 2) Preload icon in the center // 3) Setup icon in the center and app icon in the top right corner. if (mDisabledForSafeMode) { - FastBitmapDrawable disabledIcon = Utilities.createIconDrawable(mIcon); + FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); disabledIcon.setGhostModeEnabled(true); mCenterDrawable = disabledIcon; mTopCornerDrawable = null; @@ -131,7 +134,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen sPreloaderTheme.applyStyle(R.style.PreloadIcon, true); } - FastBitmapDrawable drawable = Utilities.createIconDrawable(mIcon); + FastBitmapDrawable drawable = mLauncher.createIconDrawable(mIcon); mCenterDrawable = new PreloadIconDrawable(drawable, sPreloaderTheme); mCenterDrawable.setCallback(this); mTopCornerDrawable = null; @@ -173,12 +176,12 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen return; } + DeviceProfile grid = mLauncher.getDeviceProfile(); if (mTopCornerDrawable == null) { if (mDrawableSizeChanged) { int outset = (mCenterDrawable instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) mCenterDrawable).getOutset() : 0; - int maxSize = LauncherAppState.getInstance().getDynamicGrid() - .getDeviceProfile().iconSizePx + 2 * outset; + int maxSize = grid.iconSizePx + 2 * outset; int size = Math.min(maxSize, Math.min( getWidth() - getPaddingLeft() - getPaddingRight(), getHeight() - getPaddingTop() - getPaddingBottom())); @@ -193,7 +196,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen } else { // Draw the top corner icon and "Setup" text is possible if (mDrawableSizeChanged) { - DeviceProfile grid = getDeviceProfile(); int iconSize = grid.iconSizePx; int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); @@ -251,8 +253,4 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen } } } - - private DeviceProfile getDeviceProfile() { - return LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); - } } diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index 2972c4f9b..bcb59c448 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -54,12 +54,11 @@ class PreloadIconDrawable extends Drawable { mPaint.setStrokeCap(Paint.Cap.ROUND); setBounds(icon.getBounds()); - applyTheme(theme); + applyPreloaderTheme(theme); onLevelChange(0); } - @Override - public void applyTheme(Theme t) { + public void applyPreloaderTheme(Theme t) { TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); mBgDrawable.setFilterBitmap(true); diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 99c2e0859..4cdf1cac9 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -33,23 +33,22 @@ import android.widget.FrameLayout; */ public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { - private static final int sTransitionInDuration = 200; - private static final int sTransitionOutDuration = 175; + private static final int TRANSITION_DURATION = 200; - private ObjectAnimator mDropTargetBarAnim; - private ValueAnimator mQSBSearchBarAnim; + private ObjectAnimator mShowDropTargetBarAnim; + private ValueAnimator mHideSearchBarAnim; private static final AccelerateInterpolator sAccelerateInterpolator = new AccelerateInterpolator(); private boolean mIsSearchBarHidden; private View mQSBSearchBar; private View mDropTargetBar; - private ButtonDropTarget mInfoDropTarget; - private ButtonDropTarget mDeleteDropTarget; - private int mBarHeight; private boolean mDeferOnDragEnd = false; - private boolean mEnableDropDownDropTargets; + // Drop targets + private ButtonDropTarget mInfoDropTarget; + private ButtonDropTarget mDeleteDropTarget; + private ButtonDropTarget mUninstallDropTarget; public SearchDropTargetBar(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -61,29 +60,30 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D public void setup(Launcher launcher, DragController dragController) { dragController.addDragListener(this); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDragListener(mInfoDropTarget); dragController.addDragListener(mDeleteDropTarget); + dragController.addDragListener(mUninstallDropTarget); + dragController.addDropTarget(mInfoDropTarget); dragController.addDropTarget(mDeleteDropTarget); - dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDropTarget(mUninstallDropTarget); + mInfoDropTarget.setLauncher(launcher); mDeleteDropTarget.setLauncher(launcher); + mUninstallDropTarget.setLauncher(launcher); } public void setQsbSearchBar(View qsb) { mQSBSearchBar = qsb; if (mQSBSearchBar != null) { - if (mEnableDropDownDropTargets) { - mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0, - -mBarHeight); - } else { - mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); - } - setupAnimation(mQSBSearchBarAnim, mQSBSearchBar); + mHideSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); + setupAnimation(mHideSearchBarAnim, mQSBSearchBar); } else { // Create a no-op animation of the search bar is null - mQSBSearchBarAnim = ValueAnimator.ofFloat(0, 0); - mQSBSearchBarAnim.setDuration(sTransitionInDuration); + mHideSearchBarAnim = ValueAnimator.ofFloat(0, 0); + mHideSearchBarAnim.setDuration(TRANSITION_DURATION); } } @@ -97,7 +97,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D private void setupAnimation(ValueAnimator anim, final View v) { anim.setInterpolator(sAccelerateInterpolator); - anim.setDuration(sTransitionInDuration); + anim.setDuration(TRANSITION_DURATION); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -116,80 +116,80 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D mDropTargetBar = findViewById(R.id.drag_target_bar); mInfoDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.info_target_text); mDeleteDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.delete_target_text); + mUninstallDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.uninstall_target_text); mInfoDropTarget.setSearchDropTargetBar(this); mDeleteDropTarget.setSearchDropTargetBar(this); - - mEnableDropDownDropTargets = - getResources().getBoolean(R.bool.config_useDropTargetDownTransition); + mUninstallDropTarget.setSearchDropTargetBar(this); // Create the various fade animations - if (mEnableDropDownDropTargets) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mBarHeight = grid.searchBarSpaceHeightPx; - mDropTargetBar.setTranslationY(-mBarHeight); - mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "translationY", - -mBarHeight, 0f); - - } else { - mDropTargetBar.setAlpha(0f); - mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); - } - setupAnimation(mDropTargetBarAnim, mDropTargetBar); + mDropTargetBar.setAlpha(0f); + mShowDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); + setupAnimation(mShowDropTargetBarAnim, mDropTargetBar); } + /** + * Finishes all the animations on the search and drop target bars. + */ public void finishAnimations() { prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.reverse(); + mShowDropTargetBarAnim.reverse(); prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); + mHideSearchBarAnim.reverse(); } - /* - * Shows and hides the search bar. + /** + * Shows the search bar. */ public void showSearchBar(boolean animated) { - boolean needToCancelOngoingAnimation = mQSBSearchBarAnim.isRunning() && !animated; - if (!mIsSearchBarHidden && !needToCancelOngoingAnimation) return; + if (!mIsSearchBarHidden) return; if (animated) { prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); + mHideSearchBarAnim.reverse(); } else { - mQSBSearchBarAnim.cancel(); - if (mQSBSearchBar != null && mEnableDropDownDropTargets) { - mQSBSearchBar.setTranslationY(0); - } else if (mQSBSearchBar != null) { + mHideSearchBarAnim.cancel(); + if (mQSBSearchBar != null) { mQSBSearchBar.setAlpha(1f); } } mIsSearchBarHidden = false; } + + /** + * Hides the search bar. We only use this for clings. + */ public void hideSearchBar(boolean animated) { - boolean needToCancelOngoingAnimation = mQSBSearchBarAnim.isRunning() && !animated; - if (mIsSearchBarHidden && !needToCancelOngoingAnimation) return; + if (mIsSearchBarHidden) return; if (animated) { prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.start(); + mHideSearchBarAnim.start(); } else { - mQSBSearchBarAnim.cancel(); - if (mQSBSearchBar != null && mEnableDropDownDropTargets) { - mQSBSearchBar.setTranslationY(-mBarHeight); - } else if (mQSBSearchBar != null) { + mHideSearchBarAnim.cancel(); + if (mQSBSearchBar != null) { mQSBSearchBar.setAlpha(0f); } } mIsSearchBarHidden = true; } - /* - * Gets various transition durations. + /** + * Shows the drop target bar. */ - public int getTransitionInDuration() { - return sTransitionInDuration; + public void showDeleteTarget() { + // Animate out the QSB search bar, and animate in the drop target bar + prepareStartAnimation(mDropTargetBar); + mShowDropTargetBarAnim.start(); + hideSearchBar(true); } - public int getTransitionOutDuration() { - return sTransitionOutDuration; + + /** + * Hides the drop target bar. + */ + public void hideDeleteTarget() { + // Restore the QSB search bar, and animate out the drop target bar + prepareStartAnimation(mDropTargetBar); + mShowDropTargetBarAnim.reverse(); + showSearchBar(true); } /* @@ -197,13 +197,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D */ @Override public void onDragStart(DragSource source, Object info, int dragAction) { - // Animate out the QSB search bar, and animate in the drop target bar - prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.start(); - if (!mIsSearchBarHidden) { - prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.start(); - } + showDeleteTarget(); } public void deferOnDragEnd() { @@ -213,13 +207,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D @Override public void onDragEnd() { if (!mDeferOnDragEnd) { - // Restore the QSB search bar, and animate out the drop target bar - prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.reverse(); - if (!mIsSearchBarHidden) { - prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); - } + hideDeleteTarget(); } else { mDeferOnDragEnd = false; } @@ -240,4 +228,13 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D return null; } } + + public void enableAccessibleDrag(boolean enable) { + if (mQSBSearchBar != null) { + mQSBSearchBar.setVisibility(enable ? View.GONE : View.VISIBLE); + } + mInfoDropTarget.enableAccessibleDrag(enable); + mDeleteDropTarget.enableAccessibleDrag(enable); + mUninstallDropTarget.enableAccessibleDrag(enable); + } } diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java new file mode 100644 index 000000000..dab71c862 --- /dev/null +++ b/src/com/android/launcher3/SettingsActivity.java @@ -0,0 +1,76 @@ +/* + * 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.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; + +/** + * Settings activity for Launcher. Currently implements the following setting: Allow rotation + */ +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. + */ + public static class LauncherSettingsFragment extends PreferenceFragment + implements OnPreferenceChangeListener { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.launcher_preferences); + + SwitchPreference pref = (SwitchPreference) findPreference( + Utilities.ALLOW_ROTATION_PREFERENCE_KEY); + pref.setPersistent(false); + + Bundle extras = new Bundle(); + extras.putBoolean(LauncherSettings.Settings.EXTRA_DEFAULT_VALUE, false); + Bundle value = getActivity().getContentResolver().call( + LauncherSettings.Settings.CONTENT_URI, + LauncherSettings.Settings.METHOD_GET_BOOLEAN, + Utilities.ALLOW_ROTATION_PREFERENCE_KEY, extras); + pref.setChecked(value.getBoolean(LauncherSettings.Settings.EXTRA_VALUE)); + + pref.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Bundle extras = new Bundle(); + extras.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, (Boolean) newValue); + getActivity().getContentResolver().call( + LauncherSettings.Settings.CONTENT_URI, + LauncherSettings.Settings.METHOD_SET_BOOLEAN, + preference.getKey(), extras); + return true; + } + } +} diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index bb5601e90..157b48a39 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -23,6 +23,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; public class ShortcutAndWidgetContainer extends ViewGroup { static final String TAG = "CellLayoutChildren"; @@ -44,10 +45,13 @@ public class ShortcutAndWidgetContainer extends ViewGroup { private int mCountX; private int mCountY; + private Launcher mLauncher; + private boolean mInvertIfRtl = false; public ShortcutAndWidgetContainer(Context context) { super(context); + mLauncher = (Launcher) context; mWallpaperManager = WallpaperManager.getInstance(context); } @@ -124,22 +128,19 @@ public class ShortcutAndWidgetContainer extends ViewGroup { } int getCellContentWidth() { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); return Math.min(getMeasuredHeight(), mIsHotseatLayout ? grid.hotseatCellWidthPx: grid.cellWidthPx); } int getCellContentHeight() { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); return Math.min(getMeasuredHeight(), mIsHotseatLayout ? grid.hotseatCellHeightPx : grid.cellHeightPx); } public void measureChild(View child) { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); final int cellWidth = mCellWidth; final int cellHeight = mCellHeight; CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); @@ -168,12 +169,8 @@ public class ShortcutAndWidgetContainer extends ViewGroup { child.measure(childWidthMeasureSpec, childheightMeasureSpec); } - private boolean invertLayoutHorizontally() { - return mInvertIfRtl && isLayoutRtl(); - } - - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + public boolean invertLayoutHorizontally() { + return mInvertIfRtl && Utilities.isRtl(getResources()); } @Override diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 01f79314e..56c0b9d2f 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -23,7 +23,10 @@ import android.content.Intent; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import java.util.ArrayList; import java.util.Arrays; @@ -46,18 +49,24 @@ public class ShortcutInfo extends ItemInfo { * be present along with {@link #FLAG_RESTORED_ICON}, and is set during default layout * parsing. */ - public static final int FLAG_AUTOINTALL_ICON = 2; + public static final int FLAG_AUTOINTALL_ICON = 2; //0B10; /** * The icon is being installed. If {@link FLAG_RESTORED_ICON} or {@link FLAG_AUTOINTALL_ICON} * is set, then the icon is either being installed or is in a broken state. */ - public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; + public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; // 0B100; /** * Indicates that the widget restore has started. */ - public static final int FLAG_RESTORE_STARTED = 8; + public static final int FLAG_RESTORE_STARTED = 8; //0B1000; + + /** + * Indicates if it represents a common type mentioned in {@link CommonAppTypeParser}. + * Upto 15 different types supported. + */ + public static final int FLAG_RESTORED_APP_TYPE = 0B0011110000; /** * The intent used to start the application. @@ -67,8 +76,9 @@ public class ShortcutInfo extends ItemInfo { /** * Indicates whether the icon comes from an application's resource (if false) * or from a custom Bitmap (if true.) + * TODO: remove this flag */ - boolean customIcon; + public boolean customIcon; /** * Indicates whether we're using the default fallback icon instead of something from the @@ -77,10 +87,15 @@ public class ShortcutInfo extends ItemInfo { boolean usingFallbackIcon; /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; + + /** * If isShortcut=true and customIcon=false, this contains a reference to the * shortcut icon as an application's resource. */ - Intent.ShortcutIconResource iconResource; + public Intent.ShortcutIconResource iconResource; /** * The application icon. @@ -113,7 +128,7 @@ public class ShortcutInfo extends ItemInfo { /** * Refer {@link AppInfo#firstInstallTime}. */ - long firstInstallTime; + public long firstInstallTime; /** * TODO move this to {@link status} @@ -139,7 +154,7 @@ public class ShortcutInfo extends ItemInfo { Bitmap icon, UserHandleCompat user) { this(); this.intent = intent; - this.title = title; + this.title = Utilities.trim(title); this.contentDescription = contentDescription; mIcon = icon; this.user = user; @@ -147,7 +162,7 @@ public class ShortcutInfo extends ItemInfo { public ShortcutInfo(Context context, ShortcutInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); if (info.iconResource != null) { iconResource = new Intent.ShortcutIconResource(); @@ -165,7 +180,7 @@ public class ShortcutInfo extends ItemInfo { /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ public ShortcutInfo(AppInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); customIcon = false; flags = info.flags; @@ -184,8 +199,10 @@ public class ShortcutInfo extends ItemInfo { } public void updateIcon(IconCache iconCache) { - mIcon = iconCache.getIcon(promisedIntent != null ? promisedIntent : intent, user); - usingFallbackIcon = iconCache.isDefaultIcon(mIcon, user); + if (itemType == Favorites.ITEM_TYPE_APPLICATION) { + iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user, + shouldUseLowResIcon()); + } } @Override @@ -198,6 +215,7 @@ public class ShortcutInfo extends ItemInfo { String uri = promisedIntent != null ? promisedIntent.toUri(0) : (intent != null ? intent.toUri(0) : null); values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri); + values.put(LauncherSettings.Favorites.RESTORED, status); if (customIcon) { values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, @@ -207,9 +225,9 @@ public class ShortcutInfo extends ItemInfo { if (!usingFallbackIcon) { writeBitmap(values, mIcon); } - values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, - LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); if (iconResource != null) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE, iconResource.packageName); values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE, @@ -256,5 +274,23 @@ public class ShortcutInfo extends ItemInfo { mInstallProgress = progress; status |= FLAG_INSTALL_SESSION_ACTIVE; } + + public boolean shouldUseLowResIcon() { + return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + } + + public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) { + final ShortcutInfo shortcut = new ShortcutInfo(); + shortcut.user = info.getUser(); + shortcut.title = Utilities.trim(info.getLabel()); + shortcut.contentDescription = UserManagerCompat.getInstance(context) + .getBadgedLabelForUser(info.getLabel(), info.getUser()); + shortcut.customIcon = false; + shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser()); + shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + shortcut.flags = AppInfo.initFlags(info); + shortcut.firstInstallTime = info.getFirstInstallTime(); + return shortcut; + } } diff --git a/src/com/android/launcher3/SmoothPagedView.java b/src/com/android/launcher3/SmoothPagedView.java deleted file mode 100644 index 4e331aa2c..000000000 --- a/src/com/android/launcher3/SmoothPagedView.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2008 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.content.Context; -import android.util.AttributeSet; -import android.view.animation.Interpolator; - -public abstract class SmoothPagedView extends PagedView { - private static final float SMOOTHING_SPEED = 0.75f; - private static final float SMOOTHING_CONSTANT = (float) (0.016 / Math.log(SMOOTHING_SPEED)); - - private float mBaseLineFlingVelocity; - private float mFlingVelocityInfluence; - - static final int DEFAULT_MODE = 0; - static final int X_LARGE_MODE = 1; - - int mScrollMode; - - private Interpolator mScrollInterpolator; - - public static class OvershootInterpolator implements Interpolator { - private static final float DEFAULT_TENSION = 1.3f; - private float mTension; - - public OvershootInterpolator() { - mTension = DEFAULT_TENSION; - } - - public void setDistance(int distance) { - mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION; - } - - public void disableSettle() { - mTension = 0.f; - } - - public float getInterpolation(float t) { - t -= 1.0f; - return t * t * ((mTension + 1) * t + mTension) + 1.0f; - } - } - - /** - * Used to inflate the Workspace from XML. - * - * @param context The application's context. - * @param attrs The attributes set containing the Workspace's customization values. - */ - public SmoothPagedView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - /** - * Used to inflate the Workspace from XML. - * - * @param context The application's context. - * @param attrs The attributes set containing the Workspace's customization values. - * @param defStyle Unused. - */ - public SmoothPagedView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mUsePagingTouchSlop = false; - - // This means that we'll take care of updating the scroll parameter ourselves (we do it - // in computeScroll), we only do this in the OVERSHOOT_MODE, ie. on phones - mDeferScrollUpdate = mScrollMode != X_LARGE_MODE; - } - - protected int getScrollMode() { - return X_LARGE_MODE; - } - - /** - * Initializes various states for this workspace. - */ - @Override - protected void init() { - super.init(); - - mScrollMode = getScrollMode(); - if (mScrollMode == DEFAULT_MODE) { - mBaseLineFlingVelocity = 2500.0f; - mFlingVelocityInfluence = 0.4f; - mScrollInterpolator = new OvershootInterpolator(); - setDefaultInterpolator(mScrollInterpolator); - } - } - - @Override - protected void snapToDestination() { - if (mScrollMode == X_LARGE_MODE) { - super.snapToDestination(); - } else { - snapToPageWithVelocity(getPageNearestToCenterOfScreen(), 0); - } - } - - @Override - protected void snapToPageWithVelocity(int whichPage, int velocity) { - if (mScrollMode == X_LARGE_MODE) { - super.snapToPageWithVelocity(whichPage, velocity); - } else { - snapToPageWithVelocity(whichPage, 0, true); - } - } - - private void snapToPageWithVelocity(int whichPage, int velocity, boolean settle) { - // if (!mScroller.isFinished()) return; - - whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); - - final int screenDelta = Math.max(1, Math.abs(whichPage - mCurrentPage)); - final int newX = getScrollForPage(whichPage); - final int delta = newX - mUnboundedScrollX; - int duration = (screenDelta + 1) * 100; - - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } - - if (settle) { - ((OvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); - } else { - ((OvershootInterpolator) mScrollInterpolator).disableSettle(); - } - - velocity = Math.abs(velocity); - if (velocity > 0) { - duration += (duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence; - } else { - duration += 100; - } - - snapToPage(whichPage, delta, duration); - } - - @Override - protected void snapToPage(int whichPage) { - if (mScrollMode == X_LARGE_MODE) { - super.snapToPage(whichPage); - } else { - snapToPageWithVelocity(whichPage, 0, false); - } - } - - @Override - public void computeScroll() { - if (mScrollMode == X_LARGE_MODE) { - super.computeScroll(); - } else { - boolean scrollComputed = computeScrollHelper(); - - if (!scrollComputed && mTouchState == TOUCH_STATE_SCROLLING) { - final float now = System.nanoTime() / NANOTIME_DIV; - final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT); - - final float dx = mTouchX - mUnboundedScrollX; - scrollTo(Math.round(mUnboundedScrollX + dx * e), getScrollY()); - mSmoothingTime = now; - - // Keep generating points as long as we're more than 1px away from the target - if (dx > 1.f || dx < -1.f) { - invalidate(); - } - } - } - } -} diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index a87986562..cb0e252b2 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -20,16 +20,64 @@ 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 java.io.*; -import java.util.ArrayList; +import android.view.View; +import android.view.ViewParent; public class Stats { - private static final boolean DEBUG_BROADCASTS = false; - private static final String TAG = "Launcher3/Stats"; - private static final boolean LOCAL_LAUNCH_LOG = true; + /** + * 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"; public static final String EXTRA_INTENT = "intent"; @@ -37,55 +85,37 @@ 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"; - private static final int LOG_VERSION = 1; - private static final int LOG_TAG_VERSION = 0x1; - private static final int LOG_TAG_LAUNCH = 0x1000; + 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"; - private static final int STATS_VERSION = 1; - private static final int INITIAL_STATS_SIZE = 100; + 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"; - // TODO: delayed/batched writes - private static final boolean FLUSH_IMMEDIATELY = true; + 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; - DataOutputStream mLog; - - ArrayList<String> mIntents; - ArrayList<Integer> mHistogram; - public Stats(Launcher launcher) { mLauncher = launcher; - mLaunchBroadcastPermission = launcher.getResources().getString(R.string.receive_launch_broadcasts_permission); - loadStats(); - - if (LOCAL_LAUNCH_LOG) { - try { - mLog = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.LAUNCHES_LOG, Context.MODE_APPEND)); - mLog.writeInt(LOG_TAG_VERSION); - mLog.writeInt(LOG_VERSION); - } catch (FileNotFoundException e) { - Log.e(TAG, "unable to create stats log: " + e); - mLog = null; - } catch (IOException e) { - Log.e(TAG, "unable to write to stats log: " + e); - mLog = null; - } - } - if (DEBUG_BROADCASTS) { launcher.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - android.util.Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + intent.getStringExtra(EXTRA_INTENT)); } }, @@ -96,26 +126,11 @@ public class Stats { } } - public void incrementLaunch(String intentStr) { - int pos = mIntents.indexOf(intentStr); - if (pos < 0) { - mIntents.add(intentStr); - mHistogram.add(1); - } else { - mHistogram.set(pos, mHistogram.get(pos) + 1); - } - } - - 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); final String flat = intent.toUri(0); - Intent broadcastIntent = new Intent(ACTION_LAUNCH).putExtra(EXTRA_INTENT, flat); if (shortcut != null) { broadcastIntent.putExtra(EXTRA_CONTAINER, shortcut.container) @@ -123,95 +138,10 @@ public class Stats { .putExtra(EXTRA_CELLX, shortcut.cellX) .putExtra(EXTRA_CELLY, shortcut.cellY); } - mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); - - incrementLaunch(flat); - - if (FLUSH_IMMEDIATELY) { - saveStats(); - } - - if (LOCAL_LAUNCH_LOG && mLog != null) { - try { - mLog.writeInt(LOG_TAG_LAUNCH); - mLog.writeLong(System.currentTimeMillis()); - if (shortcut == null) { - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - } else { - mLog.writeShort((short) shortcut.container); - mLog.writeShort((short) shortcut.screenId); - mLog.writeShort((short) shortcut.cellX); - mLog.writeShort((short) shortcut.cellY); - } - mLog.writeUTF(flat); - if (FLUSH_IMMEDIATELY) { - mLog.flush(); // TODO: delayed writes - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private void saveStats() { - DataOutputStream stats = null; - try { - stats = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.STATS_LOG + ".tmp", Context.MODE_PRIVATE)); - stats.writeInt(STATS_VERSION); - final int N = mHistogram.size(); - stats.writeInt(N); - for (int i=0; i<N; i++) { - stats.writeUTF(mIntents.get(i)); - stats.writeInt(mHistogram.get(i)); - } - stats.close(); - stats = null; - mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG + ".tmp") - .renameTo(mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG)); - } catch (FileNotFoundException e) { - Log.e(TAG, "unable to create stats data: " + e); - } catch (IOException e) { - Log.e(TAG, "unable to write to stats data: " + e); - } finally { - if (stats != null) { - try { - stats.close(); - } catch (IOException e) { } - } - } - } - private void loadStats() { - mIntents = new ArrayList<String>(INITIAL_STATS_SIZE); - mHistogram = new ArrayList<Integer>(INITIAL_STATS_SIZE); - DataInputStream stats = null; - try { - stats = new DataInputStream(mLauncher.openFileInput(LauncherFiles.STATS_LOG)); - final int version = stats.readInt(); - if (version == STATS_VERSION) { - final int N = stats.readInt(); - for (int i=0; i<N; i++) { - final String pkg = stats.readUTF(); - final int count = stats.readInt(); - mIntents.add(pkg); - mHistogram.add(count); - } - } - } catch (FileNotFoundException e) { - // not a problem - } catch (IOException e) { - // more of a problem - - } finally { - if (stats != null) { - try { - stats.close(); - } catch (IOException e) { } - } - } + Bundle sourceExtras = LaunchSourceUtils.createSourceData(); + LaunchSourceUtils.populateSourceDataFromAncestorProvider(v, sourceExtras); + broadcastIntent.putExtra(EXTRA_SOURCE, sourceExtras); + mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); } } diff --git a/src/com/android/launcher3/StylusEventHelper.java b/src/com/android/launcher3/StylusEventHelper.java new file mode 100644 index 000000000..da46e6a54 --- /dev/null +++ b/src/com/android/launcher3/StylusEventHelper.java @@ -0,0 +1,84 @@ +package com.android.launcher3; + +import com.android.launcher3.Utilities; + +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Helper for identifying when a stylus touches a view while the primary stylus button is pressed. + * This can occur in {@value MotionEvent#ACTION_DOWN} or {@value MotionEvent#ACTION_MOVE}. On a + * stylus button press this performs the view's {@link View#performLongClick()} method, if the view + * is long clickable. + */ +public class StylusEventHelper { + private boolean mIsButtonPressed; + private View mView; + + public StylusEventHelper(View view) { + mView = view; + } + + /** + * Call this in onTouchEvent method of a view to identify a stylus button press and perform a + * long click (if the view is long clickable). + * + * @param event The event to check for a stylus button press. + * @return Whether a stylus event occurred and was handled. + */ + public boolean checkAndPerformStylusEvent(MotionEvent event) { + final float slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop(); + + if (!mView.isLongClickable()) { + // We don't do anything unless the view is long clickable. + return false; + } + + final boolean stylusButtonPressed = isStylusButtonPressed(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mIsButtonPressed = false; + if (stylusButtonPressed && mView.performLongClick()) { + mIsButtonPressed = true; + return true; + } + break; + case MotionEvent.ACTION_MOVE: + if (Utilities.pointInView(mView, event.getX(), event.getY(), slop)) { + if (!mIsButtonPressed && stylusButtonPressed && mView.performLongClick()) { + mIsButtonPressed = true; + return true; + } else if (mIsButtonPressed && !stylusButtonPressed) { + mIsButtonPressed = false; + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsButtonPressed = false; + break; + } + return false; + } + + /** + * Whether a stylus button press is occurring. + */ + public boolean inStylusButtonPressed() { + return mIsButtonPressed; + } + + /** + * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button + * pressed. + * + * @param event The event to check. + * @return Whether a stylus button press occurred. + */ + public static boolean isStylusButtonPressed(MotionEvent event) { + return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS + && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java new file mode 100644 index 000000000..0819f8ce0 --- /dev/null +++ b/src/com/android/launcher3/UninstallDropTarget.java @@ -0,0 +1,132 @@ +package com.android.launcher3; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.UserManager; +import android.util.AttributeSet; +import android.util.Pair; +import com.android.launcher3.R; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.Thunk; + +public class UninstallDropTarget extends ButtonDropTarget { + + public UninstallDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UninstallDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + // Get the hover color + mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint); + + setDrawable(R.drawable.ic_uninstall_launcher); + } + + @Override + protected boolean supportsDrop(DragSource source, Object info) { + return supportsDrop(getContext(), info); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + public static boolean supportsDrop(Context context, Object info) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + Bundle restrictions = userManager.getUserRestrictions(); + if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) + || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) { + return false; + } + } + + Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(info); + return componentInfo != null && (componentInfo.second & AppInfo.DOWNLOADED_FLAG) != 0; + } + + /** + * @return the component name and flags if {@param info} is an AppInfo or an app shortcut. + */ + private static Pair<ComponentName, Integer> getAppInfoFlags(Object item) { + if (item instanceof AppInfo) { + AppInfo info = (AppInfo) item; + return Pair.create(info.componentName, info.flags); + } else if (item instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) item; + ComponentName component = info.getTargetComponent(); + if (info.itemType == LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION + && component != null) { + return Pair.create(component, info.flags); + } + } + return null; + } + + @Override + public void onDrop(DragObject d) { + // Differ item deletion + if (d.dragSource instanceof UninstallSource) { + ((UninstallSource) d.dragSource).deferCompleteDropAfterUninstallActivity(); + } + super.onDrop(d); + } + + @Override + void completeDrop(final DragObject d) { + final Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(d.dragInfo); + final UserHandleCompat user = ((ItemInfo) d.dragInfo).user; + if (startUninstallActivity(mLauncher, d.dragInfo)) { + + final Runnable checkIfUninstallWasSuccess = new Runnable() { + @Override + public void run() { + String packageName = componentInfo.first.getPackageName(); + boolean uninstallSuccessful = !AllAppsList.packageHasActivities( + getContext(), packageName, user); + sendUninstallResult(d.dragSource, uninstallSuccessful); + } + }; + mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); + } else { + sendUninstallResult(d.dragSource, false); + } + } + + public static boolean startUninstallActivity(Launcher launcher, Object info) { + final Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(info); + final UserHandleCompat user = ((ItemInfo) info).user; + return launcher.startApplicationUninstallActivity( + componentInfo.first, componentInfo.second, user); + } + + @Thunk void sendUninstallResult(DragSource target, boolean result) { + if (target instanceof UninstallSource) { + ((UninstallSource) target).onUninstallActivityReturned(result); + } + } + + /** + * Interface defining an object that can provide uninstallable drag objects. + */ + public static interface UninstallSource { + + /** + * A pending uninstall operation was complete. + * @param result true if uninstall was successful, false otherwise. + */ + void onUninstallActivityReturned(boolean result); + + /** + * Indicates that an uninstall request are made and the actual result may come + * after some time. + */ + void deferCompleteDropAfterUninstallActivity(); + } +} diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java deleted file mode 100644 index ccea4ec0c..000000000 --- a/src/com/android/launcher3/UninstallShortcutReceiver.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2008 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.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.widget.Toast; - -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Iterator; - -public class UninstallShortcutReceiver extends BroadcastReceiver { - private static final String ACTION_UNINSTALL_SHORTCUT = - "com.android.launcher.action.UNINSTALL_SHORTCUT"; - - // The set of shortcuts that are pending uninstall - private static ArrayList<PendingUninstallShortcutInfo> mUninstallQueue = - new ArrayList<PendingUninstallShortcutInfo>(); - - // Determines whether to defer uninstalling shortcuts immediately until - // disableAndFlushUninstallQueue() is called. - private static boolean mUseUninstallQueue = false; - - private static class PendingUninstallShortcutInfo { - Intent data; - - public PendingUninstallShortcutInfo(Intent rawData) { - data = rawData; - } - } - - public void onReceive(Context context, Intent data) { - if (!ACTION_UNINSTALL_SHORTCUT.equals(data.getAction())) { - return; - } - - PendingUninstallShortcutInfo info = new PendingUninstallShortcutInfo(data); - if (mUseUninstallQueue) { - mUninstallQueue.add(info); - } else { - processUninstallShortcut(context, info); - } - } - - static void enableUninstallQueue() { - mUseUninstallQueue = true; - } - - static void disableAndFlushUninstallQueue(Context context) { - mUseUninstallQueue = false; - Iterator<PendingUninstallShortcutInfo> iter = mUninstallQueue.iterator(); - while (iter.hasNext()) { - processUninstallShortcut(context, iter.next()); - iter.remove(); - } - } - - private static void processUninstallShortcut(Context context, - PendingUninstallShortcutInfo pendingInfo) { - final Intent data = pendingInfo.data; - - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - synchronized (app) { // TODO: make removeShortcut internally threadsafe - removeShortcut(context, data); - } - } - - private static void removeShortcut(Context context, Intent data) { - Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); - String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); - boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); - - if (intent != null && name != null) { - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT }, - LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null); - - final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); - final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - - boolean changed = false; - - try { - while (c.moveToNext()) { - try { - if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) { - final long id = c.getLong(idIndex); - final Uri uri = LauncherSettings.Favorites.getContentUri(id, false); - cr.delete(uri, null, null); - changed = true; - if (!duplicate) { - break; - } - } - } catch (URISyntaxException e) { - // Ignore - } - } - } finally { - c.close(); - } - - if (changed) { - cr.notifyChange(LauncherSettings.Favorites.CONTENT_URI, null); - Toast.makeText(context, context.getString(R.string.shortcut_uninstalled, name), - Toast.LENGTH_SHORT).show(); - } - } - } -} diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/UserInitializeReceiver.java deleted file mode 100644 index d8e17b12f..000000000 --- a/src/com/android/launcher3/UserInitializeReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2012 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Takes care of setting initial wallpaper for a user, by selecting the - * first wallpaper that is not in use by another user. - */ -public class UserInitializeReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // TODO: initial wallpaper now that wallpapers are owned by another app - } -} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 215d63d2e..8fd298df7 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -25,13 +25,16 @@ 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; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; @@ -42,26 +45,38 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import android.util.TypedValue; import android.view.View; import android.widget.Toast; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +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; - private static int sIconHeight = -1; + private static final String TAG = "Launcher.Utilities"; private static final Rect sOldBounds = new Rect(); private static final Canvas sCanvas = new Canvas(); + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + static { sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG)); @@ -69,39 +84,30 @@ public final class Utilities { static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; static int sColorIndex = 0; - static int[] sLoc0 = new int[2]; - static int[] sLoc1 = new int[2]; + private static final int[] sLoc0 = new int[2]; + private static final int[] sLoc1 = new int[2]; // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] - static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; - public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; + private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); - /** - * Returns a FastBitmapDrawable with the icon, accurately sized. - */ - public static FastBitmapDrawable createIconDrawable(Bitmap icon) { - FastBitmapDrawable d = new FastBitmapDrawable(icon); - d.setFilterBitmap(true); - resizeIconDrawable(d); - return d; - } - - /** - * Resizes an icon drawable to the correct icon size. - */ - static void resizeIconDrawable(Drawable icon) { - icon.setBounds(0, 0, sIconWidth, sIconHeight); - } + public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; public static boolean isPropertyEnabled(String propertyName) { 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, boolean multiProcess) { + SharedPreferences sharedPrefs = context.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE | (multiProcess ? + Context.MODE_MULTI_PROCESS : 0)); + 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); } /** @@ -111,11 +117,30 @@ public final class Utilities { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } + public static boolean isLmpMR1OrAbove() { + // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; + return Build.VERSION.SDK_INT >= 22; + } + + public static boolean isLmpMR1() { + // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; + return Build.VERSION.SDK_INT == 22; + } + + public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { + byte[] data = c.getBlob(iconIndex); + try { + return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context); + } catch (Exception e) { + return null; + } + } + /** * Returns a bitmap suitable for the all apps view. If the package or the resource do not * exist, it returns null. */ - static Bitmap createIconBitmap(String packageName, String resourceName, IconCache cache, + public static Bitmap createIconBitmap(String packageName, String resourceName, Context context) { PackageManager packageManager = context.getPackageManager(); // the resource @@ -124,7 +149,8 @@ public final class Utilities { if (resources != null) { final int id = resources.getIdentifier(resourceName, null, null); return createIconBitmap( - resources.getDrawableForDensity(id, cache.getFullResIconDpi()), context); + resources.getDrawableForDensity(id, LauncherAppState.getInstance() + .getInvariantDeviceProfile().fillResIconDpi), context); } } catch (Exception e) { // Icon not found. @@ -132,16 +158,16 @@ public final class Utilities { return null; } + private static int getIconBitmapSize() { + return LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize; + } + /** * Returns a bitmap which is of the appropriate size to be displayed as an icon */ - static Bitmap createIconBitmap(Bitmap icon, Context context) { - synchronized (sCanvas) { // we share the statics :-( - if (sIconWidth == -1) { - initStatics(context); - } - } - if (sIconWidth == icon.getWidth() && sIconHeight == icon.getHeight()) { + public static Bitmap createIconBitmap(Bitmap icon, Context context) { + final int iconBitmapSize = getIconBitmapSize(); + if (iconBitmapSize == icon.getWidth() && iconBitmapSize == icon.getHeight()) { return icon; } return createIconBitmap(new BitmapDrawable(context.getResources(), icon), context); @@ -151,13 +177,11 @@ public final class Utilities { * Returns a bitmap suitable for the all apps view. */ public static Bitmap createIconBitmap(Drawable icon, Context context) { - synchronized (sCanvas) { // we share the statics :-( - if (sIconWidth == -1) { - initStatics(context); - } + synchronized (sCanvas) { + final int iconBitmapSize = getIconBitmapSize(); - int width = sIconWidth; - int height = sIconHeight; + int width = iconBitmapSize; + int height = iconBitmapSize; if (icon instanceof PaintDrawable) { PaintDrawable painter = (PaintDrawable) icon; @@ -184,8 +208,8 @@ public final class Utilities { } // no intrinsic size --> use default size - int textureWidth = sIconWidth; - int textureHeight = sIconHeight; + int textureWidth = iconBitmapSize; + int textureHeight = iconBitmapSize; final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, Bitmap.Config.ARGB_8888); @@ -315,15 +339,6 @@ public final class Utilities { localY < (v.getHeight() + slop); } - private static void initStatics(Context context) { - final Resources resources = context.getResources(); - sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); - } - - public static void setIconSize(int widthPx) { - sIconWidth = sIconHeight = widthPx; - } - public static void scaleRect(Rect r, float scale) { if (scale != 1.0f) { r.left = (int) (r.left * scale + 0.5f); @@ -542,4 +557,156 @@ public final class Utilities { } return defaultWidgetForSearchPackage; } + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + /** + * Find the first vacant cell, if there is one. + * + * @param vacant Holds the x and y coordinate of the vacant cell + * @param spanX Horizontal cell span. + * @param spanY Vertical cell span. + * + * @return true if a vacant cell was found + */ + public static boolean findVacantCell(int[] vacant, int spanX, int spanY, + int xCount, int yCount, boolean[][] occupied) { + + for (int y = 0; (y + spanY) <= yCount; y++) { + for (int x = 0; (x + spanX) <= xCount; x++) { + boolean available = !occupied[x][y]; + out: for (int i = x; i < x + spanX; i++) { + for (int j = y; j < y + spanY; j++) { + available = available && !occupied[i][j]; + if (!available) break out; + } + } + + if (available) { + vacant[0] = x; + vacant[1] = y; + return true; + } + } + } + + return false; + } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + if (s == null) { + return null; + } + + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } + + /** + * Calculates the height of a given string at a specific text size. + */ + public static float calculateTextHeight(float textSizePx) { + Paint p = new Paint(); + p.setTextSize(textSizePx); + Paint.FontMetrics fm = p.getFontMetrics(); + return -fm.top + fm.bottom; + } + + /** + * Convenience println with multiple args. + */ + public static void println(String key, Object... args) { + StringBuilder b = new StringBuilder(); + b.append(key); + b.append(": "); + boolean isFirstArgument = true; + for (Object arg : args) { + if (isFirstArgument) { + isFirstArgument = false; + } else { + b.append(", "); + } + b.append(arg); + } + System.out.println(b.toString()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isRtl(Resources res) { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) && + (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } + + public static void assertWorkerThread() { + if (LauncherAppState.isDogfoodBuild() && + (LauncherModel.sWorkerThread.getThreadId() != Process.myTid())) { + throw new IllegalStateException(); + } + } + + /** + * Returns true if the intent is a valid launch intent for a launcher activity of an app. + * This is used to identify shortcuts which are different from the ones exposed by the + * applications' manifest file. + * + * @param launchIntent The intent that will be launched when the shortcut is clicked. + */ + public static boolean isLauncherAppTarget(Intent launchIntent) { + if (launchIntent != null + && Intent.ACTION_MAIN.equals(launchIntent.getAction()) + && launchIntent.getComponent() != null + && launchIntent.getCategories() != null + && launchIntent.getCategories().size() == 1 + && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) + && TextUtils.isEmpty(launchIntent.getDataString())) { + // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE. + Bundle extras = launchIntent.getExtras(); + if (extras == null) { + return true; + } else { + Set<String> keys = extras.keySet(); + return keys.size() == 1 && keys.contains(ItemInfo.EXTRA_PROFILE); + } + }; + return false; + } + + public static float dpiFromPx(int size, DisplayMetrics metrics){ + float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; + return (size / densityRatio); + } + public static int pxFromDp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, metrics)); + } + public static int pxFromSp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, metrics)); + } + + public static String createDbSelectionQuery(String columnName, Iterable<?> values) { + return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); + } } diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/WeightWatcher.java index 70b8afea8..75684797f 100644 --- a/src/com/android/launcher3/WeightWatcher.java +++ b/src/com/android/launcher3/WeightWatcher.java @@ -34,6 +34,8 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + public class WeightWatcher extends LinearLayout { private static final int RAM_GRAPH_RSS_COLOR = 0xFF990000; private static final int RAM_GRAPH_PSS_COLOR = 0xFF99CC00; @@ -81,7 +83,7 @@ public class WeightWatcher extends LinearLayout { } } }; - private MemoryTracker mMemoryService; + @Thunk MemoryTracker mMemoryService; public WeightWatcher(Context context, AttributeSet attrs) { super(context, attrs); @@ -134,7 +136,7 @@ public class WeightWatcher extends LinearLayout { GraphView mRamGraph; TextView mText; int mPid; - private MemoryTracker.ProcessMemInfo mMemInfo; + @Thunk MemoryTracker.ProcessMemInfo mMemInfo; public ProcessWatcher(Context context) { this(context, null); diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 4e6fe1f88..629387ed0 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -1,526 +1,353 @@ package com.android.launcher3; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; -import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDiskIOException; import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteReadOnlyDatabaseException; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Rect; -import android.graphics.Shader; +import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.os.Build; +import android.os.Handler; import android.util.Log; +import android.util.LongSparseArray; + import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetCell; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; public class WidgetPreviewLoader { - private static abstract class SoftReferenceThreadLocal<T> { - private ThreadLocal<SoftReference<T>> mThreadLocal; - public SoftReferenceThreadLocal() { - mThreadLocal = new ThreadLocal<SoftReference<T>>(); - } - - abstract T initialValue(); - - public void set(T t) { - mThreadLocal.set(new SoftReference<T>(t)); - } - - public T get() { - SoftReference<T> reference = mThreadLocal.get(); - T obj; - if (reference == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference<T>(obj)); - return obj; - } else { - obj = reference.get(); - if (obj == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference<T>(obj)); - } - return obj; - } - } - } - - private static class CanvasCache extends SoftReferenceThreadLocal<Canvas> { - @Override - protected Canvas initialValue() { - return new Canvas(); - } - } - - private static class PaintCache extends SoftReferenceThreadLocal<Paint> { - @Override - protected Paint initialValue() { - return null; - } - } - - private static class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { - @Override - protected Bitmap initialValue() { - return null; - } - } - - private static class RectCache extends SoftReferenceThreadLocal<Rect> { - @Override - protected Rect initialValue() { - return new Rect(); - } - } - - private static class BitmapFactoryOptionsCache extends - SoftReferenceThreadLocal<BitmapFactory.Options> { - @Override - protected BitmapFactory.Options initialValue() { - return new BitmapFactory.Options(); - } - } - private static final String TAG = "WidgetPreviewLoader"; - private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; + private static final boolean DEBUG = false; private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; - private static final HashSet<String> sInvalidPackages = new HashSet<String>(); - - // Used for drawing shortcut previews - private final BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); - private final PaintCache mCachedShortcutPreviewPaint = new PaintCache(); - private final CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); - // Used for drawing widget previews - private final CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); - private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); - private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); - private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); - private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache(); - private final BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); + private final HashMap<String, long[]> mPackageVersions = new HashMap<>(); - private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>(); - private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); + /** + * Weak reference objects, do not prevent their referents from being made finalizable, + * finalized, and then reclaimed. + * Note: synchronized block used for this variable is expensive and the block should always + * be posted to a background thread. + */ + @Thunk final Set<Bitmap> mUnusedBitmaps = + Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); private final Context mContext; - private final int mAppIconSize; private final IconCache mIconCache; + private final UserManagerCompat mUserManager; private final AppWidgetManagerCompat mManager; - - private int mPreviewBitmapWidth; - private int mPreviewBitmapHeight; - private String mSize; - private PagedViewCellLayout mWidgetSpacingLayout; - - private String mCachedSelectQuery; - - - private CacheDb mDb; + private final CacheDb mDb; + private final int mProfileBadgeMargin; private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); + @Thunk final Handler mWorkerHandler; - public WidgetPreviewLoader(Context context) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - + public WidgetPreviewLoader(Context context, IconCache iconCache) { mContext = context; - mAppIconSize = grid.iconSizePx; - mIconCache = app.getIconCache(); + mIconCache = iconCache; mManager = AppWidgetManagerCompat.getInstance(context); - - mDb = app.getWidgetPreviewCacheDb(); - - SharedPreferences sp = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null); - final String versionName = android.os.Build.VERSION.INCREMENTAL; - final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - if (!versionName.equals(lastVersionName)) { - try { - // clear all the previews whenever the system version changes, to ensure that - // previews are up-to-date for any apps that might have been updated with the system - clearDb(); - } catch (SQLiteReadOnlyDatabaseException e) { - if (isLollipopOrGreater) { - // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only - // issue, then ignore this error and leave the old previews - } else { - throw e; - } - } finally { - SharedPreferences.Editor editor = sp.edit(); - editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName); - editor.commit(); - } - } + mUserManager = UserManagerCompat.getInstance(context); + mDb = new CacheDb(context); + mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); + mProfileBadgeMargin = context.getResources() + .getDimensionPixelSize(R.dimen.profile_badge_margin); } - public void recreateDb() { - LauncherAppState app = LauncherAppState.getInstance(); - app.recreateWidgetPreviewDb(); - mDb = app.getWidgetPreviewCacheDb(); - } - - public void setPreviewSize(int previewWidth, int previewHeight, - PagedViewCellLayout widgetSpacingLayout) { - mPreviewBitmapWidth = previewWidth; - mPreviewBitmapHeight = previewHeight; - mSize = previewWidth + "x" + previewHeight; - mWidgetSpacingLayout = widgetSpacingLayout; - } - - public Bitmap getPreview(final Object o) { - final String name = getObjectName(o); - final String packageName = getObjectPackage(o); - // check if the package is valid - synchronized(sInvalidPackages) { - boolean packageValid = !sInvalidPackages.contains(packageName); - if (!packageValid) { - return null; - } - } - synchronized(mLoadedPreviews) { - // check if it exists in our existing cache - if (mLoadedPreviews.containsKey(name)) { - WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name); - Bitmap bitmap = bitmapReference.get(); - if (bitmap != null) { - return bitmap; - } - } - } - - Bitmap unusedBitmap = null; - synchronized(mUnusedBitmaps) { - // not in cache; we need to load it from the db - while (unusedBitmap == null && mUnusedBitmaps.size() > 0) { - Bitmap candidate = mUnusedBitmaps.remove(0).get(); - if (candidate != null && candidate.isMutable() && - candidate.getWidth() == mPreviewBitmapWidth && - candidate.getHeight() == mPreviewBitmapHeight) { - unusedBitmap = candidate; - } - } - if (unusedBitmap != null) { - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - c.setBitmap(unusedBitmap); - c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); - } - } - - if (unusedBitmap == null) { - unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, - Bitmap.Config.ARGB_8888); - } - Bitmap preview = readFromDb(name, unusedBitmap); - - if (preview != null) { - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); - } - return preview; - } else { - // it's not in the db... we need to generate it - final Bitmap generatedPreview = generatePreview(o, unusedBitmap); - preview = generatedPreview; - if (preview != unusedBitmap) { - throw new RuntimeException("generatePreview is not recycling the bitmap " + o); - } - - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); - } - - // write to db on a thread pool... this can be done lazily and improves the performance - // of the first time widget previews are loaded - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - writeToDb(o, generatedPreview); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - - return preview; - } - } - - public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { - String name = getObjectName(o); - synchronized (mLoadedPreviews) { - if (mLoadedPreviews.containsKey(name)) { - Bitmap b = mLoadedPreviews.get(name).get(); - if (b == bitmapToRecycle) { - mLoadedPreviews.remove(name); - if (bitmapToRecycle.isMutable()) { - synchronized (mUnusedBitmaps) { - mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); - } - } - } else { - throw new RuntimeException("Bitmap passed in doesn't match up"); - } - } - } + /** + * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be + * called on UI thread + * + * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo} + * @return a request id which can be used to cancel the request. + */ + public PreviewLoadRequest getPreview(final Object o, int previewWidth, + int previewHeight, WidgetCell caller) { + String size = previewWidth + "x" + previewHeight; + WidgetCacheKey key = getObjectKey(o, size); + + PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return new PreviewLoadRequest(task); } - static class CacheDb extends SQLiteOpenHelper { - final static int DB_VERSION = 2; - final static String TABLE_NAME = "shortcut_and_widget_previews"; - final static String COLUMN_NAME = "name"; - final static String COLUMN_SIZE = "size"; - final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; - Context mContext; + /** + * The DB holds the generated previews for various components. Previews can also have different + * sizes (landscape vs portrait). + */ + private static class CacheDb extends SQLiteOpenHelper { + private static final int DB_VERSION = 3; + + private static final String TABLE_NAME = "shortcut_and_widget_previews"; + private static final String COLUMN_COMPONENT = "componentName"; + private static final String COLUMN_USER = "profileId"; + private static final String COLUMN_SIZE = "size"; + private static final String COLUMN_PACKAGE = "packageName"; + private static final String COLUMN_LAST_UPDATED = "lastUpdated"; + private static final String COLUMN_VERSION = "version"; + private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; public CacheDb(Context context) { - super(context, new File(context.getCacheDir(), - LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION); - // Store the context for later use - mContext = context; + super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + - COLUMN_NAME + " TEXT NOT NULL, " + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + COLUMN_SIZE + " TEXT NOT NULL, " + - COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + - "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + + COLUMN_PACKAGE + " TEXT NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_PREVIEW_BITMAP + " BLOB, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion != newVersion) { - // Delete all the records; they'll be repopulated as this is a cache - db.execSQL("DELETE FROM " + TABLE_NAME); + clearDB(db); } } - } - - private static final String WIDGET_PREFIX = "Widget:"; - private static final String SHORTCUT_PREFIX = "Shortcut:"; - private static String getObjectName(Object o) { - // should cache the string builder - StringBuilder sb = new StringBuilder(); - String output; - if (o instanceof AppWidgetProviderInfo) { - sb.append(WIDGET_PREFIX); - sb.append(((AppWidgetProviderInfo) o).toString()); - output = sb.toString(); - sb.setLength(0); - } else { - sb.append(SHORTCUT_PREFIX); + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } - ResolveInfo info = (ResolveInfo) o; - sb.append(new ComponentName(info.activityInfo.packageName, - info.activityInfo.name).flattenToString()); - output = sb.toString(); - sb.setLength(0); + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); } - return output; } - private String getObjectPackage(Object o) { - if (o instanceof AppWidgetProviderInfo) { - return ((AppWidgetProviderInfo) o).provider.getPackageName(); + private WidgetCacheKey getObjectKey(Object o, String size) { + // should cache the string builder + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o; + return new WidgetCacheKey(info.provider, mManager.getUser(info), size); } else { ResolveInfo info = (ResolveInfo) o; - return info.activityInfo.packageName; + return new WidgetCacheKey( + new ComponentName(info.activityInfo.packageName, info.activityInfo.name), + UserHandleCompat.myUserHandle(), size); } } - private void writeToDb(Object o, Bitmap preview) { - String name = getObjectName(o); - SQLiteDatabase db = mDb.getWritableDatabase(); + @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) { ContentValues values = new ContentValues(); + values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString()); + values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user)); + values.put(CacheDb.COLUMN_SIZE, key.size); + values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName()); + values.put(CacheDb.COLUMN_VERSION, versions[0]); + values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); + values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview)); - values.put(CacheDb.COLUMN_NAME, name); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - preview.compress(Bitmap.CompressFormat.PNG, 100, stream); - values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); - values.put(CacheDb.COLUMN_SIZE, mSize); try { - db.insert(CacheDb.TABLE_NAME, null, values); - } catch (SQLiteDiskIOException e) { - recreateDb(); - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLException e) { + Log.e(TAG, "Error saving image to DB", e); } } - private void clearDb() { - SQLiteDatabase db = mDb.getWritableDatabase(); - // Delete everything + public void removePackage(String packageName, UserHandleCompat user) { + removePackage(packageName, user, mUserManager.getSerialNumberForUser(user)); + } + + private void removePackage(String packageName, UserHandleCompat user, long userSerial) { + synchronized(mPackageVersions) { + mPackageVersions.remove(packageName); + } + try { - db.delete(CacheDb.TABLE_NAME, null, null); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME, + CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", + new String[] {packageName, Long.toString(userSerial)}); + } catch (SQLException e) { + Log.e(TAG, "Unable to delete items from DB", e); } } - public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) { - synchronized(sInvalidPackages) { - sInvalidPackages.add(packageName); + /** + * Updates the persistent DB: + * 1. Any preview generated for an old package version is removed + * 2. Any preview for an absent package is removed + * This ensures that we remove entries for packages which changed while the launcher was dead. + */ + public void removeObsoletePreviews(ArrayList<Object> list) { + Utilities.assertWorkerThread(); + + LongSparseArray<UserHandleCompat> userIdCache = new LongSparseArray<>(); + LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>(); + + for (Object obj : list) { + final UserHandleCompat user; + final String pkg; + if (obj instanceof ResolveInfo) { + user = UserHandleCompat.myUserHandle(); + pkg = ((ResolveInfo) obj).activityInfo.packageName; + } else { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj; + user = mManager.getUser(info); + pkg = info.provider.getPackageName(); + } + + int userIdIndex = userIdCache.indexOfValue(user); + final long userId; + if (userIdIndex < 0) { + userId = mUserManager.getSerialNumberForUser(user); + userIdCache.put(userId, user); + } else { + userId = userIdCache.keyAt(userIdIndex); + } + + HashSet<String> packages = validPackages.get(userId); + if (packages == null) { + packages = new HashSet<>(); + validPackages.put(userId, packages); + } + packages.add(pkg); } - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); - try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " LIKE ? OR " + - CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query - new String[] { - WIDGET_PREFIX + packageName + "/%", - SHORTCUT_PREFIX + packageName + "/%" - } // args to SELECT query - ); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + + LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>(); + Cursor c = null; + try { + c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME, + new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, + CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, + null, null, null, null, null); + while (c.moveToNext()) { + long userId = c.getLong(0); + String pkg = c.getString(1); + long lastUpdated = c.getLong(2); + long version = c.getLong(3); + + HashSet<String> packages = validPackages.get(userId); + if (packages != null && packages.contains(pkg)) { + long[] versions = getPackageVersion(pkg); + if (versions[0] == version && versions[1] == lastUpdated) { + // Every thing checks out + continue; + } } - synchronized(sInvalidPackages) { - sInvalidPackages.remove(packageName); + + // We need to delete this package. + packages = packagesToDelete.get(userId); + if (packages == null) { + packages = new HashSet<>(); + packagesToDelete.put(userId, packages); } - return null; + packages.add(pkg); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); - try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " = ? ", // SELECT query - new String[] { objectName }); // args to SELECT query - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + for (int i = 0; i < packagesToDelete.size(); i++) { + long userId = packagesToDelete.keyAt(i); + UserHandleCompat user = mUserManager.getUserForSerialNumber(userId); + for (String pkg : packagesToDelete.valueAt(i)) { + removePackage(pkg, user, userId); } - return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } catch (SQLException e) { + Log.e(TAG, "Error updatating widget previews", e); + } finally { + if (c != null) { + c.close(); + } + } } - private Bitmap readFromDb(String name, Bitmap b) { - if (mCachedSelectQuery == null) { - mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + - CacheDb.COLUMN_SIZE + " = ?"; - } - SQLiteDatabase db = mDb.getReadableDatabase(); - Cursor result; + /** + * Reads the preview bitmap from the DB or null if the preview is not in the DB. + */ + @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { + Cursor cursor = null; try { - result = db.query(CacheDb.TABLE_NAME, - new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return - mCachedSelectQuery, // select query - new String[] { name, mSize }, // args to select query - null, - null, - null, - null); - } catch (SQLiteDiskIOException e) { - recreateDb(); - return null; - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; - } - if (result.getCount() > 0) { - result.moveToFirst(); - byte[] blob = result.getBlob(0); - result.close(); - final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); - opts.inBitmap = b; - opts.inSampleSize = 1; - try { - return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); - } catch (IllegalArgumentException e) { - removeItemFromDb(mDb, name); + cursor = mDb.getReadableDatabase().query( + CacheDb.TABLE_NAME, + new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, + CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?", + new String[] { + key.componentName.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(key.user)), + key.size + }, + null, null, null); + // If cancelled, skip getting the blob and decoding it into a bitmap + if (loadTask.isCancelled()) { return null; } - } else { - result.close(); - return null; + if (cursor.moveToNext()) { + byte[] blob = cursor.getBlob(0); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inBitmap = recycle; + try { + if (!loadTask.isCancelled()) { + return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); + } + } catch (Exception e) { + return null; + } + } + } catch (SQLException e) { + Log.w(TAG, "Error loading preview from DB", e); + } finally { + if (cursor != null) { + cursor.close(); + } } + return null; } - private Bitmap generatePreview(Object info, Bitmap preview) { - if (preview != null && - (preview.getWidth() != mPreviewBitmapWidth || - preview.getHeight() != mPreviewBitmapHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } - if (info instanceof AppWidgetProviderInfo) { - return generateWidgetPreview((AppWidgetProviderInfo) info, preview); + @Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle, + int previewWidth, int previewHeight) { + if (info instanceof LauncherAppWidgetProviderInfo) { + return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info, + previewWidth, recycle, null); } else { - return generateShortcutPreview( - (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); + return generateShortcutPreview(launcher, + (ResolveInfo) info, previewWidth, previewHeight, recycle); } } - public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { - int[] cellSpans = Launcher.getSpanForWidget(mContext, info); - int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); - int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); - return generateWidgetPreview(info, cellSpans[0], cellSpans[1], - maxWidth, maxHeight, preview, null); - } - - public int maxWidthForWidgetPreview(int spanX) { - return Math.min(mPreviewBitmapWidth, - mWidgetSpacingLayout.estimateCellWidth(spanX)); - } - - public int maxHeightForWidgetPreview(int spanY) { - return Math.min(mPreviewBitmapHeight, - mWidgetSpacingLayout.estimateCellHeight(spanY)); - } - - public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, int cellHSpan, int cellVSpan, - int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) { + public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info, + int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) { // Load the preview image if possible if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; - if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; Drawable drawable = null; if (info.previewImage != 0) { @@ -533,61 +360,23 @@ public class WidgetPreviewLoader { } } + final boolean widgetPreviewExists = (drawable != null); + final int spanX = info.getSpanX(launcher) < 1 ? 1 : info.getSpanX(launcher); + final int spanY = info.getSpanY(launcher) < 1 ? 1 : info.getSpanY(launcher); + int previewWidth; int previewHeight; - Bitmap defaultPreview = null; - boolean widgetPreviewExists = (drawable != null); + Bitmap tileBitmap = null; + if (widgetPreviewExists) { previewWidth = drawable.getIntrinsicWidth(); previewHeight = drawable.getIntrinsicHeight(); } else { // Generate a preview image if we couldn't load one - if (cellHSpan < 1) cellHSpan = 1; - if (cellVSpan < 1) cellVSpan = 1; - - // This Drawable is not directly drawn, so there's no need to mutate it. - BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() - .getDrawable(R.drawable.widget_tile); - final int previewDrawableWidth = previewDrawable - .getIntrinsicWidth(); - final int previewDrawableHeight = previewDrawable - .getIntrinsicHeight(); - previewWidth = previewDrawableWidth * cellHSpan; - previewHeight = previewDrawableHeight * cellVSpan; - - defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - c.setBitmap(defaultPreview); - Paint p = mDefaultAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setShader(new BitmapShader(previewDrawable.getBitmap(), - Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); - mDefaultAppWidgetPreviewPaint.set(p); - } - final Rect dest = mCachedAppWidgetPreviewDestRect.get(); - dest.set(0, 0, previewWidth, previewHeight); - c.drawRect(dest, p); - c.setBitmap(null); - - // Draw the icon in the top left corner - int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); - int smallestSide = Math.min(previewWidth, previewHeight); - float iconScale = Math.min((float) smallestSide - / (mAppIconSize + 2 * minOffset), 1f); - - try { - Drawable icon = mManager.loadIcon(info, mIconCache); - if (icon != null) { - int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); - int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); - icon = mutateOnMainThread(icon); - renderDrawableToBitmap(icon, defaultPreview, hoffset, - yoffset, (int) (mAppIconSize * iconScale), - (int) (mAppIconSize * iconScale)); - } - } catch (Resources.NotFoundException e) { - } + tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable( + R.drawable.widget_tile)).getBitmap(); + previewWidth = tileBitmap.getWidth() * spanX; + previewHeight = tileBitmap.getHeight() * spanY; } // Scale to fit width only - let the widget preview be clipped in the @@ -605,102 +394,110 @@ public class WidgetPreviewLoader { } // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size + final Canvas c = new Canvas(); if (preview == null) { preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else { + // Reusing bitmap. Clear it. + c.setBitmap(preview); + c.drawColor(0, PorterDuff.Mode.CLEAR); } // Draw the scaled preview into the final bitmap - int x = (preview.getWidth() - previewWidth) / 2; + int x = (preview.getWidth() - previewWidth - mProfileBadgeMargin) / 2; if (widgetPreviewExists) { - renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, - previewHeight); + drawable.setBounds(x, 0, x + previewWidth, previewHeight); + drawable.draw(c); } else { - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - final Rect src = mCachedAppWidgetPreviewSrcRect.get(); - final Rect dest = mCachedAppWidgetPreviewDestRect.get(); - c.setBitmap(preview); - src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); - dest.set(x, 0, x + previewWidth, previewHeight); - - Paint p = mCachedAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setFilterBitmap(true); - mCachedAppWidgetPreviewPaint.set(p); + final Paint p = new Paint(); + p.setFilterBitmap(true); + int appIconSize = launcher.getDeviceProfile().iconSizePx; + + // draw the spanX x spanY tiles + final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight()); + + float tileW = scale * tileBitmap.getWidth(); + float tileH = scale * tileBitmap.getHeight(); + final RectF dst = new RectF(0, 0, tileW, tileH); + + float tx = x; + for (int i = 0; i < spanX; i++, tx += tileW) { + float ty = 0; + for (int j = 0; j < spanY; j++, ty += tileH) { + dst.offsetTo(tx, ty); + c.drawBitmap(tileBitmap, src, dst, p); + } } - c.drawBitmap(defaultPreview, src, dest, p); + + // Draw the icon in the top left corner + // TODO: use top right for RTL + int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); + int smallestSide = Math.min(previewWidth, previewHeight); + float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale); + + try { + Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache)); + if (icon != null) { + int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x; + int yoffset = (int) ((tileH - appIconSize * iconScale) / 2); + icon.setBounds(hoffset, yoffset, + hoffset + (int) (appIconSize * iconScale), + yoffset + (int) (appIconSize * iconScale)); + icon.draw(c); + } + } catch (Resources.NotFoundException e) { } c.setBitmap(null); } - return mManager.getBadgeBitmap(info, preview); + int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin); + return mManager.getBadgeBitmap(info, preview, imageHeight); } private Bitmap generateShortcutPreview( - ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { - Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); - final Canvas c = mCachedShortcutPreviewCanvas.get(); - if (tempBitmap == null || - tempBitmap.getWidth() != maxWidth || - tempBitmap.getHeight() != maxHeight) { - tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); - mCachedShortcutPreviewBitmap.set(tempBitmap); + Launcher launcher, ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { + final Canvas c = new Canvas(); + if (preview == null) { + preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) { + throw new RuntimeException("Improperly sized bitmap passed as argument"); } else { - c.setBitmap(tempBitmap); + // Reusing bitmap. Clear it. + c.setBitmap(preview); c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); } - // Render the icon - Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); - int paddingTop = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); - int paddingLeft = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); - int paddingRight = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); + Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); + icon.setFilterBitmap(true); + // Draw a desaturated/scaled version of the icon in the background as a watermark + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + icon.setAlpha((int) (255 * 0.06f)); + + Resources res = mContext.getResources(); + int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); + int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); + int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); + icon.setBounds(paddingLeft, paddingTop, + paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth); + icon.draw(c); - renderDrawableToBitmap( - icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); + // Draw the final icon at top left corner. + // TODO: use top right for RTL + int appIconSize = launcher.getDeviceProfile().iconSizePx; - if (preview != null && - (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } else if (preview == null) { - preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); - } + icon.setAlpha(255); + icon.setColorFilter(null); + icon.setBounds(0, 0, appIconSize, appIconSize); + icon.draw(c); - c.setBitmap(preview); - // Draw a desaturated/scaled version of the icon in the background as a watermark - Paint p = mCachedShortcutPreviewPaint.get(); - if (p == null) { - p = new Paint(); - ColorMatrix colorMatrix = new ColorMatrix(); - colorMatrix.setSaturation(0); - p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); - p.setAlpha((int) (255 * 0.06f)); - mCachedShortcutPreviewPaint.set(p); - } - c.drawBitmap(tempBitmap, 0, 0, p); c.setBitmap(null); - - renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); - return preview; } - private static void renderDrawableToBitmap( - Drawable d, Bitmap bitmap, int x, int y, int w, int h) { - if (bitmap != null) { - Canvas c = new Canvas(bitmap); - Rect oldBounds = d.copyBounds(); - d.setBounds(x, y, x + w, y + h); - d.draw(c); - d.setBounds(oldBounds); // Restore the bounds - c.setBitmap(null); - } - } - private Drawable mutateOnMainThread(final Drawable drawable) { try { return mMainThreadExecutor.submit(new Callable<Drawable>() { @@ -717,82 +514,195 @@ public class WidgetPreviewLoader { } } - private static final int MAX_OPEN_FILES = 1024; - private static final int SAMPLE_RATE = 23; /** - * Dumps all files that are open in this process without allocating a file descriptor. + * @return an array of containing versionCode and lastUpdatedTime for the package. */ - private static void dumpOpenFiles() { - try { - Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):"); - final String TYPE_APK = "apk"; - final String TYPE_JAR = "jar"; - final String TYPE_PIPE = "pipe"; - final String TYPE_SOCKET = "socket"; - final String TYPE_DB = "db"; - final String TYPE_ANON_INODE = "anon_inode"; - final String TYPE_DEV = "dev"; - final String TYPE_NON_FS = "non-fs"; - final String TYPE_OTHER = "other"; - List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB, - TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER); - int[] count = new int[types.size()]; - int[] duplicates = new int[types.size()]; - HashSet<String> files = new HashSet<String>(); - int total = 0; - for (int i = 0; i < MAX_OPEN_FILES; i++) { - // This is a gigantic hack but unfortunately the only way to resolve an fd - // to a file name. Note that we have to loop over all possible fds because - // reading the directory would require allocating a new fd. The kernel is - // currently implemented such that no fd is larger then the current rlimit, - // which is why it's safe to loop over them in such a way. - String fd = "/proc/self/fd/" + i; + @Thunk long[] getPackageVersion(String packageName) { + synchronized (mPackageVersions) { + long[] versions = mPackageVersions.get(packageName); + if (versions == null) { + versions = new long[2]; try { - // getCanonicalPath() uses readlink behind the scene which doesn't require - // a file descriptor. - String resolved = new File(fd).getCanonicalPath(); - int type = types.indexOf(TYPE_OTHER); - if (resolved.startsWith("/dev/")) { - type = types.indexOf(TYPE_DEV); - } else if (resolved.endsWith(".apk")) { - type = types.indexOf(TYPE_APK); - } else if (resolved.endsWith(".jar")) { - type = types.indexOf(TYPE_JAR); - } else if (resolved.contains("/fd/pipe:")) { - type = types.indexOf(TYPE_PIPE); - } else if (resolved.contains("/fd/socket:")) { - type = types.indexOf(TYPE_SOCKET); - } else if (resolved.contains("/fd/anon_inode:")) { - type = types.indexOf(TYPE_ANON_INODE); - } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) { - type = types.indexOf(TYPE_DB); - } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) { - // Those are the files that don't point anywhere on the file system. - // getCanonicalPath() wrongly interprets these as relative symlinks and - // resolves them within /proc/<pid>/fd/. - type = types.indexOf(TYPE_NON_FS); - } - count[type]++; - total++; - if (files.contains(resolved)) { - duplicates[type]++; + PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0); + versions[0] = info.versionCode; + versions[1] = info.lastUpdateTime; + } catch (NameNotFoundException e) { + Log.e(TAG, "PackageInfo not found", e); + } + mPackageVersions.put(packageName, versions); + } + return versions; + } + } + + /** + * A request Id which can be used by the client to cancel any request. + */ + public class PreviewLoadRequest { + + @Thunk final PreviewLoadTask mTask; + + public PreviewLoadRequest(PreviewLoadTask task) { + mTask = task; + } + + public void cleanup() { + if (mTask != null) { + mTask.cancel(true); + } + + // This only handles the case where the PreviewLoadTask is cancelled after the task has + // successfully completed (including having written to disk when necessary). In the + // other cases where it is cancelled while the task is running, it will be cleaned up + // in the tasks's onCancelled() call, and if cancelled while the task is writing to + // disk, it will be cancelled in the task's onPostExecute() call. + if (mTask.mBitmapToRecycle != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(mTask.mBitmapToRecycle); + } + mTask.mBitmapToRecycle = null; } - files.add(resolved); - if (total % SAMPLE_RATE == 0) { - Log.i(TAG, " fd " + i + ": " + resolved - + " (" + types.get(type) + ")"); + }); + } + } + } + + public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> { + @Thunk final WidgetCacheKey mKey; + private final Object mInfo; + private final int mPreviewHeight; + private final int mPreviewWidth; + private final WidgetCell mCaller; + @Thunk long[] mVersions; + @Thunk Bitmap mBitmapToRecycle; + + PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth, + int previewHeight, WidgetCell caller) { + mKey = key; + mInfo = info; + mPreviewHeight = previewHeight; + mPreviewWidth = previewWidth; + mCaller = caller; + if (DEBUG) { + Log.d(TAG, String.format("%s, %s, %d, %d", + mKey, mInfo, mPreviewHeight, mPreviewWidth)); + } + } + + @Override + protected Bitmap doInBackground(Void... params) { + Bitmap unusedBitmap = null; + + // If already cancelled before this gets to run in the background, then return early + if (isCancelled()) { + return null; + } + synchronized (mUnusedBitmaps) { + // Check if we can re-use a bitmap + for (Bitmap candidate : mUnusedBitmaps) { + if (candidate != null && candidate.isMutable() && + candidate.getWidth() == mPreviewWidth && + candidate.getHeight() == mPreviewHeight) { + unusedBitmap = candidate; + mUnusedBitmaps.remove(unusedBitmap); + break; } - } catch (IOException e) { - // Ignoring exceptions for non-existing file descriptors. } } - for (int i = 0; i < types.size(); i++) { - Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates", - types.get(i), count[i], duplicates[i])); + + // creating a bitmap is expensive. Do not do this inside synchronized block. + if (unusedBitmap == null) { + unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888); + } + // If cancelled now, don't bother reading the preview from the DB + if (isCancelled()) { + return unusedBitmap; + } + Bitmap preview = readFromDb(mKey, unusedBitmap, this); + // Only consider generating the preview if we have not cancelled the task already + if (!isCancelled() && preview == null) { + // Fetch the version info before we generate the preview, so that, in-case the + // app was updated while we are generating the preview, we use the old version info, + // which would gets re-written next time. + mVersions = getPackageVersion(mKey.componentName.getPackageName()); + + Launcher launcher = (Launcher) mCaller.getContext(); + + // it's not in the db... we need to generate it + preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight); + } + return preview; + } + + @Override + protected void onPostExecute(final Bitmap preview) { + mCaller.applyPreview(preview); + + // Write the generated preview to the DB in the worker thread + if (mVersions != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + if (!isCancelled()) { + // If we are still using this preview, then write it to the DB and then + // let the normal clear mechanism recycle the bitmap + writeToDb(mKey, mVersions, preview); + mBitmapToRecycle = preview; + } else { + // If we've already cancelled, then skip writing the bitmap to the DB + // and manually add the bitmap back to the recycled set + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(preview); + } + } + } + }); + } else { + // If we don't need to write to disk, then ensure the preview gets recycled by + // the normal clear mechanism + mBitmapToRecycle = preview; + } + } + + @Override + protected void onCancelled(final Bitmap preview) { + // If we've cancelled while the task is running, then can return the bitmap to the + // recycled set immediately. Otherwise, it will be recycled after the preview is written + // to disk. + if (preview != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(preview); + } + } + }); } - } catch (Throwable t) { - // Catch everything. This is called from an exception handler that we shouldn't upset. - Log.e(TAG, "Unable to log open files.", t); + } + } + + private static final class WidgetCacheKey extends ComponentKey { + + // TODO: remove dependency on size + @Thunk final String size; + + public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) { + super(componentName, user); + this.size = size; + } + + @Override + public int hashCode() { + return super.hashCode() ^ size.hashCode(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) && ((WidgetCacheKey) o).size.equals(size); } } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 8bd799198..4a6b90afe 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -17,24 +17,18 @@ package com.android.launcher3; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.WallpaperManager; 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.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -46,12 +40,12 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; -import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -69,16 +63,20 @@ import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.Launcher.CustomContentCallbacks; import com.android.launcher3.Launcher.LauncherOverlay; import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.UninstallDropTarget.UninstallSource; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; +import com.android.launcher3.widget.PendingAddShortcutInfo; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** @@ -86,63 +84,45 @@ import java.util.concurrent.atomic.AtomicInteger; * Each page contains a number of icons, folders or widgets the user can * interact with. A workspace is meant to be used with a fixed width only. */ -public class Workspace extends SmoothPagedView +public class Workspace extends PagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener, - Insettable { + Insettable, UninstallSource, AccessibilityDragSource, Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Workspace"; - // Y rotation to apply to the workspace screens - private static final float WORKSPACE_OVERSCROLL_ROTATION = 24f; - - private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0; - private static final int CHILDREN_OUTLINE_FADE_OUT_DURATION = 375; - private static final int CHILDREN_OUTLINE_FADE_IN_DURATION = 100; + private static boolean ENFORCE_DRAG_EVENT_ORDER = false; protected static final int SNAP_OFF_EMPTY_SCREEN_DURATION = 400; protected static final int FADE_EMPTY_SCREEN_DURATION = 150; - private static final int BACKGROUND_FADE_OUT_DURATION = 350; private static final int ADJACENT_SCREEN_DROP_DURATION = 300; - private static final int FLING_THRESHOLD_VELOCITY = 500; - - private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; static final boolean MAP_NO_RECURSE = false; static final boolean MAP_RECURSE = true; - // These animators are used to fade the children's outlines - private ObjectAnimator mChildrenOutlineFadeInAnimation; - private ObjectAnimator mChildrenOutlineFadeOutAnimation; - private float mChildrenOutlineAlpha = 0; - - // These properties refer to the background protection gradient used for AllApps and Customize - private ValueAnimator mBackgroundFadeInAnimation; - private ValueAnimator mBackgroundFadeOutAnimation; - private static final long CUSTOM_CONTENT_GESTURE_DELAY = 200; private long mTouchDownTime = -1; private long mCustomContentShowTime = -1; private LayoutTransition mLayoutTransition; - private final WallpaperManager mWallpaperManager; - private IBinder mWindowToken; + @Thunk final WallpaperManager mWallpaperManager; + @Thunk IBinder mWindowToken; private int mOriginalDefaultPage; private int mDefaultPage; private ShortcutAndWidgetContainer mDragSourceInternal; - private static boolean sAccessibilityEnabled; // The screen id used for the empty screen always present to the right. final static long EXTRA_EMPTY_SCREEN_ID = -201; private final static long CUSTOM_CONTENT_SCREEN_ID = -301; - private HashMap<Long, CellLayout> mWorkspaceScreens = new HashMap<Long, CellLayout>(); - private ArrayList<Long> mScreenOrder = new ArrayList<Long>(); + @Thunk LongArrayMap<CellLayout> mWorkspaceScreens = new LongArrayMap<>(); + @Thunk ArrayList<Long> mScreenOrder = new ArrayList<Long>(); - private Runnable mRemoveEmptyScreenRunnable; - private boolean mDeferRemoveExtraEmptyScreen = false; + @Thunk Runnable mRemoveEmptyScreenRunnable; + @Thunk boolean mDeferRemoveExtraEmptyScreen = false; + @Thunk boolean mAddNewPageOnDrag = true; /** * CellInfo for the cell that is currently being dragged @@ -152,7 +132,7 @@ public class Workspace extends SmoothPagedView /** * Target drop area calculated during last acceptDrop call. */ - private int[] mTargetCell = new int[2]; + @Thunk int[] mTargetCell = new int[2]; private int mDragOverX = -1; private int mDragOverY = -1; @@ -167,7 +147,7 @@ public class Workspace extends SmoothPagedView /** * The CellLayout that is currently being dragged over */ - private CellLayout mDragTargetLayout = null; + @Thunk CellLayout mDragTargetLayout = null; /** * The CellLayout that we will show as glowing */ @@ -178,16 +158,16 @@ public class Workspace extends SmoothPagedView */ private CellLayout mDropToLayout = null; - private Launcher mLauncher; - private IconCache mIconCache; - private DragController mDragController; + @Thunk Launcher mLauncher; + @Thunk IconCache mIconCache; + @Thunk DragController mDragController; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. private int[] mTempCell = new int[2]; private int[] mTempPt = new int[2]; private int[] mTempEstimate = new int[2]; - private float[] mDragViewVisualCenter = new float[2]; + @Thunk float[] mDragViewVisualCenter = new float[2]; private float[] mTempCellLayoutCenterCoordinates = new float[2]; private Matrix mTempInverseMatrix = new Matrix(); @@ -212,34 +192,31 @@ public class Workspace extends SmoothPagedView private boolean mInScrollArea = false; private HolographicOutlineHelper mOutlineHelper; - private Bitmap mDragOutline = null; + @Thunk Bitmap mDragOutline = null; private static final Rect sTempRect = new Rect(); private final int[] mTempXY = new int[2]; private int[] mTempVisiblePagesRange = new int[2]; - private boolean mOverscrollEffectSet; public static final int DRAG_BITMAP_PADDING = 2; private boolean mWorkspaceFadeInAdjacentScreens; WallpaperOffsetInterpolator mWallpaperOffset; - private boolean mWallpaperIsLiveWallpaper; - private int mNumPagesForWallpaperParallax; - private float mLastSetWallpaperOffsetSteps = 0; + @Thunk boolean mWallpaperIsLiveWallpaper; + @Thunk int mNumPagesForWallpaperParallax; + @Thunk float mLastSetWallpaperOffsetSteps = 0; - private Runnable mDelayedResizeRunnable; + @Thunk Runnable mDelayedResizeRunnable; private Runnable mDelayedSnapToPageRunnable; private Point mDisplaySize = new Point(); - private int mCameraDistance; // Variables relating to the creation of user folders by hovering shortcuts over shortcuts private static final int FOLDER_CREATION_TIMEOUT = 0; public static final int REORDER_TIMEOUT = 350; private final Alarm mFolderCreationAlarm = new Alarm(); private final Alarm mReorderAlarm = new Alarm(); - private FolderRingAnimator mDragFolderRingAnimator = null; + @Thunk FolderRingAnimator mDragFolderRingAnimator = null; private FolderIcon mDragOverFolderIcon = null; private boolean mCreateUserFolderOnDrop = false; private boolean mAddToExistingFolderOnDrop = false; - private DropTarget.DragEnforcer mDragEnforcer; private float mMaxDistanceForFolderCreation; private final Canvas mCanvas = new Canvas(); @@ -264,30 +241,16 @@ public class Workspace extends SmoothPagedView private static final int DRAG_MODE_ADD_TO_FOLDER = 2; private static final int DRAG_MODE_REORDER = 3; private int mDragMode = DRAG_MODE_NONE; - private int mLastReorderX = -1; - private int mLastReorderY = -1; + @Thunk int mLastReorderX = -1; + @Thunk int mLastReorderY = -1; private SparseArray<Parcelable> mSavedStates; private final ArrayList<Integer> mRestoredPages = new ArrayList<Integer>(); - // These variables are used for storing the initial and final values during workspace animations - private int mSavedScrollX; - private float mSavedRotationY; - private float mSavedTranslationX; - private float mCurrentScale; - private float mNewScale; - private float[] mOldBackgroundAlphas; - private float[] mOldAlphas; - private float[] mNewBackgroundAlphas; - private float[] mNewAlphas; - private int mLastChildCount = -1; private float mTransitionProgress; - private Animator mStateAnimator = null; - - float mOverScrollEffect = 0f; - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; @@ -298,6 +261,11 @@ public class Workspace extends SmoothPagedView boolean mShouldSendPageSettled; int mLastOverlaySroll = 0; + // Handles workspace state transitions + private WorkspaceStateTransitionAnimation mStateTransitionAnimation; + + private AccessibilityDelegate mPagesAccessibilityDelegate; + private final Runnable mBindPages = new Runnable() { @Override public void run() { @@ -324,29 +292,22 @@ public class Workspace extends SmoothPagedView */ public Workspace(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mContentIsRefreshable = false; mOutlineHelper = HolographicOutlineHelper.obtain(context); - mDragEnforcer = new DropTarget.DragEnforcer(context); - // With workspace, data is available straight from the get-go - setDataIsReady(); - mLauncher = (Launcher) context; + mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this); final Resources res = getResources(); - mWorkspaceFadeInAdjacentScreens = LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().shouldFadeAdjacentWorkspaceScreens(); + DeviceProfile grid = mLauncher.getDeviceProfile(); + mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); mFadeInAdjacentScreens = false; mWallpaperManager = WallpaperManager.getInstance(context); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0); mSpringLoadedShrinkFactor = res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; - mOverviewModeShrinkFactor = grid.getOverviewModeScale(); - mCameraDistance = res.getInteger(R.integer.config_cameraDistance); + mOverviewModeShrinkFactor = grid.getOverviewModeScale(mIsRtl); mOriginalDefaultPage = mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); a.recycle(); @@ -357,7 +318,6 @@ public class Workspace extends SmoothPagedView // Disable multitouch across the workspace/all apps/customize tray setMotionEventSplittingEnabled(true); - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } @Override @@ -375,13 +335,12 @@ public class Workspace extends SmoothPagedView // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each // dimension if unsuccessful - public int[] estimateItemSize(int hSpan, int vSpan, - ItemInfo itemInfo, boolean springLoaded) { + public int[] estimateItemSize(ItemInfo itemInfo, boolean springLoaded) { int[] size = new int[2]; if (getChildCount() > 0) { // Use the first non-custom page to estimate the child position CellLayout cl = (CellLayout) getChildAt(numCustomPages()); - Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, itemInfo.spanX, itemInfo.spanY); size[0] = r.width(); size[1] = r.height(); if (springLoaded) { @@ -403,32 +362,39 @@ public class Workspace extends SmoothPagedView return r; } + @Override public void onDragStart(final DragSource source, Object info, int dragAction) { + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragStart", 0, 0); + } + mIsDragOccuring = true; updateChildrenLayersEnabled(false); mLauncher.lockScreenOrientation(); mLauncher.onInteractionBegin(); - setChildrenBackgroundAlphaMultipliers(1f); // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging InstallShortcutReceiver.enableInstallQueue(); - UninstallShortcutReceiver.enableUninstallQueue(); - post(new Runnable() { - @Override - public void run() { - if (mIsDragOccuring) { - mDeferRemoveExtraEmptyScreen = false; - addExtraEmptyScreenOnDrag(); - } - } - }); + + if (mAddNewPageOnDrag) { + mDeferRemoveExtraEmptyScreen = false; + addExtraEmptyScreenOnDrag(); + } } + public void setAddNewPageOnDrag(boolean addPage) { + mAddNewPageOnDrag = addPage; + } public void deferRemoveExtraEmptyScreen() { mDeferRemoveExtraEmptyScreen = true; } + @Override public void onDragEnd() { + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragEnd", 0, 0); + } + if (!mDeferRemoveExtraEmptyScreen) { removeExtraEmptyScreen(true, mDragSourceInternal != null); } @@ -439,7 +405,6 @@ public class Workspace extends SmoothPagedView // Re-enable any Un/InstallShortcutReceiver and now process any queued items InstallShortcutReceiver.disableAndFlushInstallQueue(getContext()); - UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext()); mDragSourceInternal = null; mLauncher.onInteractionEnd(); @@ -450,9 +415,8 @@ public class Workspace extends SmoothPagedView */ protected void initWorkspace() { mCurrentPage = mDefaultPage; - Launcher.setScreen(mCurrentPage); LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); mIconCache = app.getIconCache(); setWillNotDraw(false); setClipChildren(false); @@ -467,10 +431,11 @@ public class Workspace extends SmoothPagedView display.getSize(mDisplaySize); mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx); - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); // Set the wallpaper dimensions when Launcher starts up setWallpaperDimension(); + + setEdgeGlowColor(getResources().getColor(R.color.workspace_edge_effect_color)); } private void setupLayoutTransition() { @@ -491,11 +456,6 @@ public class Workspace extends SmoothPagedView } @Override - protected int getScrollMode() { - return SmoothPagedView.X_LARGE_MODE; - } - - @Override public void onChildViewAdded(View parent, View child) { if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); @@ -503,7 +463,7 @@ public class Workspace extends SmoothPagedView CellLayout cl = ((CellLayout) child); cl.setOnInterceptTouchListener(this); cl.setClickable(true); - cl.setImportantForAccessibility(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); + cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); super.onChildViewAdded(parent, child); } @@ -518,7 +478,7 @@ public class Workspace extends SmoothPagedView /** * @return The open folder on the current screen, or null if there is none */ - Folder getOpenFolder() { + public Folder getOpenFolder() { DragLayer dragLayer = mLauncher.getDragLayer(); int count = dragLayer.getChildCount(); for (int i = 0; i < count; i++) { @@ -572,16 +532,14 @@ public class Workspace extends SmoothPagedView } public long insertNewWorkspaceScreen(long screenId, int insertIndex) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId + - " at index: " + insertIndex, true); - if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); } - CellLayout newScreen = (CellLayout) - mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); + // Inflate the cell layout, but do not add it automatically so that we can get the newly + // created CellLayout. + CellLayout newScreen = (CellLayout) mLauncher.getLayoutInflater().inflate( + R.layout.workspace_screen, this, false /* attachToRoot */); newScreen.setOnLongClickListener(mLongClickListener); newScreen.setOnClickListener(mLauncher); @@ -589,13 +547,18 @@ public class Workspace extends SmoothPagedView mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); + + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate != null && delegate.isInAccessibleDrag()) { + newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); + } return screenId; } public void createCustomContentContainer() { CellLayout customScreen = (CellLayout) - mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); - customScreen.disableBackground(); + mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, this, false); customScreen.disableDragTarget(); mWorkspaceScreens.put(CUSTOM_CONTENT_SCREEN_ID, customScreen); @@ -675,9 +638,6 @@ public class Workspace extends SmoothPagedView } public void addExtraEmptyScreenOnDrag() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreenOnDrag()", true); - boolean lastChildOnScreen = false; boolean childOnFinalScreen = false; @@ -704,9 +664,6 @@ public class Workspace extends SmoothPagedView } public boolean addExtraEmptyScreen() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreen()", true); - if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID); return true; @@ -715,9 +672,6 @@ public class Workspace extends SmoothPagedView } private void convertFinalScreenToEmptyScreenIfNecessary() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - convertFinalScreenToEmptyScreenIfNecessary()", true); - if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -742,7 +696,6 @@ public class Workspace extends SmoothPagedView // Update the model if we have changed any screens mLauncher.getModel().updateWorkspaceScreenOrder(mLauncher, mScreenOrder); - Launcher.addDumpLog(TAG, "11683562 - extra empty screen: " + finalScreenId, true); } } @@ -752,8 +705,6 @@ public class Workspace extends SmoothPagedView public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete, final int delay, final boolean stripEmptyScreens) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - removeExtraEmptyScreen()", true); if (mLauncher.isWorkspaceLoading()) { // Don't strip empty screens if the workspace is still loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -778,6 +729,7 @@ public class Workspace extends SmoothPagedView fadeAndRemoveEmptyScreen(SNAP_OFF_EMPTY_SCREEN_DURATION, FADE_EMPTY_SCREEN_DURATION, onComplete, stripEmptyScreens); } else { + snapToPage(getNextPage(), 0); fadeAndRemoveEmptyScreen(0, FADE_EMPTY_SCREEN_DURATION, onComplete, stripEmptyScreens); } @@ -795,9 +747,7 @@ public class Workspace extends SmoothPagedView private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete, final boolean stripEmptyScreens) { - // Log to disk // XXX: Do we need to update LM workspace screens below? - Launcher.addDumpLog(TAG, "11683562 - fadeAndRemoveEmptyScreen()", true); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f); @@ -841,8 +791,6 @@ public class Workspace extends SmoothPagedView } public long commitExtraEmptyScreen() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - commitExtraEmptyScreen()", true); if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -875,12 +823,9 @@ public class Workspace extends SmoothPagedView } public long getIdForScreen(CellLayout layout) { - Iterator<Long> iter = mWorkspaceScreens.keySet().iterator(); - while (iter.hasNext()) { - long id = iter.next(); - if (mWorkspaceScreens.get(id) == layout) { - return id; - } + int index = mWorkspaceScreens.indexOfValue(layout); + if (index != -1) { + return mWorkspaceScreens.keyAt(index); } return -1; } @@ -896,14 +841,11 @@ public class Workspace extends SmoothPagedView return -1; } - ArrayList<Long> getScreenOrder() { + public ArrayList<Long> getScreenOrder() { return mScreenOrder; } public void stripEmptyScreens() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - stripEmptyScreens()", true); - if (mLauncher.isWorkspaceLoading()) { // Don't strip empty screens if the workspace is still loading. // This is dangerous and can result in data loss. @@ -918,8 +860,10 @@ public class Workspace extends SmoothPagedView int currentPage = getNextPage(); ArrayList<Long> removeScreens = new ArrayList<Long>(); - for (Long id: mWorkspaceScreens.keySet()) { - CellLayout cl = mWorkspaceScreens.get(id); + int total = mWorkspaceScreens.size(); + for (int i = 0; i < total; i++) { + long id = mWorkspaceScreens.keyAt(i); + CellLayout cl = mWorkspaceScreens.valueAt(i); if (id >= 0 && cl.getShortcutsAndWidgets().getChildCount() == 0) { removeScreens.add(id); } @@ -931,7 +875,6 @@ public class Workspace extends SmoothPagedView int pageShift = 0; for (Long id: removeScreens) { - Launcher.addDumpLog(TAG, "11683562 - removing id: " + id, true); CellLayout cl = mWorkspaceScreens.get(id); mWorkspaceScreens.remove(id); mScreenOrder.remove(id); @@ -1075,6 +1018,7 @@ public class Workspace extends SmoothPagedView * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout * that it should intercept touch events, which is not something that is normally supported. */ + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { return (workspaceInModalState() || !isFinishedSwitchingState()) @@ -1116,7 +1060,7 @@ public class Workspace extends SmoothPagedView case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_REST) { final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); - if (currentPage != null && !currentPage.lastDownOnOccupiedCell()) { + if (currentPage != null) { onWallpaperTap(ev); } } @@ -1178,7 +1122,7 @@ public class Workspace extends SmoothPagedView boolean passRightSwipesToCustomContent = (mTouchDownTime - mCustomContentShowTime) > CUSTOM_CONTENT_GESTURE_DELAY; - boolean swipeInIgnoreDirection = isLayoutRtl() ? deltaX < 0 : deltaX > 0; + boolean swipeInIgnoreDirection = mIsRtl ? deltaX < 0 : deltaX > 0; boolean onCustomContentScreen = getScreenIdForPageIndex(getCurrentPage()) == CUSTOM_CONTENT_SCREEN_ID; if (swipeInIgnoreDirection && onCustomContentScreen && passRightSwipesToCustomContent) { @@ -1285,15 +1229,14 @@ public class Workspace extends SmoothPagedView @Override protected void overScroll(float amount) { - boolean isRtl = isLayoutRtl(); - boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || isRtl)) || - (amount >= 0 && (!hasCustomContent() || !isRtl)); + boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || mIsRtl)) || + (amount >= 0 && (!hasCustomContent() || !mIsRtl)); boolean shouldScrollOverlay = mLauncherOverlay != null && - ((amount <= 0 && !isRtl) || (amount >= 0 && isRtl)); + ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl)); boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlaySroll != 0 && - ((amount >= 0 && !isRtl) || (amount <= 0 && isRtl)); + ((amount >= 0 && !mIsRtl) || (amount <= 0 && mIsRtl)); if (shouldScrollOverlay) { if (!mStartedSendingScrollEvents && mScrollInteractionBegan) { @@ -1307,23 +1250,26 @@ public class Workspace extends SmoothPagedView int progress = (int) Math.abs((f * 100)); mLastOverlaySroll = progress; - mLauncherOverlay.onScrollChange(progress, isRtl); + mLauncherOverlay.onScrollChange(progress, mIsRtl); } else if (shouldOverScroll) { dampedOverScroll(amount); - mOverScrollEffect = acceleratedOverFactor(amount); - } else { - mOverScrollEffect = 0; } if (shouldZeroOverlay) { - mLauncherOverlay.onScrollChange(0, isRtl); + mLauncherOverlay.onScrollChange(0, mIsRtl); } } @Override + protected void getEdgeVerticalPostion(int[] pos) { + View child = getChildAt(getPageCount() - 1); + pos[0] = child.getTop(); + pos[1] = child.getBottom(); + } + + @Override protected void notifyPageSwitchListener() { super.notifyPageSwitchListener(); - Launcher.setScreen(getNextPage()); if (hasCustomContent() && getNextPage() == 0 && !mCustomContentShowing) { mCustomContentShowing = true; @@ -1346,10 +1292,10 @@ public class Workspace extends SmoothPagedView protected void setWallpaperDimension() { new AsyncTask<Void, Void, Void>() { public Void doInBackground(Void ... args) { - String spKey = WallpaperCropActivity.getSharedPreferencesKey(); + String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); - LauncherWallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(), + WallpaperUtils.suggestWallpaperDimension(mLauncher.getResources(), sp, mLauncher.getWindowManager(), mWallpaperManager, mLauncher.overrideWallpaperDimensions()); return null; @@ -1439,7 +1385,22 @@ public class Workspace extends SmoothPagedView } private float wallpaperOffsetForCurrentScroll() { + // TODO: do different behavior if it's a live wallpaper? + // Don't use up all the wallpaper parallax until you have at least + // MIN_PARALLAX_PAGE_SPAN pages + int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); + int parallaxPageSpan; + if (mWallpaperIsLiveWallpaper) { + parallaxPageSpan = numScrollingPages - 1; + } else { + parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); + } + mNumPagesForWallpaperParallax = parallaxPageSpan; + if (getChildCount() <= 1) { + if (mIsRtl) { + return 1 - 1.0f/mNumPagesForWallpaperParallax; + } return 0; } @@ -1448,7 +1409,7 @@ public class Workspace extends SmoothPagedView int firstIndex = numCustomPages(); // Exclude the last extra empty screen (if we have > MIN_PARALLAX_PAGE_SPAN pages) int lastIndex = getChildCount() - 1 - emptyExtraPages; - if (isLayoutRtl()) { + if (mIsRtl) { int temp = firstIndex; firstIndex = lastIndex; lastIndex = temp; @@ -1459,28 +1420,20 @@ public class Workspace extends SmoothPagedView if (scrollRange == 0) { return 0; } else { - // TODO: do different behavior if it's a live wallpaper? // Sometimes the left parameter of the pages is animated during a layout transition; // this parameter offsets it to keep the wallpaper from animating as well int adjustedScroll = getScrollX() - firstPageScrollX - getLayoutTransitionOffsetForPage(0); float offset = Math.min(1, adjustedScroll / (float) scrollRange); offset = Math.max(0, offset); - // Don't use up all the wallpaper parallax until you have at least - // MIN_PARALLAX_PAGE_SPAN pages - int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); - int parallaxPageSpan; - if (mWallpaperIsLiveWallpaper) { - parallaxPageSpan = numScrollingPages - 1; - } else { - parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); - } - mNumPagesForWallpaperParallax = parallaxPageSpan; // On RTL devices, push the wallpaper offset to the right if we don't have enough // pages (ie if numScrollingPages < MIN_PARALLAX_PAGE_SPAN) - int padding = isLayoutRtl() ? parallaxPageSpan - numScrollingPages + 1 : 0; - return offset * (padding + numScrollingPages - 1) / parallaxPageSpan; + if (!mWallpaperIsLiveWallpaper && numScrollingPages < MIN_PARALLAX_PAGE_SPAN + && mIsRtl) { + return offset * (parallaxPageSpan - numScrollingPages + 1) / parallaxPageSpan; + } + return offset * (numScrollingPages - 1) / parallaxPageSpan; } } @@ -1560,81 +1513,17 @@ public class Workspace extends SmoothPagedView @Override public void announceForAccessibility(CharSequence text) { // Don't announce if apps is on top of us. - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { super.announceForAccessibility(text); } } - void showOutlines() { - if (!workspaceInModalState() && !mIsSwitchingState) { - if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); - if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); - mChildrenOutlineFadeInAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 1.0f); - mChildrenOutlineFadeInAnimation.setDuration(CHILDREN_OUTLINE_FADE_IN_DURATION); - mChildrenOutlineFadeInAnimation.start(); - } - } - - void hideOutlines() { - if (!workspaceInModalState() && !mIsSwitchingState) { - if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); - if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); - mChildrenOutlineFadeOutAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 0.0f); - mChildrenOutlineFadeOutAnimation.setDuration(CHILDREN_OUTLINE_FADE_OUT_DURATION); - mChildrenOutlineFadeOutAnimation.setStartDelay(CHILDREN_OUTLINE_FADE_OUT_DELAY); - mChildrenOutlineFadeOutAnimation.start(); - } - } - public void showOutlinesTemporarily() { if (!mIsPageMoving && !isTouchActive()) { snapToPage(mCurrentPage); } } - public void setChildrenOutlineAlpha(float alpha) { - mChildrenOutlineAlpha = alpha; - for (int i = 0; i < getChildCount(); i++) { - CellLayout cl = (CellLayout) getChildAt(i); - cl.setBackgroundAlpha(alpha); - } - } - - public float getChildrenOutlineAlpha() { - return mChildrenOutlineAlpha; - } - - private void animateBackgroundGradient(float finalAlpha, boolean animated) { - final DragLayer dragLayer = mLauncher.getDragLayer(); - - if (mBackgroundFadeInAnimation != null) { - mBackgroundFadeInAnimation.cancel(); - mBackgroundFadeInAnimation = null; - } - if (mBackgroundFadeOutAnimation != null) { - mBackgroundFadeOutAnimation.cancel(); - mBackgroundFadeOutAnimation = null; - } - float startAlpha = dragLayer.getBackgroundAlpha(); - if (finalAlpha != startAlpha) { - if (animated) { - mBackgroundFadeOutAnimation = - LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha); - mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - dragLayer.setBackgroundAlpha( - ((Float)animation.getAnimatedValue()).floatValue()); - } - }); - mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); - mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); - mBackgroundFadeOutAnimation.start(); - } else { - dragLayer.setBackgroundAlpha(finalAlpha); - } - } - } - float backgroundAlphaInterpolator(float r) { float pivotA = 0.1f; float pivotB = 0.4f; @@ -1648,28 +1537,38 @@ public class Workspace extends SmoothPagedView } private void updatePageAlphaValues(int screenCenter) { - boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; if (mWorkspaceFadeInAdjacentScreens && !workspaceInModalState() && - !mIsSwitchingState && - !isInOverscroll) { + !mIsSwitchingState) { for (int i = numCustomPages(); i < getChildCount(); i++) { CellLayout child = (CellLayout) getChildAt(i); if (child != null) { float scrollProgress = getScrollProgress(screenCenter, child, i); float alpha = 1 - Math.abs(scrollProgress); child.getShortcutsAndWidgets().setAlpha(alpha); - //child.setBackgroundAlphaMultiplier(1 - alpha); } } } } - private void setChildrenBackgroundAlphaMultipliers(float a) { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void enableAccessibleDrag(boolean enable) { for (int i = 0; i < getChildCount(); i++) { CellLayout child = (CellLayout) getChildAt(i); - child.setBackgroundAlphaMultiplier(a); + child.enableAccessibleDrag(enable, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); + } + + if (enable) { + // We need to allow our individual children to become click handlers in this case + setOnClickListener(null); + } else { + // Reset our click listener + setOnClickListener(mLauncher); } + mLauncher.getSearchBar().enableAccessibleDrag(enable); + mLauncher.getHotseat().getLayout() + .enableAccessibleDrag(enable, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); } public boolean hasCustomContent() { @@ -1696,7 +1595,7 @@ public class Workspace extends SmoothPagedView translationX = scrollRange - scrollDelta; progress = (scrollRange - scrollDelta) / scrollRange; - if (isLayoutRtl()) { + if (mIsRtl) { translationX = Math.min(0, translationX); } else { translationX = Math.max(0, translationX); @@ -1713,7 +1612,11 @@ public class Workspace extends SmoothPagedView mLastCustomContentScrollProgress = progress; - mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f); + // We should only update the drag layer background alpha if we are not in all apps or the + // widgets tray + if (mState == State.NORMAL) { + mLauncher.getDragLayer().setBackgroundAlpha(progress == 1 ? 0 : progress * 0.8f); + } if (mLauncher.getHotseat() != null) { mLauncher.getHotseat().setTranslationX(translationX); @@ -1738,7 +1641,7 @@ public class Workspace extends SmoothPagedView OnClickListener listener = new OnClickListener() { @Override public void onClick(View arg0) { - enterOverviewMode(); + mLauncher.showOverviewMode(true); } }; return listener; @@ -1746,35 +1649,9 @@ public class Workspace extends SmoothPagedView @Override protected void screenScrolled(int screenCenter) { - final boolean isRtl = isLayoutRtl(); - super.screenScrolled(screenCenter); - updatePageAlphaValues(screenCenter); updateStateForCustomContent(screenCenter); enableHwLayersOnVisiblePages(); - - boolean shouldOverScroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; - - if (shouldOverScroll) { - int index = 0; - final int lowerIndex = 0; - final int upperIndex = getChildCount() - 1; - - final boolean isLeftPage = mOverScrollX < 0; - index = (!isRtl && isLeftPage) || (isRtl && !isLeftPage) ? lowerIndex : upperIndex; - - CellLayout cl = (CellLayout) getChildAt(index); - float effect = Math.abs(mOverScrollEffect); - cl.setOverScrollAmount(Math.abs(effect), isLeftPage); - - mOverscrollEffectSet = true; - } else { - if (mOverscrollEffectSet && getChildCount() > 0) { - mOverscrollEffectSet = false; - ((CellLayout) getChildAt(0)).setOverScrollAmount(0, false); - ((CellLayout) getChildAt(getChildCount() - 1)).setOverScrollAmount(0, false); - } - } } protected void onAttachedToWindow() { @@ -1798,9 +1675,6 @@ public class Workspace extends SmoothPagedView getPageIndicator().setOnClickListener(listener); } } - AccessibilityManager am = (AccessibilityManager) - getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - sAccessibilityEnabled = am.isEnabled(); // Update wallpaper dimensions if they were changed since last onResume // (we also always set the wallpaper dimensions in the constructor) @@ -1832,7 +1706,7 @@ public class Workspace extends SmoothPagedView @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { return openFolder.requestFocus(direction, previouslyFocusedRect); @@ -1853,7 +1727,7 @@ public class Workspace extends SmoothPagedView @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { openFolder.addFocusables(views, direction); @@ -1898,7 +1772,7 @@ public class Workspace extends SmoothPagedView } } - private void updateChildrenLayersEnabled(boolean force) { + @Thunk void updateChildrenLayersEnabled(boolean force) { boolean small = mState == State.OVERVIEW || mIsSwitchingState; boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); @@ -1971,64 +1845,6 @@ public class Workspace extends SmoothPagedView } /* - * This interpolator emulates the rate at which the perceived scale of an object changes - * as its distance from a camera increases. When this interpolator is applied to a scale - * animation on a view, it evokes the sense that the object is shrinking due to moving away - * from the camera. - */ - static class ZInterpolator implements TimeInterpolator { - private float focalLength; - - public ZInterpolator(float foc) { - focalLength = foc; - } - - public float getInterpolation(float input) { - return (1.0f - focalLength / (focalLength + input)) / - (1.0f - focalLength / (focalLength + 1.0f)); - } - } - - /* - * The exact reverse of ZInterpolator. - */ - static class InverseZInterpolator implements TimeInterpolator { - private ZInterpolator zInterpolator; - public InverseZInterpolator(float foc) { - zInterpolator = new ZInterpolator(foc); - } - public float getInterpolation(float input) { - return 1 - zInterpolator.getInterpolation(1 - input); - } - } - - /* - * ZInterpolator compounded with an ease-out. - */ - static class ZoomOutInterpolator implements TimeInterpolator { - private final DecelerateInterpolator decelerate = new DecelerateInterpolator(0.75f); - private final ZInterpolator zInterpolator = new ZInterpolator(0.13f); - - public float getInterpolation(float input) { - return decelerate.getInterpolation(zInterpolator.getInterpolation(input)); - } - } - - /* - * InvereZInterpolator compounded with an ease-out. - */ - static class ZoomInInterpolator implements TimeInterpolator { - private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); - private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); - - public float getInterpolation(float input) { - return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); - } - } - - private final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); - - /* * * We call these methods (onDragStartedWithItemSpans/onDragStartedWithSize) whenever we * start a drag in Launcher, regardless of whether the drag has ever entered the Workspace @@ -2054,16 +1870,14 @@ public class Workspace extends SmoothPagedView public void onExternalDragStartedWithItem(View v) { // Compose a drag bitmap with the view scaled to the icon size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); int iconSize = grid.iconSizePx; int bmpWidth = v.getMeasuredWidth(); int bmpHeight = v.getMeasuredHeight(); // If this is a text view, use its drawable instead if (v instanceof TextView) { - TextView tv = (TextView) v; - Drawable d = tv.getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); bmpWidth = bounds.width(); bmpHeight = bounds.height(); @@ -2081,7 +1895,7 @@ public class Workspace extends SmoothPagedView } public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) { - int[] size = estimateItemSize(info.spanX, info.spanY, info, false); + int[] size = estimateItemSize(info, false); // The outline is used to visualize where the item will land if dropped mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha); @@ -2092,21 +1906,6 @@ public class Workspace extends SmoothPagedView dragLayer.clearAllResizeFrames(); } - private void initAnimationArrays() { - final int childCount = getChildCount(); - if (mLastChildCount == childCount) return; - - mOldBackgroundAlphas = new float[childCount]; - mOldAlphas = new float[childCount]; - mNewBackgroundAlphas = new float[childCount]; - mNewAlphas = new float[childCount]; - } - - Animator getChangeStateAnimation(final State state, boolean animated, - ArrayList<View> layerViews) { - return getChangeStateAnimation(state, animated, 0, -1, layerViews); - } - @Override protected void getFreeScrollPageRange(int[] range) { getOverviewModePages(range); @@ -2120,14 +1919,13 @@ public class Workspace extends SmoothPagedView range[1] = Math.max(0, end); } - protected void onStartReordering() { + public void onStartReordering() { super.onStartReordering(); - showOutlines(); // Reordering handles its own animations, disable the automatic ones. disableLayoutTransitions(); } - protected void onEndReordering() { + public void onEndReordering() { super.onEndReordering(); if (mLauncher.isWorkspaceLoading()) { @@ -2135,7 +1933,6 @@ public class Workspace extends SmoothPagedView return; } - hideOutlines(); mScreenOrder.clear(); int count = getChildCount(); for (int i = 0; i < count; i++) { @@ -2153,44 +1950,8 @@ public class Workspace extends SmoothPagedView return mState == State.OVERVIEW; } - public boolean enterOverviewMode() { - if (mTouchState != TOUCH_STATE_REST) { - return false; - } - enableOverviewMode(true, -1, true); - return true; - } - - public void exitOverviewMode(boolean animated) { - exitOverviewMode(-1, animated); - } - - public void exitOverviewMode(int snapPage, boolean animated) { - enableOverviewMode(false, snapPage, animated); - } - - private void enableOverviewMode(boolean enable, int snapPage, boolean animated) { - State finalState = Workspace.State.OVERVIEW; - if (!enable) { - finalState = Workspace.State.NORMAL; - } - - Animator workspaceAnim = getChangeStateAnimation(finalState, animated, 0, snapPage); - if (workspaceAnim != null) { - onTransitionPrepare(); - workspaceAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator arg0) { - onTransitionEnd(); - } - }); - workspaceAnim.start(); - } - } - int getOverviewModeTranslationY() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); Rect overviewBar = grid.getOverviewModeButtonBarRect(); int availableHeight = getViewportHeight(); @@ -2202,332 +1963,75 @@ public class Workspace extends SmoothPagedView return -offsetFromTopEdge + mInsets.top + offsetToCenterInOverview; } - public void updateInteractionForState() { - if (mState != State.NORMAL) { - mLauncher.onInteractionBegin(); - } else { - mLauncher.onInteractionEnd(); - } - } - - private void setState(State state) { - mState = state; - updateInteractionForState(); + /** + * Sets the current workspace {@link State}, returning an animation transitioning the workspace + * to that new state. + */ + public Animator setStateWithAnimation(State toState, int toPage, boolean animated, + boolean hasOverlaySearchBar, HashMap<View, Integer> layerViews) { + // Create the animation to the new state + Animator workspaceAnim = mStateTransitionAnimation.getAnimationToState(mState, + toState, toPage, animated, hasOverlaySearchBar, layerViews); + + // Update the current state + mState = toState; updateAccessibilityFlags(); + + return workspaceAnim; } State getState() { return mState; } - private void updateAccessibilityFlags() { - int accessible = mState == State.NORMAL ? - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES : - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; - setImportantForAccessibility(accessible); - } - - private static final int HIDE_WORKSPACE_DURATION = 100; - - Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage) { - return getChangeStateAnimation(state, animated, delay, snapPage, null); - } - - Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage, - ArrayList<View> layerViews) { - if (mState == state) { - return null; - } - - // Initialize animation arrays for the first time if necessary - initAnimationArrays(); - - AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null; - - // We only want a single instance of a workspace animation to be running at once, so - // we cancel any incomplete transition. - if (mStateAnimator != null) { - mStateAnimator.cancel(); - } - mStateAnimator = anim; - - final State oldState = mState; - final boolean oldStateIsNormal = (oldState == State.NORMAL); - final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); - final boolean oldStateIsNormalHidden = (oldState == State.NORMAL_HIDDEN); - final boolean oldStateIsOverviewHidden = (oldState == State.OVERVIEW_HIDDEN); - final boolean oldStateIsOverview = (oldState == State.OVERVIEW); - setState(state); - final boolean stateIsNormal = (state == State.NORMAL); - final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED); - final boolean stateIsNormalHidden = (state == State.NORMAL_HIDDEN); - final boolean stateIsOverviewHidden = (state == State.OVERVIEW_HIDDEN); - final boolean stateIsOverview = (state == State.OVERVIEW); - float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f; - float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f; - float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f; - float finalSearchBarAlpha = !stateIsNormal ? 0f : 1f; - float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ? - getOverviewModeTranslationY() : 0; - - boolean workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); - boolean overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); - boolean allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); - boolean workspaceToOverview = (oldStateIsNormal && stateIsOverview); - boolean overviewToWorkspace = (oldStateIsOverview && stateIsNormal); - - mNewScale = 1.0f; - - if (oldStateIsOverview) { - disableFreeScroll(); - } else if (stateIsOverview) { - enableFreeScroll(); - } - - if (state != State.NORMAL) { - if (stateIsSpringLoaded) { - mNewScale = mSpringLoadedShrinkFactor; - } else if (stateIsOverview || stateIsOverviewHidden) { - mNewScale = mOverviewModeShrinkFactor; - } - } - - final int duration; - if (workspaceToAllApps || overviewToAllApps) { - duration = HIDE_WORKSPACE_DURATION; //getResources().getInteger(R.integer.config_workspaceUnshrinkTime); - } else if (workspaceToOverview || overviewToWorkspace) { - duration = getResources().getInteger(R.integer.config_overviewTransitionTime); - } else { - duration = getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); - } - - if (snapPage == -1) { - snapPage = getPageNearestToCenterOfScreen(); - } - snapToPage(snapPage, duration, mZoomInInterpolator); - - for (int i = 0; i < getChildCount(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - boolean isCurrentPage = (i == snapPage); - float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); - float finalAlpha; - if (stateIsNormalHidden || stateIsOverviewHidden) { - finalAlpha = 0f; - } else if (stateIsNormal && mWorkspaceFadeInAdjacentScreens) { - finalAlpha = (i == snapPage || i < numCustomPages()) ? 1f : 0f; - } else { - finalAlpha = 1f; - } - - // If we are animating to/from the small state, then hide the side pages and fade the - // current page in - if (!mIsSwitchingState) { - if (workspaceToAllApps || allAppsToWorkspace) { - if (allAppsToWorkspace && isCurrentPage) { - initialAlpha = 0f; - } else if (!isCurrentPage) { - initialAlpha = finalAlpha = 0f; - } - cl.setShortcutAndWidgetAlpha(initialAlpha); - } + public void updateAccessibilityFlags() { + if (Utilities.isLmpOrAbove()) { + int total = getPageCount(); + for (int i = numCustomPages(); i < total; i++) { + updateAccessibilityFlags((CellLayout) getPageAt(i), i); } - - mOldAlphas[i] = initialAlpha; - mNewAlphas[i] = finalAlpha; - if (animated) { - mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); - mNewBackgroundAlphas[i] = finalBackgroundAlpha; - } else { - cl.setBackgroundAlpha(finalBackgroundAlpha); - cl.setShortcutAndWidgetAlpha(finalAlpha); - } - } - - final View searchBar = mLauncher.getQsbBar(); - final View overviewPanel = mLauncher.getOverviewPanel(); - final View hotseat = mLauncher.getHotseat(); - final View pageIndicator = getPageIndicator(); - if (animated) { - LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(this); - scale.scaleX(mNewScale) - .scaleY(mNewScale) - .translationY(finalWorkspaceTranslationY) - .setDuration(duration) - .setInterpolator(mZoomInInterpolator); - anim.play(scale); - for (int index = 0; index < getChildCount(); index++) { - final int i = index; - final CellLayout cl = (CellLayout) getChildAt(i); - float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); - if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { - cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); - cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); - } else { - if (layerViews != null) { - layerViews.add(cl); - } - if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { - LauncherViewPropertyAnimator alphaAnim = - new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); - alphaAnim.alpha(mNewAlphas[i]) - .setDuration(duration) - .setInterpolator(mZoomInInterpolator); - anim.play(alphaAnim); - } - if (mOldBackgroundAlphas[i] != 0 || - mNewBackgroundAlphas[i] != 0) { - ValueAnimator bgAnim = - LauncherAnimUtils.ofFloat(cl, 0f, 1f); - bgAnim.setInterpolator(mZoomInInterpolator); - bgAnim.setDuration(duration); - bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - cl.setBackgroundAlpha( - a * mOldBackgroundAlphas[i] + - b * mNewBackgroundAlphas[i]); - } - }); - anim.play(bgAnim); - } - } - } - Animator pageIndicatorAlpha = null; - if (pageIndicator != null) { - pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator) - .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); - pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator)); - } else { - // create a dummy animation so we don't need to do null checks later - pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0); - } - - Animator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat) - .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); - hotseatAlpha.addListener(new AlphaUpdateListener(hotseat)); - - Animator overviewPanelAlpha = new LauncherViewPropertyAnimator(overviewPanel) - .alpha(finalOverviewPanelAlpha).withLayer(); - overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel)); - - // For animation optimations, we may need to provide the Launcher transition - // with a set of views on which to force build layers in certain scenarios. - hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); - overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (layerViews != null) { - layerViews.add(hotseat); - layerViews.add(overviewPanel); - } - - if (workspaceToOverview) { - pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); - hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); - overviewPanelAlpha.setInterpolator(null); - } else if (overviewToWorkspace) { - pageIndicatorAlpha.setInterpolator(null); - hotseatAlpha.setInterpolator(null); - overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); - } - - overviewPanelAlpha.setDuration(duration); - pageIndicatorAlpha.setDuration(duration); - hotseatAlpha.setDuration(duration); - - if (searchBar != null) { - Animator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar) - .alpha(finalSearchBarAlpha).withLayer(); - searchBarAlpha.addListener(new AlphaUpdateListener(searchBar)); - searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (layerViews != null) { - layerViews.add(searchBar); - } - searchBarAlpha.setDuration(duration); - anim.play(searchBarAlpha); - } - - anim.play(overviewPanelAlpha); - anim.play(hotseatAlpha); - anim.play(pageIndicatorAlpha); - anim.setStartDelay(delay); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mStateAnimator = null; - } - }); + setImportantForAccessibility((mState == State.NORMAL || mState == State.OVERVIEW) + ? IMPORTANT_FOR_ACCESSIBILITY_AUTO + : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } else { - overviewPanel.setAlpha(finalOverviewPanelAlpha); - AlphaUpdateListener.updateVisibility(overviewPanel); - hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha); - AlphaUpdateListener.updateVisibility(hotseat); - if (pageIndicator != null) { - pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha); - AlphaUpdateListener.updateVisibility(pageIndicator); - } - if (searchBar != null) { - searchBar.setAlpha(finalSearchBarAlpha); - AlphaUpdateListener.updateVisibility(searchBar); - } - updateCustomContentVisibility(); - setScaleX(mNewScale); - setScaleY(mNewScale); - setTranslationY(finalWorkspaceTranslationY); - } - - if (stateIsNormal) { - animateBackgroundGradient(0f, animated); - } else { - animateBackgroundGradient(getResources().getInteger( - R.integer.config_workspaceScrimAlpha) / 100f, animated); + int accessible = mState == State.NORMAL ? + IMPORTANT_FOR_ACCESSIBILITY_AUTO : + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; + setImportantForAccessibility(accessible); } - return anim; } - static class AlphaUpdateListener implements AnimatorUpdateListener, AnimatorListener { - View view; - public AlphaUpdateListener(View v) { - view = v; - } + private void updateAccessibilityFlags(CellLayout page, int pageNo) { + if (mState == State.OVERVIEW) { + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + page.getShortcutsAndWidgets().setImportantForAccessibility( + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + page.setContentDescription(getPageDescription(pageNo)); - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - updateVisibility(view); - } - - public static void updateVisibility(View view) { - // We want to avoid the extra layout pass by setting the views to GONE unless - // accessibility is on, in which case not setting them to GONE causes a glitch. - int invisibleState = sAccessibilityEnabled ? GONE : INVISIBLE; - if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { - view.setVisibility(invisibleState); - } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD - && view.getVisibility() != VISIBLE) { - view.setVisibility(VISIBLE); + if (mPagesAccessibilityDelegate == null) { + mPagesAccessibilityDelegate = new OverviewScreenAccessibilityDelegate(this); } - } - - @Override - public void onAnimationCancel(Animator arg0) { - } - - @Override - public void onAnimationEnd(Animator arg0) { - updateVisibility(view); - } - - @Override - public void onAnimationRepeat(Animator arg0) { - } - - @Override - public void onAnimationStart(Animator arg0) { - // We want the views to be visible for animation, so fade-in/out is visible - view.setVisibility(VISIBLE); + page.setAccessibilityDelegate(mPagesAccessibilityDelegate); + } else { + int accessible = mState == State.NORMAL ? + IMPORTANT_FOR_ACCESSIBILITY_AUTO : + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + page.getShortcutsAndWidgets().setImportantForAccessibility(accessible); + page.setContentDescription(null); + page.setAccessibilityDelegate(null); } } @Override public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - onTransitionPrepare(); + mIsSwitchingState = true; + + // Invalidate here to ensure that the pages are rendered during the state change transition. + invalidate(); + + updateChildrenLayersEnabled(false); + hideCustomContentIfNecessary(); } @Override @@ -2541,17 +2045,9 @@ public class Workspace extends SmoothPagedView @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - onTransitionEnd(); - } - - private void onTransitionPrepare() { - mIsSwitchingState = true; - - // Invalidate here to ensure that the pages are rendered during the state change transition. - invalidate(); - + mIsSwitchingState = false; updateChildrenLayersEnabled(false); - hideCustomContentIfNecessary(); + showCustomContentIfNecessary(); } void updateCustomContentVisibility() { @@ -2577,15 +2073,17 @@ public class Workspace extends SmoothPagedView } } - private void onTransitionEnd() { - mIsSwitchingState = false; - updateChildrenLayersEnabled(false); - showCustomContentIfNecessary(); - } - - @Override - public View getContent() { - return this; + /** + * Returns the drawable for the given text view. + */ + public static Drawable getTextViewIcon(TextView tv) { + final Drawable[] drawables = tv.getCompoundDrawables(); + for (int i = 0; i < drawables.length; i++) { + if (drawables[i] != null) { + return drawables[i]; + } + } + return null; } /** @@ -2603,7 +2101,7 @@ public class Workspace extends SmoothPagedView destCanvas.save(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding); destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top); @@ -2640,7 +2138,7 @@ public class Workspace extends SmoothPagedView int padding = expectedPadding.get(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); b = Bitmap.createBitmap(bounds.width() + padding, bounds.height() + padding, Bitmap.Config.ARGB_8888); @@ -2701,7 +2199,12 @@ public class Workspace extends SmoothPagedView return b; } - void startDrag(CellLayout.CellInfo cellInfo) { + public void startDrag(CellLayout.CellInfo cellInfo) { + startDrag(cellInfo, false); + } + + @Override + public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) { View child = cellInfo.cell; // Make sure the drag was started by a long press as opposed to a long click. @@ -2714,10 +2217,15 @@ public class Workspace extends SmoothPagedView CellLayout layout = (CellLayout) child.getParent().getParent(); layout.prepareChildForDrag(child); - beginDragShared(child, this); + beginDragShared(child, this, accessible); } - public void beginDragShared(View child, DragSource source) { + public void beginDragShared(View child, DragSource source, boolean accessible) { + beginDragShared(child, new Point(), source, accessible); + } + + public void beginDragShared(View child, Point relativeTouchPos, DragSource source, + boolean accessible) { child.clearFocus(); child.setPressed(false); @@ -2737,16 +2245,27 @@ public class Workspace extends SmoothPagedView int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - padding.get() / 2); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); Point dragVisualizeOffset = null; Rect dragRect = null; if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; int iconSize = grid.iconSizePx; int top = child.getPaddingTop(); int left = (bmpWidth - iconSize) / 2; int right = left + iconSize; int bottom = top + iconSize; + if (icon.isLayoutHorizontal()) { + // If the layout is horizontal, then if we are just picking up the icon, then just + // use the child position since the icon is top-left aligned. Otherwise, offset + // the drag layer position horizontally so that the icon is under the current + // touch position. + if (icon.getIcon().getBounds().contains(relativeTouchPos.x, relativeTouchPos.y)) { + dragLayerX = Math.round(mTempXY[0]); + } else { + dragLayerX = Math.round(mTempXY[0] + relativeTouchPos.x - (bmpWidth / 2)); + } + } dragLayerY += top; // Note: The drag region is used to calculate drag layer offsets, but the // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. @@ -2771,7 +2290,7 @@ public class Workspace extends SmoothPagedView } DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); if (child.getParent() instanceof ShortcutAndWidgetContainer) { @@ -2782,8 +2301,7 @@ public class Workspace extends SmoothPagedView } public void beginExternalDragShared(View child, DragSource source) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); int iconSize = grid.iconSizePx; // Notify launcher of drag start @@ -2821,25 +2339,13 @@ public class Workspace extends SmoothPagedView // Start the drag DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, false); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); // Recycle temporary bitmaps tmpB.recycle(); } - void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, long screenId, - int cellX, int cellY, boolean insertAtFirst, int intersectX, int intersectY) { - View view = mLauncher.createShortcut(R.layout.application, target, (ShortcutInfo) info); - - final int[] cellXY = new int[2]; - target.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY); - addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, insertAtFirst); - - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, cellXY[0], - cellXY[1]); - } - public boolean transitionStateShouldAllowDrop() { return ((!isSwitchingState() || mTransitionProgress > 0.5f) && (mState == State.NORMAL || mState == State.SPRING_LOADED)); @@ -2858,8 +2364,7 @@ public class Workspace extends SmoothPagedView } if (!transitionStateShouldAllowDrop()) return false; - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); // We want the point to be mapped to the dragTarget. if (mLauncher.isHotseatLayout(dropTargetLayout)) { @@ -3060,10 +2565,11 @@ public class Workspace extends SmoothPagedView return false; } - public void onDrop(final DragObject d) { - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, - mDragViewVisualCenter); + @Override + public void prepareAccessibilityDrop() { } + public void onDrop(final DragObject d) { + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); CellLayout dropTargetLayout = mDropToLayout; // We want the point to be mapped to the dragTarget. @@ -3075,7 +2581,7 @@ public class Workspace extends SmoothPagedView } } - int snapScreen = -1; + int snapScreen = WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE; boolean resizeOnDrop = false; if (d.dragSource != this) { final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0], @@ -3178,9 +2684,9 @@ public class Workspace extends SmoothPagedView // in its final location final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; - AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo(); - if (pinfo != null && - pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo(); + if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE + && !d.accessibleDrag) { final Runnable addResizeFrame = new Runnable() { public void run() { DragLayer dragLayer = mLauncher.getDragLayer(); @@ -3228,13 +2734,17 @@ public class Workspace extends SmoothPagedView mAnimatingViewIntoPlace = true; if (d.dragView.hasDrawn()) { final ItemInfo info = (ItemInfo) cell.getTag(); - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + if (isWidget) { int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE : ANIMATE_INTO_POSITION_AND_DISAPPEAR; animateWidgetDrop(info, parent, d.dragView, onCompleteRunnable, animationType, cell, false); } else { - int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION; + int duration = snapScreen < 0 ? + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE : + ADJACENT_SCREEN_DROP_DURATION; mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, onCompleteRunnable, this); } @@ -3246,26 +2756,26 @@ public class Workspace extends SmoothPagedView } } - public void setFinalScrollForPageChange(int pageIndex) { - CellLayout cl = (CellLayout) getChildAt(pageIndex); - if (cl != null) { - mSavedScrollX = getScrollX(); - mSavedTranslationX = cl.getTranslationX(); - mSavedRotationY = cl.getRotationY(); - final int newX = getScrollForPage(pageIndex); - setScrollX(newX); - cl.setTranslationX(0f); - cl.setRotationY(0f); + /** + * Computes the area relative to dragLayer which is used to display a page. + */ + public void getPageAreaRelativeToDragLayer(Rect outArea) { + CellLayout child = (CellLayout) getChildAt(getNextPage()); + if (child == null) { + return; } - } + ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets(); - public void resetFinalScrollForPageChange(int pageIndex) { - if (pageIndex >= 0) { - CellLayout cl = (CellLayout) getChildAt(pageIndex); - setScrollX(mSavedScrollX); - cl.setTranslationX(mSavedTranslationX); - cl.setRotationY(mSavedRotationY); - } + // Use the absolute left instead of the child left, as we want the visible area + // irrespective of the visible child. Since the view can only scroll horizontally, the + // top position is not affected. + mTempXY[0] = getViewportOffsetX() + getPaddingLeft() + boundingLayout.getLeft(); + mTempXY[1] = child.getTop() + boundingLayout.getTop(); + + float scale = mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempXY); + outArea.set(mTempXY[0], mTempXY[1], + (int) (mTempXY[0] + scale * boundingLayout.getMeasuredWidth()), + (int) (mTempXY[1] + scale * boundingLayout.getMeasuredHeight())); } public void getViewLocationRelativeToSelf(View v, int[] location) { @@ -3281,8 +2791,12 @@ public class Workspace extends SmoothPagedView location[1] = vY - y; } + @Override public void onDragEnter(DragObject d) { - mDragEnforcer.onDragEnter(); + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragEnter", 1, 1); + } + mCreateUserFolderOnDrop = false; mAddToExistingFolderOnDrop = false; @@ -3300,42 +2814,46 @@ public class Workspace extends SmoothPagedView * widthGap/heightGap (right, bottom) */ static Rect getCellLayoutMetrics(Launcher launcher, int orientation) { LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + InvariantDeviceProfile inv = app.getInvariantDeviceProfile(); Display display = launcher.getWindowManager().getDefaultDisplay(); Point smallestSize = new Point(); Point largestSize = new Point(); display.getCurrentSizeRange(smallestSize, largestSize); - int countX = (int) grid.numColumns; - int countY = (int) grid.numRows; + int countX = (int) inv.numColumns; + int countY = (int) inv.numRows; + boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); if (orientation == CellLayout.LANDSCAPE) { if (mLandscapeCellLayoutMetrics == null) { - Rect padding = grid.getWorkspacePadding(CellLayout.LANDSCAPE); + Rect padding = inv.landscapeProfile.getWorkspacePadding(isLayoutRtl); int width = largestSize.x - padding.left - padding.right; int height = smallestSize.y - padding.top - padding.bottom; mLandscapeCellLayoutMetrics = new Rect(); mLandscapeCellLayoutMetrics.set( - grid.calculateCellWidth(width, countX), - grid.calculateCellHeight(height, countY), 0, 0); + DeviceProfile.calculateCellWidth(width, countX), + DeviceProfile.calculateCellHeight(height, countY), 0, 0); } return mLandscapeCellLayoutMetrics; } else if (orientation == CellLayout.PORTRAIT) { if (mPortraitCellLayoutMetrics == null) { - Rect padding = grid.getWorkspacePadding(CellLayout.PORTRAIT); + Rect padding = inv.portraitProfile.getWorkspacePadding(isLayoutRtl); int width = smallestSize.x - padding.left - padding.right; int height = largestSize.y - padding.top - padding.bottom; mPortraitCellLayoutMetrics = new Rect(); mPortraitCellLayoutMetrics.set( - grid.calculateCellWidth(width, countX), - grid.calculateCellHeight(height, countY), 0, 0); + DeviceProfile.calculateCellWidth(width, countX), + DeviceProfile.calculateCellHeight(height, countY), 0, 0); } return mPortraitCellLayoutMetrics; } return null; } + @Override public void onDragExit(DragObject d) { - mDragEnforcer.onDragExit(); + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragExit", -1, 0); + } // Here we store the final page that will be dropped to, if the workspace in fact // receives the drop @@ -3364,12 +2882,27 @@ public class Workspace extends SmoothPagedView mSpringLoadedDragController.cancel(); - if (!mIsPageMoving) { - hideOutlines(); - } mLauncher.getDragLayer().hidePageHints(); } + private void enfoceDragParity(String event, int update, int expectedValue) { + enfoceDragParity(this, event, update, expectedValue); + for (int i = 0; i < getChildCount(); i++) { + enfoceDragParity(getChildAt(i), event, update, expectedValue); + } + } + + private void enfoceDragParity(View v, String event, int update, int expectedValue) { + Object tag = v.getTag(R.id.drag_event_parity); + int value = tag == null ? 0 : (Integer) tag; + value += update; + v.setTag(R.id.drag_event_parity, value); + + if (value != expectedValue) { + Log.e(TAG, event + ": Drag contract violated: " + value); + } + } + void setCurrentDropLayout(CellLayout layout) { if (mDragTargetLayout != null) { mDragTargetLayout.revertTempState(); @@ -3473,8 +3006,7 @@ public class Workspace extends SmoothPagedView mTempPt[1] = y; mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempPt, true); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); r = grid.getHotseatRect(); if (r.contains(mTempPt[0], mTempPt[1])) { return true; @@ -3568,38 +3100,6 @@ public class Workspace extends SmoothPagedView return bestMatchingScreen; } - // This is used to compute the visual center of the dragView. This point is then - // used to visualize drop locations and determine where to drop an item. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; - } - - // First off, the drag view has been shifted in a way that is not represented in the - // x and y values or the x/yOffsets. Here we account for that shift. - x += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetX); - y += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; - - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; - - return res; - } - private boolean isDragWidget(DragObject d) { return (d.dragInfo instanceof LauncherAppWidgetInfo || d.dragInfo instanceof PendingAddWidgetInfo); @@ -3624,8 +3124,7 @@ public class Workspace extends SmoothPagedView // Ensure that we have proper spans for the item that we are dropping if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found"); - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); final View child = (mDragInfo == null) ? null : mDragInfo.cell; // Identify whether we have dragged over a side page @@ -3700,7 +3199,7 @@ public class Workspace extends SmoothPagedView mTargetCell[1]); manageFolderFeedback(info, mDragTargetLayout, mTargetCell, - targetCellDistance, dragOverView); + targetCellDistance, dragOverView, d.accessibleDrag); boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, @@ -3738,15 +3237,21 @@ public class Workspace extends SmoothPagedView } private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, - int[] targetCell, float distance, View dragOverView) { + int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) { boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, false); - if (mDragMode == DRAG_MODE_NONE && userFolderPending && !mFolderCreationAlarm.alarmPending()) { - mFolderCreationAlarm.setOnAlarmListener(new - FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1])); - mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + + FolderCreationAlarmListener listener = new + FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]); + + if (!accessibleDrag) { + mFolderCreationAlarm.setOnAlarmListener(listener); + mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + } else { + listener.onAlarm(mFolderCreationAlarm); + } return; } @@ -3950,26 +3455,14 @@ public class Workspace extends SmoothPagedView // When dragging and dropping from customization tray, we deal with creating // widgets/shortcuts/folders in a slightly different way - switch (pendingInfo.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - int span[] = new int[2]; - span[0] = item.spanX; - span[1] = item.spanY; - mLauncher.addAppWidgetFromDrop((PendingAddWidgetInfo) pendingInfo, - container, screenId, mTargetCell, span, null); - break; - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - mLauncher.processShortcutFromDrop(pendingInfo.componentName, - container, screenId, mTargetCell, null); - break; - default: - throw new IllegalStateException("Unknown item type: " + - pendingInfo.itemType); - } + mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell, + item.spanX, item.spanY); } }; - View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET - ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + || pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + + View finalView = isWidget ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; if (finalView instanceof AppWidgetHostView && updateWidgetSize) { AppWidgetHostView awhv = (AppWidgetHostView) finalView; @@ -3978,7 +3471,7 @@ public class Workspace extends SmoothPagedView } int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR; - if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + if (isWidget && ((PendingAddWidgetInfo) pendingInfo).info != null && ((PendingAddWidgetInfo) pendingInfo).info.configure != null) { animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN; } @@ -3993,10 +3486,9 @@ public class Workspace extends SmoothPagedView case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: if (info.container == NO_ID && info instanceof AppInfo) { // Came from all apps -- make a copy - info = new ShortcutInfo((AppInfo) info); + info = ((AppInfo) info).makeShortcut(); } - view = mLauncher.createShortcut(R.layout.application, cellLayout, - (ShortcutInfo) info); + view = mLauncher.createShortcut(cellLayout, (ShortcutInfo) info); break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, @@ -4055,8 +3547,7 @@ public class Workspace extends SmoothPagedView } public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, - widgetInfo.spanY, widgetInfo, false); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo, false); int visibility = layout.getVisibility(); layout.setVisibility(VISIBLE); @@ -4127,14 +3618,16 @@ public class Workspace extends SmoothPagedView // In the case where we've prebound the widget, we remove it from the DragLayer if (finalView instanceof AppWidgetHostView && external) { - Log.d(TAG, "6557954 Animate widget drop, final view is appWidgetHostView"); mLauncher.getDragLayer().removeView(finalView); } + + boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || + info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) { Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView); dragView.setCrossFadeBitmap(crossFadeBitmap); dragView.crossFade((int) (duration * 0.8f)); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && external) { + } else if (isWidget && external) { scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]); } @@ -4170,8 +3663,8 @@ public class Workspace extends SmoothPagedView public void setFinalTransitionTransform(CellLayout layout) { if (isSwitchingState()) { mCurrentScale = getScaleX(); - setScaleX(mNewScale); - setScaleY(mNewScale); + setScaleX(mStateTransitionAnimation.getFinalScale()); + setScaleY(mStateTransitionAnimation.getFinalScale()); } } public void resetTransitionTransform(CellLayout layout) { @@ -4208,7 +3701,7 @@ public class Workspace extends SmoothPagedView * * pixelX and pixelY should be in the coordinate system of layout */ - private int[] findNearestArea(int pixelX, int pixelY, + @Thunk int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, CellLayout layout, int[] recycle) { return layout.findNearestArea( pixelX, pixelY, spanX, spanY, recycle); @@ -4242,30 +3735,17 @@ public class Workspace extends SmoothPagedView if (success && !(beingCalledAfterUninstall && !mUninstallSuccessful)) { if (target != this && mDragInfo != null) { - CellLayout parentCell = getParentCellLayoutForView(mDragInfo.cell); - if (parentCell != null) { - parentCell.removeView(mDragInfo.cell); - } else if (LauncherAppState.isDogfoodBuild()) { - throw new NullPointerException("mDragInfo.cell has null parent"); - } - if (mDragInfo.cell instanceof DropTarget) { - mDragController.removeDropTarget((DropTarget) mDragInfo.cell); - } + removeWorkspaceItem(mDragInfo.cell); } } else if (mDragInfo != null) { - CellLayout cellLayout; - if (mLauncher.isHotseatLayout(target)) { - cellLayout = mLauncher.getHotseat().getLayout(); - } else { - cellLayout = getScreenWithId(mDragInfo.screenId); - } - if (cellLayout == null && LauncherAppState.isDogfoodBuild()) { - throw new RuntimeException("Invalid state: cellLayout == null in " - + "Workspace#onDropCompleted. Please file a bug. "); - } + final CellLayout cellLayout = mLauncher.getCellLayout( + mDragInfo.container, mDragInfo.screenId); if (cellLayout != null) { cellLayout.onDropChild(mDragInfo.cell); - } + } else if (LauncherAppState.isDogfoodBuild()) { + throw new RuntimeException("Invalid state: cellLayout == null in " + + "Workspace#onDropCompleted. Please file a bug. "); + }; } if ((d.cancelled || (beingCalledAfterUninstall && !mUninstallSuccessful)) && mDragInfo.cell != null) { @@ -4275,11 +3755,28 @@ public class Workspace extends SmoothPagedView mDragInfo = null; } + /** + * For opposite operation. See {@link #addInScreen}. + */ + public void removeWorkspaceItem(View v) { + CellLayout parentCell = getParentCellLayoutForView(v); + if (parentCell != null) { + parentCell.removeView(v); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new NullPointerException("mDragInfo.cell has null parent"); + } + if (v instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) v); + } + } + + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } /// maybe move this into a smaller part + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -4311,88 +3808,6 @@ public class Workspace extends SmoothPagedView } } - ArrayList<ComponentName> getUniqueComponents(boolean stripDuplicates, ArrayList<ComponentName> duplicates) { - ArrayList<ComponentName> uniqueIntents = new ArrayList<ComponentName>(); - getUniqueIntents((CellLayout) mLauncher.getHotseat().getLayout(), uniqueIntents, duplicates, false); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - CellLayout cl = (CellLayout) getChildAt(i); - getUniqueIntents(cl, uniqueIntents, duplicates, false); - } - return uniqueIntents; - } - - void getUniqueIntents(CellLayout cl, ArrayList<ComponentName> uniqueIntents, - ArrayList<ComponentName> duplicates, boolean stripDuplicates) { - int count = cl.getShortcutsAndWidgets().getChildCount(); - - ArrayList<View> children = new ArrayList<View>(); - for (int i = 0; i < count; i++) { - View v = cl.getShortcutsAndWidgets().getChildAt(i); - children.add(v); - } - - for (int i = 0; i < count; i++) { - View v = children.get(i); - ItemInfo info = (ItemInfo) v.getTag(); - // Null check required as the AllApps button doesn't have an item info - if (info instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) info; - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - cl.removeViewInLayout(v); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - if (v instanceof FolderIcon) { - FolderIcon fi = (FolderIcon) v; - ArrayList<View> items = fi.getFolder().getItemsInReadingOrder(); - for (int j = 0; j < items.size(); j++) { - if (items.get(j).getTag() instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) items.get(j).getTag(); - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - fi.getFolderInfo().remove(si); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - } - } - } - } - void saveWorkspaceToDb() { saveWorkspaceScreenToDb((CellLayout) mLauncher.getHotseat().getLayout()); int count = getChildCount(); @@ -4425,8 +3840,7 @@ public class Workspace extends SmoothPagedView cellX = hotseat.getCellXFromOrder((int) info.screenId); cellY = hotseat.getCellYFromOrder((int) info.screenId); } - LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, - cellY, false); + LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, cellY); } if (v instanceof FolderIcon) { FolderIcon fi = (FolderIcon) v; @@ -4456,7 +3870,7 @@ public class Workspace extends SmoothPagedView } @Override - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + public void onFlingToDelete(DragObject d, PointF vec) { // Do nothing } @@ -4470,12 +3884,6 @@ public class Workspace extends SmoothPagedView } @Override - protected void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - Launcher.setScreen(mCurrentPage); - } - - @Override protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { // We don't dispatch restoreInstanceState to our children using this code path. // Some pages will be restored immediately as their items are bound immediately, and @@ -4529,7 +3937,7 @@ public class Workspace extends SmoothPagedView @Override public boolean onEnterScrollArea(int x, int y, int direction) { // Ignore the scroll area if we are dragging over the hot seat - boolean isPortrait = !LauncherAppState.isScreenLandscape(getContext()); + boolean isPortrait = !mLauncher.getDeviceProfile().isLandscape; if (mLauncher.getHotseat() != null && isPortrait) { Rect r = new Rect(); mLauncher.getHotseat().getHitRect(r); @@ -4706,7 +4114,7 @@ public class Workspace extends SmoothPagedView && packageNames.contains(cn.getPackageName())) { shortcutInfo.isDisabled |= reason; BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); + shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache); if (parent != null) { parent.invalidate(); @@ -4890,9 +4298,9 @@ public class Workspace extends SmoothPagedView updates.contains(info)) { ShortcutInfo si = (ShortcutInfo) info; BubbleTextView shortcut = (BubbleTextView) v; - boolean oldPromiseState = shortcut.getCompoundDrawables()[1] + boolean oldPromiseState = getTextViewIcon(shortcut) instanceof PreloadIconDrawable; - shortcut.applyFromShortcutInfo(si, mIconCache, true, + shortcut.applyFromShortcutInfo(si, mIconCache, si.isPromise() != oldPromiseState); if (parent != null) { @@ -4912,31 +4320,17 @@ public class Workspace extends SmoothPagedView removeItemsByPackageName(packages, user); } - public void updatePackageBadge(final String packageName, final UserHandleCompat user) { + public void updateRestoreItems(final HashSet<ItemInfo> updates) { mapOverItems(MAP_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { - if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - ComponentName cn = shortcutInfo.getTargetComponent(); - if (user.equals(shortcutInfo.user) && cn != null - && shortcutInfo.isPromise() - && packageName.equals(cn.getPackageName())) { - if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { - // For auto install apps update the icon as well as label. - mIconCache.getTitleAndIcon(shortcutInfo, - shortcutInfo.promisedIntent, user, true); - } else { - // Only update the icon for restored apps. - shortcutInfo.updateIcon(mIconCache); - } - BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); - - if (parent != null) { - parent.invalidate(); - } - } + if (info instanceof ShortcutInfo && v instanceof BubbleTextView + && updates.contains(info)) { + ((BubbleTextView) v).applyState(false); + } else if (v instanceof PendingAppWidgetHostView + && info instanceof LauncherAppWidgetInfo + && updates.contains(info)) { + ((PendingAppWidgetHostView) v).applyState(); } // process all the shortcuts return false; @@ -4944,48 +4338,13 @@ public class Workspace extends SmoothPagedView }); } - public void updatePackageState(ArrayList<PackageInstallInfo> installInfos) { - for (final PackageInstallInfo installInfo : installInfos) { - if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) { - continue; - } - - mapOverItems(MAP_RECURSE, new ItemOperator() { - @Override - public boolean evaluate(ItemInfo info, View v, View parent) { - if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { - ShortcutInfo si = (ShortcutInfo) info; - ComponentName cn = si.getTargetComponent(); - if (si.isPromise() && (cn != null) - && installInfo.packageName.equals(cn.getPackageName())) { - si.setInstallProgress(installInfo.progress); - if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) { - // Mark this info as broken. - si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; - } - ((BubbleTextView)v).applyState(false); - } - } else if (v instanceof PendingAppWidgetHostView - && info instanceof LauncherAppWidgetInfo - && ((LauncherAppWidgetInfo) info).providerName.getPackageName() - .equals(installInfo.packageName)) { - ((LauncherAppWidgetInfo) info).installProgress = installInfo.progress; - ((PendingAppWidgetHostView) v).applyState(); - } - - // process all the shortcuts - return false; - } - }); - } - } - void widgetsRestored(ArrayList<LauncherAppWidgetInfo> changedInfo) { if (!changedInfo.isEmpty()) { DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo, mLauncher.getAppWidgetHost()); - if (LauncherModel.findAppWidgetProviderInfoWithComponent(getContext(), - changedInfo.get(0).providerName) != null) { + if (LauncherModel.getProviderInfo(getContext(), + changedInfo.get(0).providerName, + changedInfo.get(0).user) != null) { // Re-inflate the widgets which have changed status widgetRefresh.run(); } else { @@ -5049,25 +4408,21 @@ public class Workspace extends SmoothPagedView return super.getPageIndicatorMarker(pageIndex); } - @Override - public void syncPages() { - } - - @Override - public void syncPageItems(int page, boolean immediate) { - } - protected String getPageIndicatorDescription() { String settings = getResources().getString(R.string.settings_button_text); return getCurrentPageDescription() + ", " + settings; } protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - int delta = numCustomPages(); if (hasCustomContent() && getNextPage() == 0) { return mCustomContentDescription; } + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + return getPageDescription(page); + } + + private String getPageDescription(int page) { + int delta = numCustomPages(); return String.format(getContext().getString(R.string.workspace_scroll_format), page + 1 - delta, getChildCount() - delta); } @@ -5076,6 +4431,12 @@ public class Workspace extends SmoothPagedView 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/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java new file mode 100644 index 000000000..b8916a72b --- /dev/null +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -0,0 +1,587 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.util.Thunk; + +import java.util.HashMap; + +/** + * A convenience class to update a view's visibility state after an alpha animation. + */ +class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener { + private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; + + private View mView; + private boolean mAccessibilityEnabled; + + public AlphaUpdateListener(View v, boolean accessibilityEnabled) { + mView = v; + mAccessibilityEnabled = accessibilityEnabled; + } + + @Override + public void onAnimationUpdate(ValueAnimator arg0) { + updateVisibility(mView, mAccessibilityEnabled); + } + + public static void updateVisibility(View view, boolean accessibilityEnabled) { + // We want to avoid the extra layout pass by setting the views to GONE unless + // accessibility is on, in which case not setting them to GONE causes a glitch. + int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE; + if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { + view.setVisibility(invisibleState); + } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD + && view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationEnd(Animator arg0) { + updateVisibility(mView, mAccessibilityEnabled); + } + + @Override + public void onAnimationStart(Animator arg0) { + // We want the views to be visible for animation, so fade-in/out is visible + mView.setVisibility(View.VISIBLE); + } +} + +/** + * This interpolator emulates the rate at which the perceived scale of an object changes + * as its distance from a camera increases. When this interpolator is applied to a scale + * animation on a view, it evokes the sense that the object is shrinking due to moving away + * from the camera. + */ +class ZInterpolator implements TimeInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } +} + +/** + * The exact reverse of ZInterpolator. + */ +class InverseZInterpolator implements TimeInterpolator { + private ZInterpolator zInterpolator; + public InverseZInterpolator(float foc) { + zInterpolator = new ZInterpolator(foc); + } + public float getInterpolation(float input) { + return 1 - zInterpolator.getInterpolation(1 - input); + } +} + +/** + * InverseZInterpolator compounded with an ease-out. + */ +class ZoomInInterpolator implements TimeInterpolator { + private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); + } +} + +/** + * Stores the transition states for convenience. + */ +class TransitionStates { + + // Raw states + final boolean oldStateIsNormal; + final boolean oldStateIsSpringLoaded; + final boolean oldStateIsNormalHidden; + final boolean oldStateIsOverviewHidden; + final boolean oldStateIsOverview; + + final boolean stateIsNormal; + final boolean stateIsSpringLoaded; + final boolean stateIsNormalHidden; + final boolean stateIsOverviewHidden; + final boolean stateIsOverview; + + // Convenience members + final boolean workspaceToAllApps; + final boolean overviewToAllApps; + final boolean allAppsToWorkspace; + final boolean workspaceToOverview; + final boolean overviewToWorkspace; + + public TransitionStates(final Workspace.State fromState, final Workspace.State toState) { + oldStateIsNormal = (fromState == Workspace.State.NORMAL); + oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED); + oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN); + oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN); + oldStateIsOverview = (fromState == Workspace.State.OVERVIEW); + + stateIsNormal = (toState == Workspace.State.NORMAL); + stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED); + stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN); + stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN); + stateIsOverview = (toState == Workspace.State.OVERVIEW); + + workspaceToOverview = (oldStateIsNormal && stateIsOverview); + workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); + overviewToWorkspace = (oldStateIsOverview && stateIsNormal); + overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); + allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); + } +} + +/** + * Manages the animations between each of the workspace states. + */ +public class WorkspaceStateTransitionAnimation { + + public static final String TAG = "WorkspaceStateTransitionAnimation"; + + public static final int SCROLL_TO_CURRENT_PAGE = -1; + @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350; + + final @Thunk Launcher mLauncher; + final @Thunk Workspace mWorkspace; + + @Thunk AnimatorSet mStateAnimator; + @Thunk float[] mOldBackgroundAlphas; + @Thunk float[] mOldAlphas; + @Thunk float[] mNewBackgroundAlphas; + @Thunk float[] mNewAlphas; + @Thunk int mLastChildCount = -1; + + @Thunk float mCurrentScale; + @Thunk float mNewScale; + + @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); + + @Thunk float mSpringLoadedShrinkFactor; + @Thunk float mOverviewModeShrinkFactor; + @Thunk float mWorkspaceScrimAlpha; + @Thunk int mAllAppsTransitionTime; + @Thunk int mOverviewTransitionTime; + @Thunk int mOverlayTransitionTime; + @Thunk boolean mWorkspaceFadeInAdjacentScreens; + + public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { + mLauncher = launcher; + mWorkspace = workspace; + + DeviceProfile grid = mLauncher.getDeviceProfile(); + Resources res = launcher.getResources(); + mAllAppsTransitionTime = res.getInteger(R.integer.config_allAppsTransitionTime); + mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime); + mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime); + mSpringLoadedShrinkFactor = + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f; + mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f; + mOverviewModeShrinkFactor = grid.getOverviewModeScale(Utilities.isRtl(res)); + mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); + } + + public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, + int toPage, boolean animated, boolean hasOverlaySearchBar, + HashMap<View, Integer> layerViews) { + AccessibilityManager am = (AccessibilityManager) + mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE); + final boolean accessibilityEnabled = am.isEnabled(); + TransitionStates states = new TransitionStates(fromState, toState); + int duration = getAnimationDuration(states); + animateWorkspace(states, toPage, animated, duration, layerViews, + accessibilityEnabled); + animateSearchBar(states, animated, duration, hasOverlaySearchBar, layerViews, + accessibilityEnabled); + animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION); + return mStateAnimator; + } + + public float getFinalScale() { + return mNewScale; + } + + /** + * Reinitializes the arrays that we need for the animations on each page. + */ + private void reinitializeAnimationArrays() { + final int childCount = mWorkspace.getChildCount(); + if (mLastChildCount == childCount) return; + + mOldBackgroundAlphas = new float[childCount]; + mOldAlphas = new float[childCount]; + mNewBackgroundAlphas = new float[childCount]; + mNewAlphas = new float[childCount]; + } + + /** + * Returns the proper animation duration for a transition. + */ + private int getAnimationDuration(TransitionStates states) { + if (states.workspaceToAllApps || states.overviewToAllApps) { + return mAllAppsTransitionTime; + } else if (states.workspaceToOverview || states.overviewToWorkspace) { + return mOverviewTransitionTime; + } else { + return mOverlayTransitionTime; + } + } + + /** + * Starts a transition animation for the workspace. + */ + private void animateWorkspace(final TransitionStates states, int toPage, final boolean animated, + final int duration, final HashMap<View, Integer> layerViews, + final boolean accessibilityEnabled) { + // Reinitialize animation arrays for the current workspace state + reinitializeAnimationArrays(); + + // Cancel existing workspace animations and create a new animator set if requested + cancelAnimation(); + if (animated) { + mStateAnimator = LauncherAnimUtils.createAnimatorSet(); + } + + // Update the workspace state + float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ? + 1.0f : 0f; + float finalHotseatAndPageIndicatorAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ? + 1f : 0f; + float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f; + float finalWorkspaceTranslationY = states.stateIsOverview || states.stateIsOverviewHidden ? + mWorkspace.getOverviewModeTranslationY() : 0; + + final int childCount = mWorkspace.getChildCount(); + final int customPageCount = mWorkspace.numCustomPages(); + + mNewScale = 1.0f; + + if (states.oldStateIsOverview) { + mWorkspace.disableFreeScroll(); + } else if (states.stateIsOverview) { + mWorkspace.enableFreeScroll(); + } + + if (!states.stateIsNormal) { + if (states.stateIsSpringLoaded) { + mNewScale = mSpringLoadedShrinkFactor; + } else if (states.stateIsOverview || states.stateIsOverviewHidden) { + mNewScale = mOverviewModeShrinkFactor; + } + } + + if (toPage == SCROLL_TO_CURRENT_PAGE) { + toPage = mWorkspace.getPageNearestToCenterOfScreen(); + } + mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator); + + for (int i = 0; i < childCount; i++) { + final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); + boolean isCurrentPage = (i == toPage); + float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); + float finalAlpha; + if (states.stateIsNormalHidden || states.stateIsOverviewHidden) { + finalAlpha = 0f; + } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { + finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; + } else { + finalAlpha = 1f; + } + + // If we are animating to/from the small state, then hide the side pages and fade the + // current page in + if (!mWorkspace.isSwitchingState()) { + if (states.workspaceToAllApps || states.allAppsToWorkspace) { + if (states.allAppsToWorkspace && isCurrentPage) { + initialAlpha = 0f; + } else if (!isCurrentPage) { + initialAlpha = finalAlpha = 0f; + } + cl.setShortcutAndWidgetAlpha(initialAlpha); + } + } + + mOldAlphas[i] = initialAlpha; + mNewAlphas[i] = finalAlpha; + if (animated) { + mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); + mNewBackgroundAlphas[i] = finalBackgroundAlpha; + } else { + cl.setBackgroundAlpha(finalBackgroundAlpha); + cl.setShortcutAndWidgetAlpha(finalAlpha); + } + } + + final ViewGroup overviewPanel = mLauncher.getOverviewPanel(); + final View hotseat = mLauncher.getHotseat(); + final View pageIndicator = mWorkspace.getPageIndicator(); + if (animated) { + LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace); + scale.scaleX(mNewScale) + .scaleY(mNewScale) + .translationY(finalWorkspaceTranslationY) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + mStateAnimator.play(scale); + for (int index = 0; index < childCount; index++) { + final int i = index; + final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); + float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); + if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { + cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); + cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); + } else { + if (layerViews != null) { + layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER); + } + if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { + LauncherViewPropertyAnimator alphaAnim = + new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); + alphaAnim.alpha(mNewAlphas[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + mStateAnimator.play(alphaAnim); + } + if (mOldBackgroundAlphas[i] != 0 || + mNewBackgroundAlphas[i] != 0) { + ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha", + mOldBackgroundAlphas[i], mNewBackgroundAlphas[i]); + LauncherAnimUtils.ofFloat(cl, 0f, 1f); + bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.setDuration(duration); + mStateAnimator.play(bgAnim); + } + } + } + Animator pageIndicatorAlpha; + if (pageIndicator != null) { + pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator) + .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); + pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator, + accessibilityEnabled)); + } else { + // create a dummy animation so we don't need to do null checks later + pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0); + } + + LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat) + .alpha(finalHotseatAndPageIndicatorAlpha); + hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled)); + + LauncherViewPropertyAnimator overviewPanelAlpha = + new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha); + overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, + accessibilityEnabled)); + + // For animation optimations, we may need to provide the Launcher transition + // with a set of views on which to force build layers in certain scenarios. + hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); + overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (layerViews != null) { + // If layerViews is not null, we add these views, and indicate that + // the caller can manage layer state. + layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + } else { + // Otherwise let the animator handle layer management. + hotseatAlpha.withLayer(); + overviewPanelAlpha.withLayer(); + } + + if (states.workspaceToOverview) { + pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); + hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); + overviewPanelAlpha.setInterpolator(null); + } else if (states.overviewToWorkspace) { + pageIndicatorAlpha.setInterpolator(null); + hotseatAlpha.setInterpolator(null); + overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); + } + + overviewPanelAlpha.setDuration(duration); + pageIndicatorAlpha.setDuration(duration); + hotseatAlpha.setDuration(duration); + + mStateAnimator.play(overviewPanelAlpha); + mStateAnimator.play(hotseatAlpha); + mStateAnimator.play(pageIndicatorAlpha); + mStateAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mStateAnimator = null; + + if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { + overviewPanel.getChildAt(0).performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + }); + } else { + overviewPanel.setAlpha(finalOverviewPanelAlpha); + AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); + hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha); + AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled); + if (pageIndicator != null) { + pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha); + AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled); + } + mWorkspace.updateCustomContentVisibility(); + mWorkspace.setScaleX(mNewScale); + mWorkspace.setScaleY(mNewScale); + mWorkspace.setTranslationY(finalWorkspaceTranslationY); + + if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { + overviewPanel.getChildAt(0).performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + } + + /** + * Coordinates with the workspace animation to animate the search bar. + * + * TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the + * bar has no idea that it is hidden, and this has no idea what state the bar is + * actually in. + */ + private void animateSearchBar(TransitionStates states, boolean animated, int duration, + boolean hasOverlaySearchBar, final HashMap<View, Integer> layerViews, + final boolean accessibilityEnabled) { + + // The search bar is only visible in the workspace + final View searchBar = mLauncher.getOrCreateQsbBar(); + if (searchBar != null) { + final boolean searchBarWillBeShown = states.stateIsNormal; + final float finalSearchBarAlpha = searchBarWillBeShown ? 1f : 0f; + if (animated) { + if (hasOverlaySearchBar) { + // If there is an overlay search bar, then we will coordinate with it. + mStateAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // If we are transitioning to a visible search bar, show it immediately + // and let the overlay search bar has faded out + if (searchBarWillBeShown) { + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + // If we are transitioning to a hidden search bar, hide it only after + // the overlay search bar has faded in + if (!searchBarWillBeShown) { + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + }); + } else { + // Otherwise, we can just do the normal animation + LauncherViewPropertyAnimator searchBarAlpha = + new LauncherViewPropertyAnimator(searchBar).alpha(finalSearchBarAlpha); + searchBarAlpha.addListener(new AlphaUpdateListener(searchBar, + accessibilityEnabled)); + searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (layerViews != null) { + // If layerViews is not null, we add these views, and indicate that + // the caller can manage layer state. + layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + } else { + // Otherwise let the animator handle layer management. + searchBarAlpha.withLayer(); + } + searchBarAlpha.setDuration(duration); + mStateAnimator.play(searchBarAlpha); + } + } else { + // Set the search bar state immediately + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + } + + /** + * Animates the background scrim. Add to the state animator to prevent jankiness. + * + * @param finalAlpha the final alpha for the background scrim + * @param animated whether or not to set the background alpha immediately + * @duration duration of the animation + */ + private void animateBackgroundGradient(TransitionStates states, + boolean animated, int duration) { + + final DragLayer dragLayer = mLauncher.getDragLayer(); + final float startAlpha = dragLayer.getBackgroundAlpha(); + float finalAlpha = states.stateIsNormal ? 0 : mWorkspaceScrimAlpha; + + if (finalAlpha != startAlpha) { + if (animated) { + // These properties refer to the background protection gradient used for AllApps + // and Widget tray. + ValueAnimator bgFadeOutAnimation = + LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha); + bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + dragLayer.setBackgroundAlpha( + ((Float)animation.getAnimatedValue()).floatValue()); + } + }); + bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + bgFadeOutAnimation.setDuration(duration); + mStateAnimator.play(bgFadeOutAnimation); + } else { + dragLayer.setBackgroundAlpha(finalAlpha); + } + } + } + + /** + * Cancels the current animation. + */ + private void cancelAnimation() { + if (mStateAnimator != null) { + mStateAnimator.setDuration(0); + mStateAnimator.cancel(); + } + mStateAnimator = null; + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java new file mode 100644 index 000000000..78accf720 --- /dev/null +++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java @@ -0,0 +1,136 @@ +/* + * 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.accessibility; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.accessibility.AccessibilityEvent; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; + +import java.util.List; + +/** + * Helper class to make drag-and-drop in a {@link CellLayout} accessible. + */ +public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper + implements OnClickListener { + protected static final int INVALID_POSITION = -1; + + private static final int[] sTempArray = new int[2]; + + protected final CellLayout mView; + protected final Context mContext; + protected final LauncherAccessibilityDelegate mDelegate; + + private final Rect mTempRect = new Rect(); + + public DragAndDropAccessibilityDelegate(CellLayout forView) { + super(forView); + mView = forView; + mContext = mView.getContext(); + mDelegate = LauncherAppState.getInstance().getAccessibilityDelegate(); + } + + @Override + protected int getVirtualViewAt(float x, float y) { + if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) { + return INVALID_ID; + } + mView.pointToCellExact((int) x, (int) y, sTempArray); + + // Map cell to id + int id = sTempArray[0] + sTempArray[1] * mView.getCountX(); + return intersectsValidDropTarget(id); + } + + /** + * @return the view id of the top left corner of a valid drop region or + * {@link #INVALID_POSITION} if there is no such valid region. + */ + protected abstract int intersectsValidDropTarget(int id); + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViews) { + // We create a virtual view for each cell of the grid + // The cell ids correspond to cells in reading order. + int nCells = mView.getCountX() * mView.getCountY(); + + for (int i = 0; i < nCells; i++) { + if (intersectsValidDropTarget(i) == i) { + virtualViews.add(i); + } + } + } + + @Override + protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) { + String confirmation = getConfirmationForIconDrop(viewId); + mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation); + return true; + } + return false; + } + + @Override + public void onClick(View v) { + onPerformActionForVirtualView(getFocusedVirtualView(), + AccessibilityNodeInfoCompat.ACTION_CLICK, null); + } + + @Override + protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) { + if (id == INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + event.setContentDescription(mContext.getString(R.string.action_move_here)); + } + + @Override + protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { + if (id == INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + + node.setContentDescription(getLocationDescriptionForIconDrop(id)); + node.setBoundsInParent(getItemBounds(id)); + + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + node.setClickable(true); + node.setFocusable(true); + } + + protected abstract String getLocationDescriptionForIconDrop(int id); + + protected abstract String getConfirmationForIconDrop(int id); + + private Rect getItemBounds(int id) { + int cellX = id % mView.getCountX(); + int cellY = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + mView.cellToRect(cellX, cellY, dragInfo.info.spanX, dragInfo.info.spanY, mTempRect); + return mTempRect; + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java new file mode 100644 index 000000000..ff9989036 --- /dev/null +++ b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java @@ -0,0 +1,56 @@ +/* + * 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.accessibility; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.FolderPagedView; +import com.android.launcher3.R; + +/** + * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD in a folder. + */ +public class FolderAccessibilityHelper extends DragAndDropAccessibilityDelegate { + + /** + * 0-index position for the first cell in {@link #mView} in {@link #mParent}. + */ + private final int mStartPosition; + + private final FolderPagedView mParent; + + public FolderAccessibilityHelper(CellLayout layout) { + super(layout); + mParent = (FolderPagedView) layout.getParent(); + + int index = mParent.indexOfChild(layout); + mStartPosition = index * layout.getCountX() * layout.getCountY(); + } + @Override + protected int intersectsValidDropTarget(int id) { + return Math.min(id, mParent.getAllocatedContentSize() - mStartPosition - 1); + } + + @Override + protected String getLocationDescriptionForIconDrop(int id) { + return mContext.getString(R.string.move_to_position, id + mStartPosition + 1); + } + + @Override + protected String getConfirmationForIconDrop(int id) { + return mContext.getString(R.string.item_moved); + } +} diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java new file mode 100644 index 000000000..fe7b25edd --- /dev/null +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -0,0 +1,434 @@ +package com.android.launcher3.accessibility; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.appwidget.AppWidgetProviderInfo; +import android.content.DialogInterface; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragSource; +import com.android.launcher3.Folder; +import com.android.launcher3.FolderInfo; +import com.android.launcher3.InfoDropTarget; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetHostView; +import com.android.launcher3.LauncherAppWidgetInfo; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.UninstallDropTarget; +import com.android.launcher3.Workspace; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener { + + private static final String TAG = "LauncherAccessibilityDelegate"; + + private static final int REMOVE = R.id.action_remove; + private static final int INFO = R.id.action_info; + private static final int UNINSTALL = R.id.action_uninstall; + private static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; + private static final int MOVE = R.id.action_move; + private static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace; + private static final int RESIZE = R.id.action_resize; + + public enum DragType { + ICON, + FOLDER, + WIDGET + } + + public static class DragInfo { + public DragType dragType; + public ItemInfo info; + public View item; + } + + private final SparseArray<AccessibilityAction> mActions = new SparseArray<>(); + @Thunk final Launcher mLauncher; + + private DragInfo mDragInfo = null; + private AccessibilityDragSource mDragSource = null; + + public LauncherAccessibilityDelegate(Launcher launcher) { + mLauncher = launcher; + + mActions.put(REMOVE, new AccessibilityAction(REMOVE, + launcher.getText(R.string.delete_target_label))); + mActions.put(INFO, new AccessibilityAction(INFO, + launcher.getText(R.string.info_target_label))); + mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL, + launcher.getText(R.string.delete_target_uninstall_label))); + mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE, + launcher.getText(R.string.action_add_to_workspace))); + mActions.put(MOVE, new AccessibilityAction(MOVE, + launcher.getText(R.string.action_move))); + mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE, + launcher.getText(R.string.action_move_to_workspace))); + mActions.put(RESIZE, new AccessibilityAction(RESIZE, + launcher.getText(R.string.action_resize))); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!(host.getTag() instanceof ItemInfo)) return; + ItemInfo item = (ItemInfo) host.getTag(); + + if (DeleteDropTarget.supportsDrop(item)) { + info.addAction(mActions.get(REMOVE)); + } + if (UninstallDropTarget.supportsDrop(host.getContext(), item)) { + info.addAction(mActions.get(UNINSTALL)); + } + if (InfoDropTarget.supportsDrop(host.getContext(), item)) { + info.addAction(mActions.get(INFO)); + } + + if ((item instanceof ShortcutInfo) + || (item instanceof LauncherAppWidgetInfo) + || (item instanceof FolderInfo)) { + info.addAction(mActions.get(MOVE)); + + if (item.container >= 0) { + info.addAction(mActions.get(MOVE_TO_WORKSPACE)); + } else if (item instanceof LauncherAppWidgetInfo) { + if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) { + info.addAction(mActions.get(RESIZE)); + } + } + } if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) { + info.addAction(mActions.get(ADD_TO_WORKSPACE)); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if ((host.getTag() instanceof ItemInfo) + && performAction(host, (ItemInfo) host.getTag(), action)) { + return true; + } + return super.performAccessibilityAction(host, action, args); + } + + public boolean performAction(final View host, final ItemInfo item, int action) { + if (action == REMOVE) { + if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) { + announceConfirmation(R.string.item_removed); + return true; + } + return false; + } else if (action == INFO) { + InfoDropTarget.startDetailsActivityForInfo(item, mLauncher); + return true; + } else if (action == UNINSTALL) { + return UninstallDropTarget.startUninstallActivity(mLauncher, item); + } else if (action == MOVE) { + beginAccessibleDrag(host, item); + } else if (action == ADD_TO_WORKSPACE) { + final int[] coordinates = new int[2]; + final long screenId = findSpaceOnWorkspace(item, coordinates); + mLauncher.showWorkspace(true, new Runnable() { + + @Override + public void run() { + if (item instanceof AppInfo) { + ShortcutInfo info = ((AppInfo) item).makeShortcut(); + LauncherModel.addItemToDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + + ArrayList<ItemInfo> itemList = new ArrayList<>(); + itemList.add(info); + mLauncher.bindItems(itemList, 0, itemList.size(), true); + } else if (item instanceof PendingAddItemInfo) { + PendingAddItemInfo info = (PendingAddItemInfo) item; + Workspace workspace = mLauncher.getWorkspace(); + workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); + mLauncher.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates, info.spanX, info.spanY); + } + announceConfirmation(R.string.item_added_to_workspace); + } + }); + return true; + } else if (action == MOVE_TO_WORKSPACE) { + Folder folder = mLauncher.getWorkspace().getOpenFolder(); + mLauncher.closeFolder(folder); + ShortcutInfo info = (ShortcutInfo) item; + folder.getInfo().remove(info); + + final int[] coordinates = new int[2]; + final long screenId = findSpaceOnWorkspace(item, coordinates); + LauncherModel.moveItemInDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + + // Bind the item in next frame so that if a new workspace page was created, + // it will get laid out. + new Handler().post(new Runnable() { + + @Override + public void run() { + ArrayList<ItemInfo> itemList = new ArrayList<>(); + itemList.add(item); + mLauncher.bindItems(itemList, 0, itemList.size(), true); + announceConfirmation(R.string.item_moved); + } + }); + } else if (action == RESIZE) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; + final ArrayList<Integer> actions = getSupportedResizeActions(host, info); + CharSequence[] labels = new CharSequence[actions.size()]; + for (int i = 0; i < actions.size(); i++) { + labels[i] = mLauncher.getText(actions.get(i)); + } + + new AlertDialog.Builder(mLauncher) + .setTitle(R.string.action_resize) + .setItems(labels, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + performResizeAction(actions.get(which), host, info); + dialog.dismiss(); + } + }) + .show(); + } + return false; + } + + private ArrayList<Integer> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { + ArrayList<Integer> actions = new ArrayList<>(); + + AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); + if (providerInfo == null) { + return actions; + } + + CellLayout layout = (CellLayout) host.getParent().getParent(); + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { + if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || + layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { + actions.add(R.string.action_increase_width); + } + + if (info.spanX > info.minSpanX && info.spanX > 1) { + actions.add(R.string.action_decrease_width); + } + } + + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { + if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || + layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { + actions.add(R.string.action_increase_height); + } + + if (info.spanY > info.minSpanY && info.spanY > 1) { + actions.add(R.string.action_decrease_height); + } + } + return actions; + } + + @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); + + if (action == R.string.action_increase_width) { + if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) + && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) + || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) { + lp.cellX --; + info.cellX --; + } + lp.cellHSpan ++; + info.spanX ++; + } else if (action == R.string.action_decrease_width) { + lp.cellHSpan --; + info.spanX --; + } else if (action == R.string.action_increase_height) { + if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) { + lp.cellY --; + info.cellY --; + } + lp.cellVSpan ++; + info.spanY ++; + } else if (action == R.string.action_decrease_height) { + lp.cellVSpan --; + info.spanY --; + } + + layout.markCellsAsOccupiedForView(host); + Rect sizeRange = new Rect(); + AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange); + ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null, + sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom); + host.requestLayout(); + LauncherModel.updateItemInDatabase(mLauncher, info); + announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY)); + } + + @Thunk void announceConfirmation(int resId) { + announceConfirmation(mLauncher.getResources().getString(resId)); + } + + @Thunk void announceConfirmation(String confirmation) { + mLauncher.getDragLayer().announceForAccessibility(confirmation); + + } + + public boolean isInAccessibleDrag() { + return mDragInfo != null; + } + + public DragInfo getDragInfo() { + return mDragInfo; + } + + /** + * @param clickedTarget the actual view that was clicked + * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used + * as the actual drop location otherwise the views center is used. + */ + public void handleAccessibleDrop(View clickedTarget, Rect dropLocation, + String confirmation) { + if (!isInAccessibleDrag()) return; + + int[] loc = new int[2]; + if (dropLocation == null) { + loc[0] = clickedTarget.getWidth() / 2; + loc[1] = clickedTarget.getHeight() / 2; + } else { + loc[0] = dropLocation.centerX(); + loc[1] = dropLocation.centerY(); + } + + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc); + mLauncher.getDragController().completeAccessibleDrag(loc); + + if (!TextUtils.isEmpty(confirmation)) { + announceConfirmation(confirmation); + } + } + + public void beginAccessibleDrag(View item, ItemInfo info) { + mDragInfo = new DragInfo(); + mDragInfo.info = info; + mDragInfo.item = item; + mDragInfo.dragType = DragType.ICON; + if (info instanceof FolderInfo) { + mDragInfo.dragType = DragType.FOLDER; + } else if (info instanceof LauncherAppWidgetInfo) { + mDragInfo.dragType = DragType.WIDGET; + } + + CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info); + + Rect pos = new Rect(); + mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos); + mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY()); + + Workspace workspace = mLauncher.getWorkspace(); + + Folder folder = workspace.getOpenFolder(); + if (folder != null) { + if (folder.getItemsInReadingOrder().contains(item)) { + mDragSource = folder; + } else { + mLauncher.closeFolder(); + } + } + if (mDragSource == null) { + mDragSource = workspace; + } + mDragSource.enableAccessibleDrag(true); + mDragSource.startDrag(cellInfo, true); + + if (mLauncher.getDragController().isDragging()) { + mLauncher.getDragController().addDragListener(this); + } + } + + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + // No-op + } + + @Override + public void onDragEnd() { + mLauncher.getDragController().removeDragListener(this); + mDragInfo = null; + if (mDragSource != null) { + mDragSource.enableAccessibleDrag(false); + mDragSource = null; + } + } + + public static interface AccessibilityDragSource { + void startDrag(CellLayout.CellInfo cellInfo, boolean accessible); + + void enableAccessibleDrag(boolean enable); + } + + /** + * Find empty space on the workspace and returns the screenId. + */ + private long findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { + Workspace workspace = mLauncher.getWorkspace(); + ArrayList<Long> workspaceScreens = workspace.getScreenOrder(); + long screenId; + + // First check if there is space on the current screen. + int screenIndex = workspace.getCurrentPage(); + screenId = workspaceScreens.get(screenIndex); + CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex); + + boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + screenIndex = workspace.hasCustomContent() ? 1 : 0; + while (!found && screenIndex < workspaceScreens.size()) { + screenId = workspaceScreens.get(screenIndex); + layout = (CellLayout) workspace.getPageAt(screenIndex); + found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + screenIndex++; + } + + if (found) { + return screenId; + } + + workspace.addExtraEmptyScreen(); + screenId = workspace.commitExtraEmptyScreen(); + layout = workspace.getScreenWithId(screenId); + found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + + if (!found) { + Log.wtf(TAG, "Not enough space on an empty screen"); + } + return screenId; + } +} diff --git a/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java new file mode 100644 index 000000000..c5b52de72 --- /dev/null +++ b/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java @@ -0,0 +1,95 @@ +/* + * 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.accessibility; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.Workspace; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class OverviewScreenAccessibilityDelegate extends AccessibilityDelegate { + + private static final int MOVE_BACKWARD = R.id.action_move_screen_backwards; + private static final int MOVE_FORWARD = R.id.action_move_screen_forwards; + + private final SparseArray<AccessibilityAction> mActions = new SparseArray<>(); + private final Workspace mWorkspace; + + public OverviewScreenAccessibilityDelegate(Workspace workspace) { + mWorkspace = workspace; + + Context context = mWorkspace.getContext(); + boolean isRtl = Utilities.isRtl(context.getResources()); + mActions.put(MOVE_BACKWARD, new AccessibilityAction(MOVE_BACKWARD, + context.getText(isRtl ? R.string.action_move_screen_right : + R.string.action_move_screen_left))); + mActions.put(MOVE_FORWARD, new AccessibilityAction(MOVE_FORWARD, + context.getText(isRtl ? R.string.action_move_screen_left : + R.string.action_move_screen_right))); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (host != null) { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS ) { + int index = mWorkspace.indexOfChild(host); + mWorkspace.setCurrentPage(index); + } else if (action == MOVE_FORWARD) { + movePage(mWorkspace.indexOfChild(host) + 1, host); + return true; + } else if (action == MOVE_BACKWARD) { + movePage(mWorkspace.indexOfChild(host) - 1, host); + return true; + } + } + + return super.performAccessibilityAction(host, action, args); + } + + private void movePage(int finalIndex, View view) { + mWorkspace.onStartReordering(); + mWorkspace.removeView(view); + mWorkspace.addView(view, finalIndex); + mWorkspace.onEndReordering(); + mWorkspace.announceForAccessibility(mWorkspace.getContext().getText(R.string.screen_moved)); + + mWorkspace.updateAccessibilityFlags(); + view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + int index = mWorkspace.indexOfChild(host); + if (index < mWorkspace.getChildCount() - 1) { + info.addAction(mActions.get(MOVE_FORWARD)); + } + if (index > mWorkspace.numCustomPages()) { + info.addAction(mActions.get(MOVE_BACKWARD)); + } + } +} diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java new file mode 100644 index 000000000..80ddc13b7 --- /dev/null +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -0,0 +1,166 @@ +/* + * 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.accessibility; + +import android.text.TextUtils; +import android.view.View; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.CellLayout; +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; + +/** + * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD on workspace. + */ +public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate { + + public WorkspaceAccessibilityHelper(CellLayout layout) { + super(layout); + } + + /** + * Find the virtual view id corresponding to the top left corner of any drop region by which + * the passed id is contained. For an icon, this is simply + */ + @Override + protected int intersectsValidDropTarget(int id) { + int mCountX = mView.getCountX(); + int mCountY = mView.getCountY(); + + int x = id % mCountX; + int y = id / mCountX; + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + if (dragInfo.dragType == DragType.WIDGET && mView.isHotseat()) { + return INVALID_POSITION; + } + + if (dragInfo.dragType == DragType.WIDGET) { + // For a widget, every cell must be vacant. In addition, we will return any valid + // drop target by which the passed id is contained. + boolean fits = false; + + // These represent the amount that we can back off if we hit a problem. They + // get consumed as we move up and to the right, trying new regions. + int spanX = dragInfo.info.spanX; + int spanY = dragInfo.info.spanY; + + for (int m = 0; m < spanX; m++) { + for (int n = 0; n < spanY; n++) { + + fits = true; + int x0 = x - m; + int y0 = y - n; + + if (x0 < 0 || y0 < 0) continue; + + for (int i = x0; i < x0 + spanX; i++) { + if (!fits) break; + for (int j = y0; j < y0 + spanY; j++) { + if (i >= mCountX || j >= mCountY || mView.isOccupied(i, j)) { + fits = false; + break; + } + } + } + if (fits) { + return x0 + mCountX * y0; + } + } + } + return INVALID_POSITION; + } else { + // For an icon, we simply check the view directly below + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + // Empty cell. Good for an icon or folder. + return id; + } else if (dragInfo.dragType != DragType.FOLDER) { + // For icons, we can consider cells that have another icon or a folder. + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof FolderInfo || + info instanceof ShortcutInfo) { + return id; + } + } + return INVALID_POSITION; + } + } + + @Override + protected String getConfirmationForIconDrop(int id) { + int x = id % mView.getCountX(); + int y = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + return mContext.getString(R.string.item_moved); + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof ShortcutInfo) { + return mContext.getString(R.string.folder_created); + + } else if (info instanceof FolderInfo) { + return mContext.getString(R.string.added_to_folder); + } + } + return ""; + } + + @Override + protected String getLocationDescriptionForIconDrop(int id) { + int x = id % mView.getCountX(); + int y = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + if (mView.isHotseat()) { + return mContext.getString(R.string.move_to_hotseat_position, id + 1); + } else { + return mContext.getString(R.string.move_to_empty_cell, y + 1, x + 1); + } + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof ShortcutInfo) { + return mContext.getString(R.string.create_folder_with, info.title); + } else if (info instanceof FolderInfo) { + if (TextUtils.isEmpty(info.title)) { + // Find the first item in the folder. + FolderInfo folder = (FolderInfo) info; + ShortcutInfo firstItem = null; + for (ShortcutInfo shortcut : folder.contents) { + if (firstItem == null || firstItem.rank > shortcut.rank) { + firstItem = shortcut; + } + } + + if (firstItem != null) { + return mContext.getString(R.string.add_to_folder_with_app, firstItem.title); + } + } + return mContext.getString(R.string.add_to_folder, info.title); + } + } + return ""; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java new file mode 100644 index 000000000..67d572819 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -0,0 +1,638 @@ +/* + * 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.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget; +import com.android.launcher3.Folder; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +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.util.ComponentKey; +import com.android.launcher3.util.Thunk; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; + + + +/** + * A merge algorithm that merges every section indiscriminately. + */ +final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { + + @Override + public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, + AlphabeticalAppsList.SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Don't merge the predicted apps + if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + // Otherwise, merge every other section + return true; + } +} + +/** + * The logic we use to merge multiple sections. 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. + */ +final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { + + private int mMinAppsPerRow; + private int mMinRowsInMergedSection; + private int mMaxAllowableMerges; + private CharsetEncoder mAsciiEncoder; + + public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { + mMinAppsPerRow = minAppsPerRow; + mMinRowsInMergedSection = minRowsInMergedSection; + mMaxAllowableMerges = maxNumMerges; + mAsciiEncoder = Charset.forName("US-ASCII").newEncoder(); + } + + @Override + public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, + AlphabeticalAppsList.SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Don't merge the predicted apps + if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + + // 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 && + !isCrossScript; + } +} + +/** + * The all apps view container. + */ +public class AllAppsContainerView extends BaseContainerView implements DragSource, + LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener, + AllAppsSearchBarController.Callbacks { + + private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; + private static final int MAX_NUM_MERGES_PHONE = 2; + + @Thunk Launcher mLauncher; + @Thunk AlphabeticalAppsList mApps; + private AllAppsGridAdapter mAdapter; + private RecyclerView.LayoutManager mLayoutManager; + private RecyclerView.ItemDecoration mItemDecoration; + + @Thunk View mContent; + @Thunk View mContainerView; + @Thunk View mRevealView; + @Thunk AllAppsRecyclerView mAppsRecyclerView; + @Thunk AllAppsSearchBarController mSearchBarController; + private ViewGroup mSearchBarContainerView; + private View mSearchBarView; + + private int mSectionNamesMargin; + private int mNumAppsPerRow; + private int mNumPredictedAppsPerRow; + private int mRecyclerViewTopBottomPadding; + // This coordinate is relative to this container view + private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1); + // This coordinate is relative to its parent + private final Point mIconLastTouchPos = new Point(); + + private SpannableStringBuilder mSearchQueryBuilder = null; + + public AllAppsContainerView(Context context) { + this(context, null); + } + + public AllAppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + Resources res = context.getResources(); + + mLauncher = (Launcher) context; + mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mApps = new AlphabeticalAppsList(context); + mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this); + mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message)); + mApps.setAdapter(mAdapter); + mLayoutManager = mAdapter.getLayoutManager(); + mItemDecoration = mAdapter.getItemDecoration(); + mRecyclerViewTopBottomPadding = + res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding); + + mSearchQueryBuilder = new SpannableStringBuilder(); + Selection.setSelection(mSearchQueryBuilder, 0); + } + + /** + * Sets the current set of predicted apps. + */ + public void setPredictedApps(List<ComponentKey> apps) { + mApps.setPredictedApps(apps); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List<AppInfo> apps) { + mApps.setApps(apps); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List<AppInfo> apps) { + mApps.addApps(apps); + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List<AppInfo> apps) { + mApps.updateApps(apps); + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List<AppInfo> apps) { + mApps.removeApps(apps); + } + + /** + * Sets the search bar that shows above the a-z list. + */ + public void setSearchBarController(AllAppsSearchBarController searchController) { + if (mSearchBarController != null) { + throw new RuntimeException("Expected search bar controller to only be set once"); + } + mSearchBarController = searchController; + mSearchBarController.initialize(mApps, this); + + // Add the new search view to the layout + View searchBarView = searchController.getView(mSearchBarContainerView); + mSearchBarContainerView.addView(searchBarView); + mSearchBarContainerView.setVisibility(View.VISIBLE); + mSearchBarView = searchBarView; + setHasSearchBar(); + + updateBackgroundAndPaddings(); + } + + /** + * Scrolls this list view to the top. + */ + public void scrollToTop() { + mAppsRecyclerView.scrollToTop(); + } + + /** + * Returns the content view used for the launcher transitions. + */ + public View getContentView() { + return mContainerView; + } + + /** + * Returns the all apps search view. + */ + public View getSearchBarView() { + return mSearchBarView; + } + + /** + * Returns the reveal view used for the launcher transitions. + */ + public View getRevealView() { + return mRevealView; + } + + /** + * Returns an new instance of the default app search controller. + */ + public AllAppsSearchBarController newDefaultAppSearchController() { + return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView); + } + + /** + * Focuses the search field and begins an app search. + */ + public void startAppsSearch() { + if (mSearchBarController != null) { + mSearchBarController.focusSearchField(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + boolean isRtl = Utilities.isRtl(getResources()); + mAdapter.setRtl(isRtl); + mContent = findViewById(R.id.content); + + // This is a focus listener that proxies focus from a view into the list view. This is to + // work around the search box from getting first focus and showing the cursor. + View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + mAppsRecyclerView.requestFocus(); + } + } + }; + mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container); + mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener); + mContainerView = findViewById(R.id.all_apps_container); + mContainerView.setOnFocusChangeListener(focusProxyListener); + mRevealView = findViewById(R.id.all_apps_reveal); + + // Load the all apps recycler view + mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); + mAppsRecyclerView.setApps(mApps); + mAppsRecyclerView.setLayoutManager(mLayoutManager); + mAppsRecyclerView.setAdapter(mAdapter); + mAppsRecyclerView.setHasFixedSize(true); + if (mItemDecoration != null) { + mAppsRecyclerView.addItemDecoration(mItemDecoration); + } + + updateBackgroundAndPaddings(); + } + + @Override + public void onBoundsChanged(Rect newBounds) { + mLauncher.updateOverlayBounds(newBounds); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Update the number of items in the grid before we measure the view + int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : + MeasureSpec.getSize(widthMeasureSpec); + DeviceProfile grid = mLauncher.getDeviceProfile(); + grid.updateAppsViewNumCols(getResources(), availableWidth); + if (mNumAppsPerRow != grid.allAppsNumCols || + mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { + mNumAppsPerRow = grid.allAppsNumCols; + mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; + + // If there is a start margin to draw section names, determine how we are going to merge + // app sections + boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone; + AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ? + new FullMergeAlgorithm() : + new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f), + MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); + + mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); + mAdapter.setNumAppsPerRow(mNumAppsPerRow); + mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + /** + * Update the background and padding of the Apps view and children. Instead of insetting the + * container view, we inset the background and padding of the recycler view to allow for the + * recycler view to handle touch events (for fast scrolling) all the way to the edge. + */ + @Override + protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + boolean isRtl = Utilities.isRtl(getResources()); + + // TODO: Use quantum_panel instead of quantum_panel_shape + InsetDrawable background = new InsetDrawable( + getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, + padding.right, 0); + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + mContainerView.setBackground(background); + mRevealView.setBackground(background.getConstantState().newDrawable()); + mAppsRecyclerView.updateBackgroundPadding(bgPadding); + mAdapter.updateBackgroundPadding(bgPadding); + + // Hack: We are going to let the recycler view take the full width, so reset the padding on + // the container to zero after setting the background and apply the top-bottom padding to + // the content view instead so that the launcher transition clips correctly. + mContent.setPadding(0, padding.top, 0, padding.bottom); + mContainerView.setPadding(0, 0, 0, 0); + + // Pad the recycler view by the background padding plus the start margin (for the section + // names) + int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth()); + int topBottomPadding = mRecyclerViewTopBottomPadding; + if (isRtl) { + mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), + topBottomPadding, padding.right + startInset, topBottomPadding); + } else { + mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding, + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding); + } + + // Inset the search bar to fit its bounds above the container + if (mSearchBarView != null) { + Rect backgroundPadding = new Rect(); + if (mSearchBarView.getBackground() != null) { + mSearchBarView.getBackground().getPadding(backgroundPadding); + } + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + mSearchBarContainerView.getLayoutParams(); + lp.leftMargin = searchBarBounds.left - backgroundPadding.left; + lp.topMargin = searchBarBounds.top - backgroundPadding.top; + lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right; + mSearchBarContainerView.requestLayout(); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Determine if the key event was actual text, if so, focus the search bar and then dispatch + // the key normally so that it can process this key event + if (!mSearchBarController.isSearchFieldFocused() && + event.getAction() == KeyEvent.ACTION_DOWN) { + final int unicodeChar = event.getUnicodeChar(); + final boolean isKeyNotWhitespace = unicodeChar > 0 && + !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); + if (isKeyNotWhitespace) { + boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, + event.getKeyCode(), event); + if (gotKey && mSearchQueryBuilder.length() > 0) { + mSearchBarController.focusSearchField(); + } + } + } + + return super.dispatchKeyEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isAppsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + + // Start the drag + mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false); + // Enter spring loaded mode + mLauncher.enterSpringLoadedDragMode(); + + return false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + DeviceProfile grid = mLauncher.getDeviceProfile(); + return (float) grid.allAppsIconSizePx / grid.iconSizePx; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + + d.deferDragViewCleanupPostAnimation = false; + } + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + if (toWorkspace) { + // Reset the search bar after transitioning home + mSearchBarController.reset(); + } + } + + /** + * Handles the touch events to dismiss all apps when clicking outside the bounds of the + * recycler view. + */ + private boolean handleTouchEvent(MotionEvent ev) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + if (!mContentBounds.isEmpty()) { + // Outset the fixed bounds and check if the touch is outside all apps + Rect tmpRect = new Rect(mContentBounds); + tmpRect.inset(-grid.allAppsIconSizePx / 2, 0); + if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) { + mBoundsCheckLastTouchDownPos.set(x, y); + return true; + } + } else { + // Check if the touch is outside all apps + if (ev.getX() < getPaddingLeft() || + ev.getX() > (getWidth() - getPaddingRight())) { + mBoundsCheckLastTouchDownPos.set(x, y); + return true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mBoundsCheckLastTouchDownPos.x > -1) { + ViewConfiguration viewConfig = ViewConfiguration.get(getContext()); + float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x; + float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y; + float distance = (float) Math.hypot(dx, dy); + if (distance < viewConfig.getScaledTouchSlop()) { + // The background was clicked, so just go home + Launcher launcher = (Launcher) getContext(); + launcher.showWorkspace(true); + return true; + } + } + // Fall through + case MotionEvent.ACTION_CANCEL: + mBoundsCheckLastTouchDownPos.set(-1, -1); + break; + } + return false; + } + + @Override + public void onSearchResult(String query, ArrayList<ComponentKey> apps) { + if (apps != null) { + if (apps.isEmpty()) { + String formatStr = getResources().getString(R.string.all_apps_no_search_results); + mAdapter.setEmptySearchText(String.format(formatStr, query)); + } else { + mAppsRecyclerView.scrollToTop(); + } + mApps.setOrderedFilter(apps); + } + } + + @Override + public void clearSearchResult() { + mApps.setOrderedFilter(null); + + // Clear the search query + mSearchQueryBuilder.clear(); + mSearchQueryBuilder.clearSpans(); + Selection.setSelection(mSearchQueryBuilder, 0); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java new file mode 100644 index 000000000..057883cab --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -0,0 +1,453 @@ +/* + * 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.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.launcher3.AppInfo; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.HashMap; +import java.util.List; + + +/** + * The grid view adapter of all the apps. + */ +class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { + + public static final String TAG = "AppsGridAdapter"; + private static final boolean DEBUG = false; + + // A section break in the grid + public static final int SECTION_BREAK_VIEW_TYPE = 0; + // A normal icon + public static final int ICON_VIEW_TYPE = 1; + // A prediction icon + public static final int PREDICTION_ICON_VIEW_TYPE = 2; + // The message shown when there are no filtered results + public static final int EMPTY_SEARCH_VIEW_TYPE = 3; + + /** + * ViewHolder for each icon. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + + public ViewHolder(View v) { + super(v); + mContent = v; + } + } + + /** + * Helper class to size the grid items. + */ + public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { + + public GridSpanSizer() { + super(); + setSpanIndexCacheEnabled(true); + } + + @Override + public int getSpanSize(int position) { + if (mApps.hasNoFilteredResults()) { + // Empty view spans full width + return mAppsPerRow; + } + + switch (mApps.getAdapterItems().get(position).viewType) { + case AllAppsGridAdapter.ICON_VIEW_TYPE: + case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE: + return 1; + default: + // Section breaks span the full width + return mAppsPerRow; + } + } + } + + /** + * Helper class to draw the section headers + */ + public class GridItemDecoration extends RecyclerView.ItemDecoration { + + private static final boolean DEBUG_SECTION_MARGIN = false; + private static final boolean FADE_OUT_SECTIONS = false; + + private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); + private Rect mTmpBounds = new Rect(); + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mApps.hasFilter() || mAppsPerRow == 0) { + return; + } + + if (DEBUG_SECTION_MARGIN) { + Paint p = new Paint(); + p.setColor(0x33ff0000); + c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin, + parent.getMeasuredHeight(), p); + } + + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + boolean hasDrawnPredictedAppsDivider = false; + boolean showSectionNames = mSectionNamesMargin > 0; + int childCount = parent.getChildCount(); + int lastSectionTop = 0; + int lastSectionHeight = 0; + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); + if (!isValidHolderAndChild(holder, child, items)) { + continue; + } + + if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) { + // Draw the divider under the predicted apps + int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset; + c.drawLine(mBackgroundPadding.left, top, + parent.getWidth() - mBackgroundPadding.right, top, + mPredictedAppsDividerPaint); + hasDrawnPredictedAppsDivider = true; + + } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) { + // At this point, we only draw sections for each section break; + int viewTopOffset = (2 * child.getPaddingTop()); + int pos = holder.getPosition(); + AlphabeticalAppsList.AdapterItem item = items.get(pos); + AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; + + // Draw all the sections for this index + String lastSectionName = item.sectionName; + for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { + AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); + String sectionName = nextItem.sectionName; + if (nextItem.sectionInfo != sectionInfo) { + break; + } + if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { + continue; + } + + + // Find the section name bounds + PointF sectionBounds = getAndCacheSectionBounds(sectionName); + + // Calculate where to draw the section + int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); + int x = mIsRtl ? + parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : + mBackgroundPadding.left; + x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); + int y = child.getTop() + sectionBaseline; + + // Determine whether this is the last row with apps in that section, if + // so, then fix the section to the row allowing it to scroll past the + // baseline, otherwise, bound it to the baseline so it's in the viewport + int appIndexInSection = items.get(pos).sectionAppIndex; + int nextRowPos = Math.min(items.size() - 1, + pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); + AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); + boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); + if (!fixedToRow) { + y = Math.max(sectionBaseline, y); + } + + // In addition, if it overlaps with the last section that was drawn, then + // offset it so that it does not overlap + if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { + y += lastSectionTop - y + lastSectionHeight; + } + + // Draw the section header + if (FADE_OUT_SECTIONS) { + int alpha = 255; + if (fixedToRow) { + alpha = Math.min(255, + (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); + } + mSectionTextPaint.setAlpha(alpha); + } + c.drawText(sectionName, x, y, mSectionTextPaint); + + lastSectionTop = y; + lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); + lastSectionName = sectionName; + } + i += (sectionInfo.numApps - item.sectionAppIndex); + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + // Do nothing + } + + /** + * Given a section name, return the bounds of the given section name. + */ + private PointF getAndCacheSectionBounds(String sectionName) { + PointF bounds = mCachedSectionBounds.get(sectionName); + if (bounds == null) { + mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); + bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); + mCachedSectionBounds.put(sectionName, bounds); + } + return bounds; + } + + /** + * Returns whether we consider this a valid view holder for us to draw a divider or section for. + */ + private boolean isValidHolderAndChild(ViewHolder holder, View child, + List<AlphabeticalAppsList.AdapterItem> items) { + // Ensure item is not already removed + GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) + child.getLayoutParams(); + if (lp.isItemRemoved()) { + return false; + } + // Ensure we have a valid holder + if (holder == null) { + return false; + } + // Ensure we have a holder position + int pos = holder.getPosition(); + if (pos < 0 || pos >= items.size()) { + return false; + } + return true; + } + + /** + * Returns whether to draw the divider for a given child. + */ + private boolean shouldDrawItemDivider(ViewHolder holder, + List<AlphabeticalAppsList.AdapterItem> items) { + int pos = holder.getPosition(); + return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; + } + + /** + * Returns whether to draw the section for the given child. + */ + private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, + List<AlphabeticalAppsList.AdapterItem> items) { + int pos = holder.getPosition(); + AlphabeticalAppsList.AdapterItem item = items.get(pos); + + // Ensure it's an icon + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + // Draw the section header for the first item in each section + return (childIndex == 0) || + (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE); + } + } + + private LayoutInflater mLayoutInflater; + @Thunk AlphabeticalAppsList mApps; + private GridLayoutManager mGridLayoutMgr; + private GridSpanSizer mGridSizer; + private GridItemDecoration mItemDecoration; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + @Thunk final Rect mBackgroundPadding = new Rect(); + @Thunk int mPredictionBarDividerOffset; + @Thunk int mAppsPerRow; + @Thunk boolean mIsRtl; + private String mEmptySearchText; + + // Section drawing + @Thunk int mSectionNamesMargin; + @Thunk int mSectionHeaderOffset; + @Thunk Paint mSectionTextPaint; + @Thunk Paint mPredictedAppsDividerPaint; + + public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + Resources res = context.getResources(); + mApps = apps; + mGridSizer = new GridSpanSizer(); + mGridLayoutMgr = new GridLayoutManager(context, 1, GridLayoutManager.VERTICAL, false); + mGridLayoutMgr.setSpanSizeLookup(mGridSizer); + mItemDecoration = new GridItemDecoration(); + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); + + mSectionTextPaint = new Paint(); + mSectionTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.all_apps_grid_section_text_size)); + mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color)); + mSectionTextPaint.setAntiAlias(true); + + mPredictedAppsDividerPaint = new Paint(); + mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics())); + mPredictedAppsDividerPaint.setColor(0x1E000000); + mPredictedAppsDividerPaint.setAntiAlias(true); + mPredictionBarDividerOffset = + (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + + res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; + } + + /** + * Sets the number of apps per row. + */ + public void setNumAppsPerRow(int appsPerRow) { + mAppsPerRow = appsPerRow; + mGridLayoutMgr.setSpanCount(appsPerRow); + } + + /** + * Sets whether we are in RTL mode. + */ + public void setRtl(boolean rtl) { + mIsRtl = rtl; + } + + /** + * Sets the text to show when there are no apps. + */ + public void setEmptySearchText(String query) { + mEmptySearchText = query; + } + + /** + * Notifies the adapter of the background padding so that it can draw things correctly in the + * item decorator. + */ + public void updateBackgroundPadding(Rect padding) { + mBackgroundPadding.set(padding); + } + + /** + * Returns the grid layout manager. + */ + public GridLayoutManager getLayoutManager() { + return mGridLayoutMgr; + } + + /** + * Returns the item decoration for the recycler view. + */ + public RecyclerView.ItemDecoration getItemDecoration() { + // We don't draw any headers when we are uncomfortably dense + return mItemDecoration; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case EMPTY_SEARCH_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, + false)); + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext())); + case ICON_VIEW_TYPE: { + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.all_apps_icon, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) + .getLongPressTimeout()); + icon.setFocusable(true); + return new ViewHolder(icon); + } + case PREDICTION_ICON_VIEW_TYPE: { + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.all_apps_prediction_bar_icon, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) + .getLongPressTimeout()); + icon.setFocusable(true); + return new ViewHolder(icon); + } + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case ICON_VIEW_TYPE: { + AppInfo info = mApps.getAdapterItems().get(position).appInfo; + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + break; + } + case PREDICTION_ICON_VIEW_TYPE: { + AppInfo info = mApps.getAdapterItems().get(position).appInfo; + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + break; + } + case EMPTY_SEARCH_VIEW_TYPE: + TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); + emptyViewText.setText(mEmptySearchText); + break; + } + } + + @Override + public int getItemCount() { + if (mApps.hasNoFilteredResults()) { + // For the empty view + return 1; + } + return mApps.getAdapterItems().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_SEARCH_VIEW_TYPE; + } + + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + return item.viewType; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java new file mode 100644 index 000000000..730c8d15a --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -0,0 +1,327 @@ +/* + * 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.Context; +import android.graphics.Canvas; +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.BaseRecyclerViewFastScrollBar; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Stats; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.List; + +/** + * A RecyclerView with custom fast scroll support for the all apps view. + */ +public class AllAppsRecyclerView extends BaseRecyclerView + implements Stats.LaunchSourceProvider { + + private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0; + private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1; + + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0; + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1; + + private AlphabeticalAppsList mApps; + private int mNumAppsPerRow; + + @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; + @Thunk int mPrevFastScrollFocusedPosition; + @Thunk int mFastScrollFrameIndex; + @Thunk final int[] mFastScrollFrames = new int[10]; + + private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON; + private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; + + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + + public AllAppsRecyclerView(Context context) { + this(context, null); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr); + } + + /** + * Sets the list of apps in this view, used to determine the fastscroll position. + */ + public void setApps(AlphabeticalAppsList apps) { + mApps = apps; + } + + /** + * Sets the number of apps per row in this recycler view. + */ + public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) { + mNumAppsPerRow = numAppsPerRow; + + RecyclerView.RecycledViewPool pool = getRecycledViewPool(); + int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); + pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); + pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); + pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); + } + + /** + * Scrolls this recycler view to the top. + */ + public void scrollToTop() { + scrollToPosition(0); + } + + /** + * We need to override the draw to ensure that we don't draw the overscroll effect beyond the + * background bounds. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + super.dispatchDraw(canvas); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Bind event handlers + 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. + */ + @Override + public String scrollToPositionAtProgress(float touchFraction) { + int rowCount = mApps.getNumAppRows(); + if (rowCount == 0) { + return ""; + } + + // Stop the scroller if it is scrolling + stopScroll(); + + // Find the fastscroll section that maps to this touch fraction + List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = + mApps.getFastScrollerSections(); + AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); + if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); + if (info.touchFraction > touchFraction) { + break; + } + lastInfo = info; + } + } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ + lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); + } else { + throw new RuntimeException("Unexpected scroll bar mode"); + } + + // Map the touch position back to the scroll of the recycler view + getCurScrollState(mScrollPosState, mApps.getAdapterItems()); + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + } + + if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { + mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; + + // Reset the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; + } + + if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { + smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); + } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } + } else { + throw new RuntimeException("Unexpected fast scroll mode"); + } + } + return lastInfo.sectionName; + } + + @Override + public void onFastScrollCompleted() { + super.onFastScrollCompleted(); + // Reset and clean up the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; + } + mPrevFastScrollFocusedPosition = -1; + } + + /** + * Updates the bounds for the scrollbar. + */ + @Override + public void onUpdateScrollbar() { + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + + // Skip early if there are no items or we haven't been measured + if (items.isEmpty() || mNumAppsPerRow == 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Find the index and height of the first visible row (all rows have the same height) + int rowCount = mApps.getNumAppRows(); + getCurScrollState(mScrollPosState, items); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + } + + /** + * This runnable runs a single frame of the smooth scroll animation and posts the next frame + * if necessary. + */ + @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { + @Override + public void run() { + if (mFastScrollFrameIndex < mFastScrollFrames.length) { + scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); + mFastScrollFrameIndex++; + postOnAnimation(mSmoothSnapNextFrameRunnable); + } else { + // Animation completed, set the fast scroll state on the target view + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && + mLastFastScrollFocusedView != vh.itemView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } + } + } + }; + + /** + * Smoothly snaps to a given position. We do this manually by calculating the keyframes + * ourselves and animating the scroll on the recycler view. + */ + private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { + removeCallbacks(mSmoothSnapNextFrameRunnable); + + // Calculate the full animation from the current scroll position to the final scroll + // position, and then run the animation for the duration. + int curScrollY = getPaddingTop() + + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); + int numFrames = mFastScrollFrames.length; + for (int i = 0; i < numFrames; i++) { + // TODO(winsonc): We can interpolate this as well. + mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; + } + mFastScrollFrameIndex = 0; + postOnAnimation(mSmoothSnapNextFrameRunnable); + } + + /** + * Returns the current scroll state of the apps rows. + */ + private void getCurScrollState(ScrollPositionState stateOut, + List<AlphabeticalAppsList.AdapterItem> items) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + // Return early if there are no items or we haven't been measured + if (items.isEmpty() || mNumAppsPerRow == 0) { + return; + } + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + int position = getChildPosition(child); + if (position != NO_POSITION) { + AlphabeticalAppsList.AdapterItem item = items.get(position); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + stateOut.rowIndex = item.rowIndex; + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + break; + } + } + } + } + + /** + * Returns the scrollY for the given position in the adapter. + */ + private int getScrollAtPosition(int position, int rowHeight) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + int offset = item.rowIndex > 0 ? getPaddingTop() : 0; + return offset + item.rowIndex * rowHeight; + } else { + return 0; + } + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java new file mode 100644 index 000000000..14e2a1863 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java @@ -0,0 +1,72 @@ +/* + * 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.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler; +import com.android.launcher3.ClickShadowView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; + +/** + * A container for RecyclerView to allow for the click shadow view to be shown behind an icon that + * is launching. + */ +public class AllAppsRecyclerViewContainerView extends FrameLayout + implements BubbleTextShadowHandler { + + private final ClickShadowView mTouchFeedbackView; + + public AllAppsRecyclerViewContainerView(Context context) { + this(context, null); + } + + public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + Launcher launcher = (Launcher) context; + DeviceProfile grid = launcher.getDeviceProfile(); + + mTouchFeedbackView = new ClickShadowView(context); + + // Make the feedback view large enough to hold the blur bitmap. + int size = grid.allAppsIconSizePx + mTouchFeedbackView.getExtraSize(); + addView(mTouchFeedbackView, size, size); + } + + @Override + public void setPressedIcon(BubbleTextView icon, Bitmap background) { + if (icon == null || background == null) { + mTouchFeedbackView.setBitmap(null); + mTouchFeedbackView.animate().cancel(); + } else if (mTouchFeedbackView.setBitmap(background)) { + mTouchFeedbackView.alignWithIconView(icon, (ViewGroup) icon.getParent()); + mTouchFeedbackView.animateShadow(); + } + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java new file mode 100644 index 000000000..2b363c0cb --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java @@ -0,0 +1,100 @@ +/* + * 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.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; + +/** + * An interface to a search box that AllApps can command. + */ +public abstract class AllAppsSearchBarController { + + protected AlphabeticalAppsList mApps; + protected Callbacks mCb; + + /** + * Sets the references to the apps model and the search result callback. + */ + public final void initialize(AlphabeticalAppsList apps, Callbacks cb) { + mApps = apps; + mCb = cb; + onInitialize(); + } + + /** + * To be overridden by subclasses. This method will get called when the controller is set, + * before getView(). + */ + protected abstract void onInitialize(); + + /** + * Returns the search bar view. + * @param parent the parent to attach the search bar view to. + */ + public abstract View getView(ViewGroup parent); + + /** + * Focuses the search field to handle key events. + */ + public abstract void focusSearchField(); + + /** + * Returns whether the search field is focused. + */ + public abstract boolean isSearchFieldFocused(); + + /** + * Resets the search bar state. + */ + public abstract void reset(); + + /** + * Returns whether the prediction bar should currently be visible depending on the state of + * the search bar. + */ + @Deprecated + public abstract boolean shouldShowPredictionBar(); + + /** + * Callback for getting search results. + */ + public interface Callbacks { + + /** + * Called when the bounds of the search bar has changed. + */ + void onBoundsChanged(Rect newBounds); + + /** + * Called when the search is complete. + * + * @param apps sorted list of matching components or null if in case of failure. + */ + void onSearchResult(String query, ArrayList<ComponentKey> apps); + + /** + * Called when the search results should be cleared. + */ + void clearSearchResult(); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java new file mode 100644 index 000000000..b7dcd66ed --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java @@ -0,0 +1,65 @@ +/* + * 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.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + + +/** + * The edit text for the search container + */ +public class AllAppsSearchEditView extends EditText { + + /** + * Implemented by listeners of the back key. + */ + public interface OnBackKeyListener { + public void onBackKey(); + } + + private OnBackKeyListener mBackKeyListener; + + public AllAppsSearchEditView(Context context) { + this(context, null); + } + + public AllAppsSearchEditView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsSearchEditView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setOnBackKeyListener(OnBackKeyListener listener) { + mBackKeyListener = listener; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + // If this is a back key, propagate the key back to the listener + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + if (mBackKeyListener != null) { + mBackKeyListener.onBackKey(); + } + return false; + } + return super.onKeyPreIme(keyCode, event); + } +} diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java new file mode 100644 index 000000000..47241ce5d --- /dev/null +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -0,0 +1,590 @@ +/* + * 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.Context; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import com.android.launcher3.AppInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.model.AppNameComparator; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +/** + * The alphabetically sorted list of applications. + */ +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 + */ + public static class SectionInfo { + // The number of applications in this section + public int numApps; + // The section break AdapterItem for this section + public AdapterItem sectionBreakItem; + // The first app AdapterItem for this section + public AdapterItem firstAppItem; + } + + /** + * Info about a fast scroller section, depending if sections are merged, the fast scroller + * sections will not be the same set as the section headers. + */ + public static class FastScrollSectionInfo { + // The section name + public String sectionName; + // The AdapterItem to scroll to for this section + public AdapterItem fastScrollToItem; + // The touch fraction that should map to this fast scroll section info + public float touchFraction; + + public FastScrollSectionInfo(String sectionName) { + this.sectionName = sectionName; + } + } + + /** + * Info about a particular adapter item (can be either section or app) + */ + public static class AdapterItem { + /** Common properties */ + // The index of this adapter item in the list + public int position; + // The type of this item + public int viewType; + // The row that this item shows up on + public int rowIndex; + + /** Section & App properties */ + // The section for this item + public SectionInfo sectionInfo; + + /** App-only properties */ + // The section name of this app. Note that there can be multiple items with different + // sectionNames in the same section + public String sectionName = null; + // The index of this app in the section + public int sectionAppIndex = -1; + // The index of this app in the row + public int rowAppIndex; + // The associated AppInfo for the app + public AppInfo appInfo = null; + // The index of this app not including sections + public int appIndex = -1; + + public static AdapterItem asSectionBreak(int pos, SectionInfo section) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE; + item.position = pos; + item.sectionInfo = section; + section.sectionBreakItem = item; + return item; + } + + public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, + int sectionAppIndex, AppInfo appInfo, int appIndex) { + AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); + item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; + return item; + } + + public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, + int sectionAppIndex, AppInfo appInfo, int appIndex) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; + item.position = pos; + item.sectionInfo = section; + item.sectionName = sectionName; + item.sectionAppIndex = sectionAppIndex; + item.appInfo = appInfo; + item.appIndex = appIndex; + return item; + } + } + + /** + * Common interface for different merging strategies. + */ + public interface MergeAlgorithm { + boolean continueMerging(SectionInfo section, SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount); + } + + private Launcher mLauncher; + + // The set of apps from the system not including predictions + private final List<AppInfo> mApps = new ArrayList<>(); + private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>(); + + // The set of filtered apps with the current filter + private List<AppInfo> mFilteredApps = new ArrayList<>(); + // The current set of adapter items + private List<AdapterItem> mAdapterItems = new ArrayList<>(); + // The set of sections for the apps with the current filter + private List<SectionInfo> mSections = new ArrayList<>(); + // The set of sections that we allow fast-scrolling to (includes non-merged sections) + private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); + // The set of predicted app component names + private List<ComponentKey> 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<ComponentKey> mSearchResults; + private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); + private RecyclerView.Adapter mAdapter; + private AlphabeticIndexCompat mIndexer; + private AppNameComparator mAppNameComparator; + private MergeAlgorithm mMergeAlgorithm; + private int mNumAppsPerRow; + private int mNumPredictedAppsPerRow; + private int mNumAppRowsInAdapter; + + public AlphabeticalAppsList(Context context) { + mLauncher = (Launcher) context; + mIndexer = new AlphabeticIndexCompat(context); + mAppNameComparator = new AppNameComparator(context); + } + + /** + * Sets the number of apps per row. + */ + public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, + MergeAlgorithm mergeAlgorithm) { + mNumAppsPerRow = numAppsPerRow; + mNumPredictedAppsPerRow = numPredictedAppsPerRow; + mMergeAlgorithm = mergeAlgorithm; + + updateAdapterItems(); + } + + /** + * Sets the adapter to notify when this dataset changes. + */ + public void setAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** + * Returns all the apps. + */ + public List<AppInfo> getApps() { + return mApps; + } + + /** + * Returns sections of all the current filtered applications. + */ + public List<SectionInfo> getSections() { + return mSections; + } + + /** + * Returns fast scroller sections of all the current filtered applications. + */ + public List<FastScrollSectionInfo> getFastScrollerSections() { + return mFastScrollerSections; + } + + /** + * Returns the current filtered list of applications broken down into their sections. + */ + public List<AdapterItem> getAdapterItems() { + return mAdapterItems; + } + + /** + * Returns the number of applications in this list. + */ + public int getSize() { + return mFilteredApps.size(); + } + + /** + * Returns the number of rows of applications (not including predictions) + */ + public int getNumAppRows() { + return mNumAppRowsInAdapter; + } + + /** + * Returns whether there are is a filter set. + */ + public boolean hasFilter() { + return (mSearchResults != null); + } + + /** + * Returns whether there are no filtered results. + */ + public boolean hasNoFilteredResults() { + return (mSearchResults != null) && mFilteredApps.isEmpty(); + } + + /** + * Sets the sorted list of filtered components. + */ + public void setOrderedFilter(ArrayList<ComponentKey> f) { + if (mSearchResults != f) { + mSearchResults = f; + updateAdapterItems(); + } + } + + /** + * Sets the current set of predicted apps. Since this can be called before we get the full set + * of applications, we should merge the results only in onAppsUpdated() which is idempotent. + */ + public void setPredictedApps(List<ComponentKey> apps) { + mPredictedAppComponents.clear(); + mPredictedAppComponents.addAll(apps); + onAppsUpdated(); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List<AppInfo> apps) { + mComponentToAppMap.clear(); + addApps(apps); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List<AppInfo> apps) { + updateApps(apps); + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List<AppInfo> apps) { + for (AppInfo app : apps) { + mComponentToAppMap.put(app.toComponentKey(), app); + } + onAppsUpdated(); + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List<AppInfo> apps) { + for (AppInfo app : apps) { + mComponentToAppMap.remove(app.toComponentKey()); + } + onAppsUpdated(); + } + + /** + * Updates internals when the set of apps are updated. + */ + private void onAppsUpdated() { + // Sort the list of apps + mApps.clear(); + mApps.addAll(mComponentToAppMap.values()); + Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); + + // As a special case for some languages (currently only Simplified Chinese), we may need to + // coalesce sections + Locale curLocale = mLauncher.getResources().getConfiguration().locale; + TreeMap<String, ArrayList<AppInfo>> sectionMap = null; + boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); + if (localeRequiresSectionSorting) { + // Compute the section headers. We use a TreeMap with the section name comparator to + // ensure that the sections are ordered when we iterate over it later + sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); + for (AppInfo info : mApps) { + // Add the section to the cache + String sectionName = getAndUpdateCachedSectionName(info.title); + + // Add it to the mapping + ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); + if (sectionApps == null) { + sectionApps = new ArrayList<>(); + sectionMap.put(sectionName, sectionApps); + } + sectionApps.add(info); + } + + // Add each of the section apps to the list in order + List<AppInfo> allApps = new ArrayList<>(mApps.size()); + for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { + allApps.addAll(entry.getValue()); + } + + mApps.clear(); + mApps.addAll(allApps); + } else { + // Just compute the section headers for use below + for (AppInfo info : mApps) { + // Add the section to the cache + getAndUpdateCachedSectionName(info.title); + } + } + + // Recompose the set of adapter items from the current set of apps + updateAdapterItems(); + } + + /** + * Updates the set of filtered apps with the current filter. At this point, we expect + * mCachedSectionNames to have been calculated for the set of all apps in mApps. + */ + private void updateAdapterItems() { + SectionInfo lastSectionInfo = null; + String lastSectionName = null; + FastScrollSectionInfo lastFastScrollerSectionInfo = null; + int position = 0; + int appIndex = 0; + + // Prepare to update the list of sections, filtered apps, etc. + mFilteredApps.clear(); + mFastScrollerSections.clear(); + mAdapterItems.clear(); + mSections.clear(); + + if (DEBUG_PREDICTIONS) { + if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + } + } + + // Process the predicted app components + mPredictedApps.clear(); + if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { + for (ComponentKey ck : mPredictedAppComponents) { + AppInfo info = mComponentToAppMap.get(ck); + if (info != null) { + mPredictedApps.add(info); + } else { + if (LauncherAppState.isDogfoodBuild()) { + Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); + } + } + // Stop at the number of predicted apps + if (mPredictedApps.size() == mNumPredictedAppsPerRow) { + break; + } + } + + if (!mPredictedApps.isEmpty()) { + // Add a section for the predictions + lastSectionInfo = new SectionInfo(); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); + AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); + mSections.add(lastSectionInfo); + mFastScrollerSections.add(lastFastScrollerSectionInfo); + mAdapterItems.add(sectionItem); + + // Add the predicted app items + for (AppInfo info : mPredictedApps) { + AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, + "", lastSectionInfo.numApps++, info, appIndex++); + if (lastSectionInfo.firstAppItem == null) { + lastSectionInfo.firstAppItem = appItem; + lastFastScrollerSectionInfo.fastScrollToItem = appItem; + } + mAdapterItems.add(appItem); + mFilteredApps.add(info); + } + } + } + + // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the + // ordered set of sections + for (AppInfo info : getFiltersAppInfos()) { + String sectionName = getAndUpdateCachedSectionName(info.title); + + // Create a new section if the section names do not match + if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { + lastSectionName = sectionName; + lastSectionInfo = new SectionInfo(); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); + mSections.add(lastSectionInfo); + mFastScrollerSections.add(lastFastScrollerSectionInfo); + + // Create a new section item to break the flow of items in the list + if (!hasFilter()) { + AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); + mAdapterItems.add(sectionItem); + } + } + + // Create an app item + AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, + lastSectionInfo.numApps++, info, appIndex++); + if (lastSectionInfo.firstAppItem == null) { + lastSectionInfo.firstAppItem = appItem; + lastFastScrollerSectionInfo.fastScrollToItem = appItem; + } + mAdapterItems.add(appItem); + mFilteredApps.add(info); + } + + // Merge multiple sections together as requested by the merge strategy for this device + mergeSections(); + + if (mNumAppsPerRow != 0) { + // Update the number of rows in the adapter after we do all the merging (otherwise, we + // would have to shift the values again) + int numAppsInSection = 0; + int numAppsInRow = 0; + int rowIndex = -1; + for (AdapterItem item : mAdapterItems) { + item.rowIndex = 0; + if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) { + numAppsInSection = 0; + } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + if (numAppsInSection % mNumAppsPerRow == 0) { + numAppsInRow = 0; + rowIndex++; + } + item.rowIndex = rowIndex; + item.rowAppIndex = numAppsInRow; + numAppsInSection++; + numAppsInRow++; + } + } + mNumAppRowsInAdapter = rowIndex + 1; + + // Pre-calculate all the fast scroller fractions based on the number of rows + float rowFraction = 1f / mNumAppRowsInAdapter; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + + float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); + info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + } + } + + // Refresh the recycler view + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + private List<AppInfo> getFiltersAppInfos() { + if (mSearchResults == null) { + return mApps; + } + + ArrayList<AppInfo> result = new ArrayList<>(); + for (ComponentKey key : mSearchResults) { + AppInfo match = mComponentToAppMap.get(key); + if (match != null) { + result.add(match); + } + } + return result; + } + + /** + * Merges multiple sections to reduce visual raggedness. + */ + private void mergeSections() { + // Ignore merging until we have an algorithm and a valid row size + if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { + return; + } + + // Go through each section and try and merge some of the sections + if (!hasFilter()) { + int sectionAppCount = 0; + 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 (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 + mAdapterItems.remove(nextSection.sectionBreakItem); + int pos = mAdapterItems.indexOf(section.firstAppItem); + + // Point the section for these new apps to the merged section + int nextPos = pos + section.numApps; + for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { + AdapterItem item = mAdapterItems.get(j); + item.sectionInfo = section; + item.sectionAppIndex += section.numApps; + } + + // Update the following adapter items of the removed section item + pos = mAdapterItems.indexOf(nextSection.firstAppItem); + for (int j = pos; j < mAdapterItems.size(); j++) { + AdapterItem item = mAdapterItems.get(j); + item.position--; + } + section.numApps += nextSection.numApps; + sectionAppCount += nextSection.numApps; + + if (DEBUG) { + Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + + " to " + section.firstAppItem.sectionName + + " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); + } + mergeCount++; + } + } + } + } + + /** + * Returns the cached section name for the given title, recomputing and updating the cache if + * the title has no cached section name. + */ + private String getAndUpdateCachedSectionName(CharSequence title) { + String sectionName = mCachedSectionNames.get(title); + if (sectionName == null) { + sectionName = mIndexer.computeSectionName(title); + mCachedSectionNames.put(title, sectionName); + } + return sectionName; + } +} diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java new file mode 100644 index 000000000..10740ec77 --- /dev/null +++ b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java @@ -0,0 +1,94 @@ +/* + * 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.os.Handler; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.ComponentKey; + +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; + + public DefaultAppSearchAlgorithm(List<AppInfo> apps) { + mApps = apps; + mResultHandler = new Handler(); + } + + public void cancel(boolean interruptActiveRequests) { + if (interruptActiveRequests) { + mResultHandler.removeCallbacksAndMessages(null); + } + } + + public void doSearch(final String query, + final AllAppsSearchBarController.Callbacks callback) { + final ArrayList<ComponentKey> result = getTitleMatchResult(query); + mResultHandler.post(new Runnable() { + + @Override + public void run() { + callback.onSearchResult(query, result); + } + }); + } + + protected ArrayList<ComponentKey> getTitleMatchResult(String query) { + // 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)) { + result.add(info.toComponentKey()); + } + } + return result; + } + + protected 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/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java new file mode 100644 index 000000000..83b920589 --- /dev/null +++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java @@ -0,0 +1,269 @@ +/* + * 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.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.List; + + +/** + * The default search controller. + */ +final class DefaultAppSearchController extends AllAppsSearchBarController + implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener { + + private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; + + private static final int FADE_IN_DURATION = 175; + private static final int FADE_OUT_DURATION = 100; + private static final int SEARCH_TRANSLATION_X_DP = 18; + + private final Context mContext; + @Thunk final InputMethodManager mInputMethodManager; + + private DefaultAppSearchAlgorithm mSearchManager; + + private ViewGroup mContainerView; + private View mSearchView; + @Thunk View mSearchBarContainerView; + private View mSearchButtonView; + private View mDismissSearchButtonView; + @Thunk AllAppsSearchEditView mSearchBarEditView; + @Thunk AllAppsRecyclerView mAppsRecyclerView; + @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { + @Override + public void run() { + mAppsRecyclerView.requestFocus(); + } + }; + + public DefaultAppSearchController(Context context, ViewGroup containerView, + AllAppsRecyclerView appsRecyclerView) { + mContext = context; + mInputMethodManager = (InputMethodManager) + mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + mContainerView = containerView; + mAppsRecyclerView = appsRecyclerView; + } + + @Override + public View getView(ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false); + mSearchView.setOnClickListener(this); + + mSearchButtonView = mSearchView.findViewById(R.id.search_button); + mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); + mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); + mDismissSearchButtonView.setOnClickListener(this); + mSearchBarEditView = (AllAppsSearchEditView) + mSearchBarContainerView.findViewById(R.id.search_box_input); + mSearchBarEditView.addTextChangedListener(this); + mSearchBarEditView.setOnEditorActionListener(this); + mSearchBarEditView.setOnBackKeyListener( + new AllAppsSearchEditView.OnBackKeyListener() { + @Override + public void onBackKey() { + // Only hide the search field if there is no query, or if there + // are no filtered results + String query = Utilities.trim( + mSearchBarEditView.getEditableText().toString()); + if (query.isEmpty() || mApps.hasNoFilteredResults()) { + hideSearchField(true, mFocusRecyclerViewRunnable); + } + } + }); + return mSearchView; + } + + @Override + public void focusSearchField() { + mSearchBarEditView.requestFocus(); + showSearchField(); + } + + @Override + public boolean isSearchFieldFocused() { + return mSearchBarEditView.isFocused(); + } + + @Override + protected void onInitialize() { + mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps()); + } + + @Override + public void reset() { + hideSearchField(false, null); + } + + @Override + public boolean shouldShowPredictionBar() { + return false; + } + + @Override + public void onClick(View v) { + if (v == mSearchView) { + showSearchField(); + } else if (v == mDismissSearchButtonView) { + hideSearchField(true, mFocusRecyclerViewRunnable); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(final Editable s) { + String query = s.toString(); + if (query.isEmpty()) { + mSearchManager.cancel(true); + mCb.clearSearchResult(); + } else { + mSearchManager.cancel(false); + mSearchManager.doSearch(query, mCb); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Skip if we disallow app-launch-on-enter + if (!ALLOW_SINGLE_APP_LAUNCH) { + return false; + } + // Skip if it's not the right action + if (actionId != EditorInfo.IME_ACTION_DONE) { + return false; + } + // Skip if there isn't exactly one item + if (mApps.getSize() != 1) { + return false; + } + // If there is exactly one icon, then quick-launch it + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + for (int i = 0; i < items.size(); i++) { + AlphabeticalAppsList.AdapterItem item = items.get(i); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { + mAppsRecyclerView.getChildAt(i).performClick(); + mInputMethodManager.hideSoftInputFromWindow( + mContainerView.getWindowToken(), 0); + return true; + } + } + return false; + } + + /** + * Focuses the search field. + */ + private void showSearchField() { + // Show the search bar and focus the search + final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, + mContext.getResources().getDisplayMetrics()); + mSearchBarContainerView.setVisibility(View.VISIBLE); + mSearchBarContainerView.setAlpha(0f); + mSearchBarContainerView.setTranslationX(translationX); + mSearchBarContainerView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + mSearchBarEditView.requestFocus(); + mInputMethodManager.showSoftInput(mSearchBarEditView, + InputMethodManager.SHOW_IMPLICIT); + } + }); + mSearchButtonView.animate() + .alpha(0f) + .translationX(-translationX) + .setDuration(FADE_OUT_DURATION) + .withLayer(); + } + + /** + * Unfocuses the search field. + */ + @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) { + mSearchManager.cancel(true); + + final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; + final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, + mContext.getResources().getDisplayMetrics()); + if (animated) { + // Hide the search bar and focus the recycler view + mSearchBarContainerView.animate() + .alpha(0f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + mSearchBarContainerView.setVisibility(View.INVISIBLE); + if (resetTextField) { + mSearchBarEditView.setText(""); + } + mCb.clearSearchResult(); + if (postAnimationRunnable != null) { + postAnimationRunnable.run(); + } + } + }); + mSearchButtonView.setTranslationX(-translationX); + mSearchButtonView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_OUT_DURATION) + .withLayer(); + } else { + mSearchBarContainerView.setVisibility(View.INVISIBLE); + if (resetTextField) { + mSearchBarEditView.setText(""); + } + mCb.clearSearchResult(); + mSearchButtonView.setAlpha(1f); + mSearchButtonView.setTranslationX(0f); + if (postAnimationRunnable != null) { + postAnimationRunnable.run(); + } + } + mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0); + } +} diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java new file mode 100644 index 000000000..ec1fb669f --- /dev/null +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -0,0 +1,170 @@ +package com.android.launcher3.compat; + +import android.content.Context; +import com.android.launcher3.Utilities; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * Fallback class to support Alphabetic indexing if not supported by the framework. + * TODO(winsonc): disable for non-english locales + */ +class BaseAlphabeticIndex { + + private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"; + private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1; + + public BaseAlphabeticIndex() {} + + /** + * Sets the max number of the label buckets in this index. + */ + public void setMaxLabelCount(int count) { + // Not currently supported + } + + /** + * Returns the index of the bucket in which the given string should appear. + */ + protected int getBucketIndex(String s) { + if (s.isEmpty()) { + return UNKNOWN_BUCKET_INDEX; + } + int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase()); + if (index != -1) { + return index; + } + return UNKNOWN_BUCKET_INDEX; + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + return BUCKETS.substring(index, index + 1); + } +} + +/** + * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index. + */ +public class AlphabeticIndexCompat extends BaseAlphabeticIndex { + + private static final String MID_DOT = "\u2219"; + + private Object mAlphabeticIndex; + private Method mAddLabelsMethod; + private Method mSetMaxLabelCountMethod; + private Method mGetBucketIndexMethod; + private Method mGetBucketLabelMethod; + private boolean mHasValidAlphabeticIndex; + private String mDefaultMiscLabel; + + public AlphabeticIndexCompat(Context context) { + super(); + try { + Locale curLocale = context.getResources().getConfiguration().locale; + Class clazz = Class.forName("libcore.icu.AlphabeticIndex"); + Constructor ctor = clazz.getConstructor(Locale.class); + mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class); + mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class); + mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class); + mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class); + mAlphabeticIndex = ctor.newInstance(curLocale); + try { + // Ensure we always have some base English locale buckets + if (!curLocale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { + mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH); + } + } catch (Exception e) { + e.printStackTrace(); + } + if (curLocale.getLanguage().equals(Locale.JAPANESE.getLanguage())) { + // Japanese character ä»– ("misc") + mDefaultMiscLabel = "\u4ed6"; + // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji + } else { + // Dot + mDefaultMiscLabel = MID_DOT; + } + mHasValidAlphabeticIndex = true; + } catch (Exception e) { + mHasValidAlphabeticIndex = false; + } + } + + /** + * Sets the max number of the label buckets in this index. + * (ICU 51 default is 99) + */ + public void setMaxLabelCount(int count) { + if (mHasValidAlphabeticIndex) { + try { + mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + super.setMaxLabelCount(count); + } + } + + /** + * Computes the section name for an given string {@param s}. + */ + public String computeSectionName(CharSequence cs) { + String s = Utilities.trim(cs); + String sectionName = getBucketLabel(getBucketIndex(s)); + if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) { + int c = s.codePointAt(0); + boolean startsWithDigit = Character.isDigit(c); + if (startsWithDigit) { + // Digit section + return "#"; + } else { + boolean startsWithLetter = Character.isLetter(c); + if (startsWithLetter) { + return mDefaultMiscLabel; + } else { + // In languages where these differ, this ensures that we differentiate + // between the misc section in the native language and a misc section + // for everything else. + return MID_DOT; + } + } + } + return sectionName; + } + + /** + * Returns the index of the bucket in which {@param s} should appear. + * Function is synchronized because underlying routine walks an iterator + * whose state is maintained inside the index object. + */ + protected int getBucketIndex(String s) { + if (mHasValidAlphabeticIndex) { + try { + return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketIndex(s); + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + if (mHasValidAlphabeticIndex) { + try { + return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketLabel(index); + } +} diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java index 6512d427e..7aa36d447 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java @@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import java.util.List; @@ -63,20 +64,21 @@ public abstract class AppWidgetManagerCompat { public abstract List<AppWidgetProviderInfo> getAllProviders(); - public abstract String loadLabel(AppWidgetProviderInfo info); + public abstract String loadLabel(LauncherAppWidgetProviderInfo info); public abstract boolean bindAppWidgetIdIfAllowed( int appWidgetId, AppWidgetProviderInfo info, Bundle options); - public abstract UserHandleCompat getUser(AppWidgetProviderInfo info); + public abstract UserHandleCompat getUser(LauncherAppWidgetProviderInfo info); public abstract void startConfigActivity(AppWidgetProviderInfo info, int widgetId, Activity activity, AppWidgetHost host, int requestCode); public abstract Drawable loadPreview(AppWidgetProviderInfo info); - public abstract Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache); + public abstract Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache); - public abstract Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap); + public abstract Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight); } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java index f599f4303..f7f4b7e4f 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -16,6 +16,7 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.app.Activity; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; @@ -28,6 +29,7 @@ import android.os.Build; import android.os.Bundle; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import java.util.List; @@ -44,10 +46,11 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public String loadLabel(AppWidgetProviderInfo info) { - return info.label.trim(); + public String loadLabel(LauncherAppWidgetProviderInfo info) { + return Utilities.trim(info.label); } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info, Bundle options) { @@ -59,7 +62,7 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public UserHandleCompat getUser(AppWidgetProviderInfo info) { + public UserHandleCompat getUser(LauncherAppWidgetProviderInfo info) { return UserHandleCompat.myUserHandle(); } @@ -79,12 +82,13 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { + public Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache) { return cache.getFullResIcon(info.provider.getPackageName(), info.icon); } @Override - public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { + public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight) { return bitmap; } } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java index 03d43a6f2..13712d8c7 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java @@ -38,6 +38,7 @@ import android.view.View; import android.widget.Toast; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.R; import java.util.ArrayList; @@ -65,8 +66,8 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public String loadLabel(AppWidgetProviderInfo info) { - return info.loadLabel(mPm); + public String loadLabel(LauncherAppWidgetProviderInfo info) { + return info.getLabel(mPm); } @Override @@ -77,7 +78,10 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public UserHandleCompat getUser(AppWidgetProviderInfo info) { + public UserHandleCompat getUser(LauncherAppWidgetProviderInfo info) { + if (info.isCustomWidget) { + return UserHandleCompat.myUserHandle(); + } return UserHandleCompat.fromUser(info.getProfile()); } @@ -99,27 +103,28 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { - return info.loadIcon(mContext, cache.getFullResIconDpi()); + public Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache) { + return info.getIcon(mContext, cache); } @Override - public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { - if (info.getProfile().equals(android.os.Process.myUserHandle())) { + public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight) { + if (info.isCustomWidget || info.getProfile().equals(android.os.Process.myUserHandle())) { return bitmap; } // Add a user badge in the bottom right of the image. final Resources res = mContext.getResources(); final int badgeSize = res.getDimensionPixelSize(R.dimen.profile_badge_size); - final int badgeMargin = res.getDimensionPixelSize(R.dimen.profile_badge_margin); + final int badgeMinTop = res.getDimensionPixelSize(R.dimen.profile_badge_minimum_top); final Rect badgeLocation = new Rect(0, 0, badgeSize, badgeSize); - final int top = bitmap.getHeight() - badgeSize - badgeMargin; + final int top = Math.max(imageHeight - badgeSize, badgeMinTop); if (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - badgeLocation.offset(badgeMargin, top); + badgeLocation.offset(0, top); } else { - badgeLocation.offset(bitmap.getWidth() - badgeSize - badgeMargin, top); + badgeLocation.offset(bitmap.getWidth() - badgeSize, top); } Drawable drawable = mPm.getUserBadgedDrawableForDensity( diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java index 90a4d1a1f..07ef0efb7 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java @@ -17,7 +17,9 @@ package com.android.launcher3.compat; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; public abstract class LauncherActivityInfoCompat { @@ -32,4 +34,11 @@ public abstract class LauncherActivityInfoCompat { public abstract ApplicationInfo getApplicationInfo(); public abstract long getFirstInstallTime(); public abstract Drawable getBadgedIcon(int density); + + /** + * Creates a LauncherActivityInfoCompat for the primary user. + */ + public static LauncherActivityInfoCompat fromResolveInfo(ResolveInfo info, Context context) { + return new LauncherActivityInfoCompatV16(context, info); + } } diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java index 1d41a6ff6..ea51aace8 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java @@ -29,13 +29,15 @@ import android.graphics.drawable.Drawable; public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { - private ActivityInfo mActivityInfo; - private ComponentName mComponentName; - private PackageManager mPm; + private final ResolveInfo mResolveInfo; + private final ActivityInfo mActivityInfo; + private final ComponentName mComponentName; + private final PackageManager mPm; LauncherActivityInfoCompatV16(Context context, ResolveInfo info) { super(); - this.mActivityInfo = info.activityInfo; + mResolveInfo = info; + mActivityInfo = info.activityInfo; mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); mPm = context.getPackageManager(); } @@ -49,31 +51,30 @@ public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { } public CharSequence getLabel() { - return mActivityInfo.loadLabel(mPm); + return mResolveInfo.loadLabel(mPm); } public Drawable getIcon(int density) { - Drawable d = null; - if (mActivityInfo.getIconResource() != 0) { - Resources resources; + int iconRes = mResolveInfo.getIconResource(); + Resources resources = null; + Drawable icon = null; + // Get the preferred density icon from the app's resources + if (density != 0 && iconRes != 0) { try { - resources = mPm.getResourcesForApplication(mActivityInfo.packageName); - } catch (PackageManager.NameNotFoundException e) { - resources = null; - } - if (resources != null) { - try { - d = resources.getDrawableForDensity(mActivityInfo.getIconResource(), density); - } catch (Resources.NotFoundException e) { - // Return default icon below. - } + resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); + icon = resources.getDrawableForDensity(iconRes, density); + } catch (NameNotFoundException | Resources.NotFoundException exc) { } } - if (d == null) { - Resources resources = Resources.getSystem(); - d = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); + // Get the default density icon + if (icon == null) { + icon = mResolveInfo.loadIcon(mPm); + } + if (icon == null) { + resources = Resources.getSystem(); + icon = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); } - return d; + return icon; } public ApplicationInfo getApplicationInfo() { diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java index b52cf1de2..4448758e7 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java @@ -16,12 +16,14 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.graphics.drawable.Drawable; -import android.os.UserHandle; +import android.os.Build; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class LauncherActivityInfoCompatVL extends LauncherActivityInfoCompat { private LauncherActivityInfo mLauncherActivityInfo; diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index e47b9a58d..ac3d252f5 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -31,6 +31,8 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.List; @@ -139,11 +141,11 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { mContext.registerReceiver(mPackageMonitor, filter); } - private synchronized List<OnAppsChangedCallbackCompat> getCallbacks() { + @Thunk synchronized List<OnAppsChangedCallbackCompat> getCallbacks() { return new ArrayList<OnAppsChangedCallbackCompat>(mCallbacks); } - private class PackageMonitor extends BroadcastReceiver { + @Thunk class PackageMonitor extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final UserHandleCompat user = UserHandleCompat.myUserHandle(); diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java index e0d28b566..fbf91b548 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -16,6 +16,7 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -32,6 +33,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class LauncherAppsCompatVL extends LauncherAppsCompat { private LauncherApps mLauncherApps; @@ -49,7 +51,7 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat { List<LauncherActivityInfo> list = mLauncherApps.getActivityList(packageName, user.getUser()); if (list.size() == 0) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } ArrayList<LauncherActivityInfoCompat> compatList = new ArrayList<LauncherActivityInfoCompat>(list.size()); diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java index 0eb8754e8..c49908328 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompat.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java @@ -20,7 +20,7 @@ import android.content.Context; import com.android.launcher3.Utilities; -import java.util.HashSet; +import java.util.HashMap; public abstract class PackageInstallerCompat { @@ -37,25 +37,20 @@ public abstract class PackageInstallerCompat { if (Utilities.isLmpOrAbove()) { sInstance = new PackageInstallerCompatVL(context); } else { - sInstance = new PackageInstallerCompatV16(context) { }; + sInstance = new PackageInstallerCompatV16(); } } return sInstance; } } - public abstract HashSet<String> updateAndGetActiveSessionCache(); - - public abstract void onPause(); - - public abstract void onResume(); - - public abstract void onFinishBind(); + /** + * @return a map of active installs to their progress + */ + public abstract HashMap<String, Integer> updateAndGetActiveSessionCache(); public abstract void onStop(); - public abstract void recordPackageUpdate(String packageName, int state, int progress); - public static final class PackageInstallInfo { public final String packageName; diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java index 1910d22ae..654e34968 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java @@ -16,160 +16,17 @@ package com.android.launcher3.compat; -import android.content.Context; -import android.content.SharedPreferences; -import android.text.TextUtils; -import android.util.Log; - -import com.android.launcher3.LauncherAppState; - -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONStringer; -import org.json.JSONTokener; - -import java.util.ArrayList; -import java.util.HashSet; +import java.util.HashMap; public class PackageInstallerCompatV16 extends PackageInstallerCompat { - private static final String TAG = "PackageInstallerCompatV16"; - private static final boolean DEBUG = false; - - private static final String KEY_PROGRESS = "progress"; - private static final String KEY_STATE = "state"; - - private static final String PREFS = - "com.android.launcher3.compat.PackageInstallerCompatV16.queue"; - - protected final SharedPreferences mPrefs; - - boolean mUseQueue; - boolean mFinishedBind; - boolean mReplayPending; - - PackageInstallerCompatV16(Context context) { - mPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); - } - - @Override - public void onPause() { - mUseQueue = true; - if (DEBUG) Log.d(TAG, "updates paused"); - } - - @Override - public void onResume() { - mUseQueue = false; - if (mFinishedBind) { - replayUpdates(); - } - } - - @Override - public void onFinishBind() { - mFinishedBind = true; - if (!mUseQueue) { - replayUpdates(); - } - } + PackageInstallerCompatV16() { } @Override public void onStop() { } - private void replayUpdates() { - if (DEBUG) Log.d(TAG, "updates resumed"); - LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - if (app == null) { - mReplayPending = true; // try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - return; - } - mReplayPending = false; - ArrayList<PackageInstallInfo> updates = new ArrayList<PackageInstallInfo>(); - for (String packageName: mPrefs.getAll().keySet()) { - final String json = mPrefs.getString(packageName, null); - if (!TextUtils.isEmpty(json)) { - updates.add(infoFromJson(packageName, json)); - } - } - if (!updates.isEmpty()) { - sendUpdate(app, updates); - } - } - - /** - * This should be called by the implementations to register a package update. - */ - @Override - public synchronized void recordPackageUpdate(String packageName, int state, int progress) { - SharedPreferences.Editor editor = mPrefs.edit(); - PackageInstallInfo installInfo = new PackageInstallInfo(packageName); - installInfo.progress = progress; - installInfo.state = state; - if (state == STATUS_INSTALLED) { - // no longer necessary to track this package - editor.remove(packageName); - if (DEBUG) Log.d(TAG, "no longer tracking " + packageName); - } else { - editor.putString(packageName, infoToJson(installInfo)); - if (DEBUG) - Log.d(TAG, "saved state: " + infoToJson(installInfo) - + " for package: " + packageName); - - } - editor.commit(); - - if (!mUseQueue) { - if (mReplayPending) { - replayUpdates(); - } else if (state != STATUS_INSTALLED) { - LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - ArrayList<PackageInstallInfo> update = new ArrayList<PackageInstallInfo>(); - update.add(installInfo); - sendUpdate(app, update); - } - } - } - - private void sendUpdate(LauncherAppState app, ArrayList<PackageInstallInfo> updates) { - if (app == null) { - mReplayPending = true; // try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - } else { - app.setPackageState(updates); - } - } - - private static PackageInstallInfo infoFromJson(String packageName, String json) { - PackageInstallInfo info = new PackageInstallInfo(packageName); - try { - JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); - info.state = object.getInt(KEY_STATE); - info.progress = object.getInt(KEY_PROGRESS); - } catch (JSONException e) { - Log.e(TAG, "failed to deserialize app state update", e); - } - return info; - } - - private static String infoToJson(PackageInstallInfo info) { - String value = null; - try { - JSONStringer json = new JSONStringer() - .object() - .key(KEY_STATE).value(info.state) - .key(KEY_PROGRESS).value(info.progress) - .endObject(); - value = json.toString(); - } catch (JSONException e) { - Log.e(TAG, "failed to serialize app state update", e); - } - return value; - } - @Override - public HashSet<String> updateAndGetActiveSessionCache() { - return new HashSet<String>(); + public HashMap<String, Integer> updateAndGetActiveSessionCache() { + return new HashMap<>(); } } diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java index 601f04cea..3ad51017d 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java @@ -16,73 +16,55 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionCallback; import android.content.pm.PackageInstaller.SessionInfo; +import android.os.Build; import android.os.Handler; -import android.util.Log; import android.util.SparseArray; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.util.Thunk; -import java.util.ArrayList; -import java.util.HashSet; +import java.util.HashMap; -public class PackageInstallerCompatVL extends PackageInstallerCompat implements Runnable { +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class PackageInstallerCompatVL extends PackageInstallerCompat { - private static final String TAG = "PackageInstallerCompatVL"; - private static final boolean DEBUG = false; + @Thunk final SparseArray<String> mActiveSessions = new SparseArray<>(); - // All updates to these sets must happen on the {@link #mWorker} thread. - private final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>(); - private final HashSet<String> mPendingBadgeUpdates = new HashSet<String>(); - - private final PackageInstaller mInstaller; + @Thunk final PackageInstaller mInstaller; private final IconCache mCache; private final Handler mWorker; - private boolean mResumed; - private boolean mBound; - PackageInstallerCompatVL(Context context) { mInstaller = context.getPackageManager().getPackageInstaller(); LauncherAppState.setApplicationContext(context.getApplicationContext()); mCache = LauncherAppState.getInstance().getIconCache(); - mWorker = new Handler(); - - mResumed = false; - mBound = false; + mWorker = new Handler(LauncherModel.getWorkerLooper()); mInstaller.registerSessionCallback(mCallback, mWorker); - - // On start, send updates for all active sessions - mWorker.post(new Runnable() { - - @Override - public void run() { - for (SessionInfo info : mInstaller.getAllSessions()) { - mPendingReplays.append(info.getSessionId(), info); - } - } - }); } @Override - public HashSet<String> updateAndGetActiveSessionCache() { - HashSet<String> activePackages = new HashSet<String>(); + public HashMap<String, Integer> updateAndGetActiveSessionCache() { + HashMap<String, Integer> activePackages = new HashMap<>(); UserHandleCompat user = UserHandleCompat.myUserHandle(); for (SessionInfo info : mInstaller.getAllSessions()) { addSessionInfoToCahce(info, user); if (info.getAppPackageName() != null) { - activePackages.add(info.getAppPackageName()); + activePackages.put(info.getAppPackageName(), (int) (info.getProgress() * 100)); + mActiveSessions.put(info.getSessionId(), info.getAppPackageName()); } } return activePackages; } - private void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { + @Thunk void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { String packageName = info.getAppPackageName(); if (packageName != null) { mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(), @@ -95,74 +77,10 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements mInstaller.unregisterSessionCallback(mCallback); } - @Override - public void onFinishBind() { - mBound = true; - mWorker.post(this); - } - - @Override - public void onPause() { - mResumed = false; - } - - @Override - public void onResume() { - mResumed = true; - mWorker.post(this); - } - - @Override - public void recordPackageUpdate(String packageName, int state, int progress) { - // No op - } - - @Override - public void run() { - // Called on mWorker thread. - replayUpdates(null); - } - - private void replayUpdates(PackageInstallInfo newInfo) { - if (DEBUG) Log.d(TAG, "updates resumed"); - if (!mResumed || !mBound) { - // Not yet ready - return; - } - if ((mPendingReplays.size() == 0) && (newInfo == null)) { - // Nothing to update - return; - } - + @Thunk void sendUpdate(PackageInstallInfo info) { LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - if (app == null) { - // Try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - return; - } - - ArrayList<PackageInstallInfo> updates = new ArrayList<PackageInstallInfo>(); - if ((newInfo != null) && (newInfo.state != STATUS_INSTALLED)) { - updates.add(newInfo); - } - for (int i = mPendingReplays.size() - 1; i >= 0; i--) { - SessionInfo session = mPendingReplays.valueAt(i); - if (session.getAppPackageName() != null) { - updates.add(new PackageInstallInfo(session.getAppPackageName(), - STATUS_INSTALLING, - (int) (session.getProgress() * 100))); - } - } - mPendingReplays.clear(); - if (!updates.isEmpty()) { - app.setPackageState(updates); - } - - if (!mPendingBadgeUpdates.isEmpty()) { - for (String pkg : mPendingBadgeUpdates) { - app.updatePackageBadge(pkg); - } - mPendingBadgeUpdates.clear(); + if (app != null) { + app.getModel().setPackageState(info); } } @@ -170,19 +88,18 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements @Override public void onCreated(int sessionId) { - pushSessionBadgeToLauncher(sessionId); + pushSessionDisplayToLauncher(sessionId); } @Override public void onFinished(int sessionId, boolean success) { - mPendingReplays.remove(sessionId); - SessionInfo session = mInstaller.getSessionInfo(sessionId); - if ((session != null) && (session.getAppPackageName() != null)) { - mPendingBadgeUpdates.remove(session.getAppPackageName()); - // Replay all updates with a one time update for this installed package. No - // need to store this record for future updates, as the app list will get - // refreshed on resume. - replayUpdates(new PackageInstallInfo(session.getAppPackageName(), + // For a finished session, we can't get the session info. So use the + // packageName from our local cache. + String packageName = mActiveSessions.get(sessionId); + mActiveSessions.remove(sessionId); + + if (packageName != null) { + sendUpdate(new PackageInstallInfo(packageName, success ? STATUS_INSTALLED : STATUS_FAILED, 0)); } } @@ -191,8 +108,9 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements public void onProgressChanged(int sessionId, float progress) { SessionInfo session = mInstaller.getSessionInfo(sessionId); if (session != null) { - mPendingReplays.put(sessionId, session); - replayUpdates(null); + sendUpdate(new PackageInstallInfo(session.getAppPackageName(), + STATUS_INSTALLING, + (int) (session.getProgress() * 100))); } } @@ -201,18 +119,18 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements @Override public void onBadgingChanged(int sessionId) { - pushSessionBadgeToLauncher(sessionId); + pushSessionDisplayToLauncher(sessionId); } - private void pushSessionBadgeToLauncher(int sessionId) { + private void pushSessionDisplayToLauncher(int sessionId) { SessionInfo session = mInstaller.getSessionInfo(sessionId); if (session != null) { addSessionInfoToCahce(session, UserHandleCompat.myUserHandle()); - if (session.getAppPackageName() != null) { - mPendingBadgeUpdates.add(session.getAppPackageName()); + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + + if (app != null) { + app.getModel().updateSessionDisplayInfo(session.getAppPackageName()); } - mPendingReplays.put(sessionId, session); - replayUpdates(null); } } }; diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java index 2ae673171..ab4b7216b 100644 --- a/src/com/android/launcher3/compat/UserHandleCompat.java +++ b/src/com/android/launcher3/compat/UserHandleCompat.java @@ -16,10 +16,10 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Intent; import android.os.Build; import android.os.UserHandle; - import com.android.launcher3.Utilities; public class UserHandleCompat { @@ -32,6 +32,7 @@ public class UserHandleCompat { private UserHandleCompat() { } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static UserHandleCompat myUserHandle() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return new UserHandleCompat(android.os.Process.myUserHandle()); @@ -40,7 +41,7 @@ public class UserHandleCompat { } } - static UserHandleCompat fromUser(UserHandle user) { + public static UserHandleCompat fromUser(UserHandle user) { if (user == null) { return null; } else { diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java index 1374b4e49..a79d94646 100644 --- a/src/com/android/launcher3/compat/UserManagerCompat.java +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -43,4 +43,5 @@ public abstract class UserManagerCompat { public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user); public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user); + public abstract long getUserCreationTime(UserHandleCompat user); } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java index 32f972e85..ffe698c8b 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV16.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -48,4 +48,9 @@ public class UserManagerCompatV16 extends UserManagerCompat { public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { return label; } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + return 0; + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV17.java b/src/com/android/launcher3/compat/UserManagerCompatV17.java index 055359afe..c42c00c7d 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV17.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV17.java @@ -16,14 +16,12 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.UserHandle; +import android.os.Build; import android.os.UserManager; -import java.util.ArrayList; -import java.util.List; - +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public class UserManagerCompatV17 extends UserManagerCompatV16 { protected UserManager mUserManager; diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index 19eeabdcf..dd7a72617 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -17,29 +17,36 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.UserHandle; -import android.os.UserManager; - +import com.android.launcher3.LauncherAppState; import java.util.ArrayList; import java.util.Collections; import java.util.List; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class UserManagerCompatVL extends UserManagerCompatV17 { + private static final String USER_CREATION_TIME_KEY = "user_creation_time_"; + private final PackageManager mPm; + private final Context mContext; UserManagerCompatVL(Context context) { super(context); mPm = context.getPackageManager(); + mContext = context; } @Override public List<UserHandleCompat> getUserProfiles() { List<UserHandle> users = mUserManager.getUserProfiles(); if (users == null) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } ArrayList<UserHandleCompat> compatUsers = new ArrayList<UserHandleCompat>( users.size()); @@ -61,5 +68,17 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { } return mPm.getUserBadgedLabel(label, user.getUser()); } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + // TODO: Use system API once available. + SharedPreferences prefs = mContext.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user); + if (!prefs.contains(key)) { + prefs.edit().putLong(key, System.currentTimeMillis()).apply(); + } + return prefs.getLong(key, 0); + } } diff --git a/src/com/android/launcher3/config/ProviderConfig.java b/src/com/android/launcher3/config/ProviderConfig.java index db25076ea..e8930d063 100644 --- a/src/com/android/launcher3/config/ProviderConfig.java +++ b/src/com/android/launcher3/config/ProviderConfig.java @@ -18,5 +18,5 @@ package com.android.launcher3.config; public class ProviderConfig { - public static final String AUTHORITY = "com.android.launcher3.settings"; + public static final String AUTHORITY = "com.android.launcher3.settings".intern(); } 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 new file mode 100644 index 000000000..c4b74d4dc --- /dev/null +++ b/src/com/android/launcher3/model/AppNameComparator.java @@ -0,0 +1,101 @@ +/* + * 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.util.Thunk; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Class to manage access to an app name comparator. + * <p> + * Used to sort application name in all apps view and widget tray view. + */ +public class AppNameComparator { + private final Collator mCollator; + private final AbstractUserComparator<ItemInfo> mAppInfoComparator; + private final Comparator<String> mSectionNameComparator; + + public AppNameComparator(Context context) { + mCollator = Collator.getInstance(); + 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()); + if (result == 0 && a instanceof AppInfo && b instanceof AppInfo) { + AppInfo aAppInfo = (AppInfo) a; + AppInfo bAppInfo = (AppInfo) b; + // If two apps have the same title, then order by the component name + result = aAppInfo.componentName.compareTo(bAppInfo.componentName); + 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) + return super.compare(a, b); + } + } + return result; + } + }; + mSectionNameComparator = new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return compareTitles(o1, o2); + } + }; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of applications. + */ + public Comparator<ItemInfo> getAppInfoComparator() { + // Clear the user serial cache so that we get serials as needed in the comparator + mAppInfoComparator.clearUserCache(); + return mAppInfoComparator; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of section names. + */ + public Comparator<String> getSectionNameComparator() { + return mSectionNameComparator; + } + + /** + * Compares two titles with the same return value semantics as Comparator. + */ + @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 = (titleA.length() > 0) && + Character.isLetterOrDigit(titleA.codePointAt(0)); + boolean bStartsWithLetter = (titleB.length() > 0) && + Character.isLetterOrDigit(titleB.codePointAt(0)); + if (aStartsWithLetter && !bStartsWithLetter) { + return -1; + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1; + } + + // Order by the title in the current locale + return mCollator.compare(titleA, titleB); + } +} diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/PackageItemInfo.java new file mode 100644 index 000000000..30f228c68 --- /dev/null +++ b/src/com/android/launcher3/model/PackageItemInfo.java @@ -0,0 +1,65 @@ +/* + * 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.graphics.Bitmap; + +import com.android.launcher3.ItemInfo; + +import java.util.Arrays; + +/** + * Represents a {@link Package} in the widget tray section. + */ +public class PackageItemInfo extends ItemInfo { + + /** + * A bitmap version of the application icon. + */ + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon. + */ + public boolean usingLowResIcon; + + /** + * Package name of the {@link ItemInfo}. + */ + public String packageName; + + /** + * Character that is used as a section name for the {@link ItemInfo#title}. + * (e.g., "G" will be stored if title is "Google") + */ + public String titleSectionName; + + int flags = 0; + + PackageItemInfo(String packageName) { + this.packageName = packageName; + } + + @Override + public String toString() { + return "PackageItemInfo(title=" + title + " id=" + this.id + + " type=" + this.itemType + " container=" + this.container + + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; + } +} diff --git a/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java new file mode 100644 index 000000000..61e895283 --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java @@ -0,0 +1,68 @@ +package com.android.launcher3.model; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + +import java.text.Collator; +import java.util.Comparator; +import java.util.HashMap; + +public class WidgetsAndShortcutNameComparator implements Comparator<Object> { + private final AppWidgetManagerCompat mManager; + private final PackageManager mPackageManager; + private final HashMap<Object, String> mLabelCache; + private final Collator mCollator; + private final UserHandleCompat mMainHandle; + + public WidgetsAndShortcutNameComparator(Context context) { + mManager = AppWidgetManagerCompat.getInstance(context); + mPackageManager = context.getPackageManager(); + mLabelCache = new HashMap<Object, String>(); + mCollator = Collator.getInstance(); + mMainHandle = UserHandleCompat.myUserHandle(); + } + + @Override + public final int compare(Object a, Object b) { + String labelA, labelB; + if (mLabelCache.containsKey(a)) { + labelA = mLabelCache.get(a); + } else { + labelA = (a instanceof LauncherAppWidgetProviderInfo) + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) a)) + : Utilities.trim(((ResolveInfo) a).loadLabel(mPackageManager)); + mLabelCache.put(a, labelA); + } + if (mLabelCache.containsKey(b)) { + labelB = mLabelCache.get(b); + } else { + labelB = (b instanceof LauncherAppWidgetProviderInfo) + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) b)) + : Utilities.trim(((ResolveInfo) b).loadLabel(mPackageManager)); + mLabelCache.put(b, labelB); + } + + // Currently, there is no work profile shortcuts, hence only considering the widget cases. + + boolean aWorkProfile = (a instanceof LauncherAppWidgetProviderInfo) && + !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) a)); + boolean bWorkProfile = (b instanceof LauncherAppWidgetProviderInfo) && + !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) b)); + + // Independent of how the labels compare, if only one of the two widget info belongs to + // work profile, put that one in the back. + if (aWorkProfile && !bWorkProfile) { + return 1; + } + if (!aWorkProfile && bWorkProfile) { + return -1; + } + return mCollator.compare(labelA, labelB); + } +}; diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java new file mode 100644 index 000000000..185dfcae3 --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -0,0 +1,170 @@ + +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.util.Log; + +import com.android.launcher3.AppFilter; +import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * Widgets data model that is used by the adapters of the widget views and controllers. + * + * <p> The widgets and shortcuts are organized using package name as its index. + */ +public class WidgetsModel { + + private static final String TAG = "WidgetsModel"; + private static final boolean DEBUG = false; + + /* List of packages that is tracked by this model. */ + private ArrayList<PackageItemInfo> mPackageItemInfos = new ArrayList<>(); + + /* Map of widgets and shortcuts that are tracked per package. */ + private HashMap<PackageItemInfo, ArrayList<Object>> mWidgetsList = new HashMap<>(); + + private ArrayList<Object> mRawList; + + private final AppWidgetManagerCompat mAppWidgetMgr; + private final Comparator mWidgetAndShortcutNameComparator; + private final Comparator mAppNameComparator; + private final IconCache mIconCache; + private final AppFilter mAppFilter; + private AlphabeticIndexCompat mIndexer; + + public WidgetsModel(Context context, IconCache iconCache, AppFilter appFilter) { + mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context); + mWidgetAndShortcutNameComparator = new WidgetsAndShortcutNameComparator(context); + mAppNameComparator = (new AppNameComparator(context)).getAppInfoComparator(); + mIconCache = iconCache; + mAppFilter = appFilter; + mIndexer = new AlphabeticIndexCompat(context); + } + + private WidgetsModel(WidgetsModel model) { + mAppWidgetMgr = model.mAppWidgetMgr; + mPackageItemInfos = (ArrayList<PackageItemInfo>) model.mPackageItemInfos.clone(); + mWidgetsList = (HashMap<PackageItemInfo, ArrayList<Object>>) model.mWidgetsList.clone(); + mRawList = (ArrayList<Object>) model.mRawList.clone(); + mWidgetAndShortcutNameComparator = model.mWidgetAndShortcutNameComparator; + mAppNameComparator = model.mAppNameComparator; + mIconCache = model.mIconCache; + mAppFilter = model.mAppFilter; + } + + // Access methods that may be deleted if the private fields are made package-private. + public int getPackageSize() { + if (mPackageItemInfos == null) { + return 0; + } + return mPackageItemInfos.size(); + } + + // Access methods that may be deleted if the private fields are made package-private. + public PackageItemInfo getPackageItemInfo(int pos) { + if (pos >= mPackageItemInfos.size() || pos < 0) { + return null; + } + return mPackageItemInfos.get(pos); + } + + public List<Object> getSortedWidgets(int pos) { + return mWidgetsList.get(mPackageItemInfos.get(pos)); + } + + public ArrayList<Object> getRawList() { + return mRawList; + } + + public void setWidgetsAndShortcuts(ArrayList<Object> rawWidgetsShortcuts) { + Utilities.assertWorkerThread(); + mRawList = rawWidgetsShortcuts; + if (DEBUG) { + Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size()); + } + + // Temporary list for {@link PackageItemInfos} to avoid having to go through + // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList} + HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>(); + + // clear the lists. + mWidgetsList.clear(); + mPackageItemInfos.clear(); + + // add and update. + for (Object o: rawWidgetsShortcuts) { + String packageName = ""; + UserHandleCompat userHandle = null; + ComponentName componentName = null; + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + componentName = widgetInfo.provider; + packageName = widgetInfo.provider.getPackageName(); + userHandle = mAppWidgetMgr.getUser(widgetInfo); + } else if (o instanceof ResolveInfo) { + ResolveInfo resolveInfo = (ResolveInfo) o; + componentName = new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + packageName = resolveInfo.activityInfo.packageName; + userHandle = UserHandleCompat.myUserHandle(); + } + + if (componentName == null || userHandle == null) { + Log.e(TAG, String.format("Widget cannot be set for %s.", o.getClass().toString())); + continue; + } + if (mAppFilter != null && !mAppFilter.shouldShowApp(componentName)) { + if (DEBUG) { + Log.d(TAG, String.format("%s is filtered and not added to the widget tray.", + packageName)); + } + continue; + } + + PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName); + ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo); + if (widgetsShortcutsList != null) { + widgetsShortcutsList.add(o); + } else { + widgetsShortcutsList = new ArrayList<Object>(); + widgetsShortcutsList.add(o); + pInfo = new PackageItemInfo(packageName); + mIconCache.getTitleAndIconForApp(packageName, userHandle, + true /* userLowResIcon */, pInfo); + pInfo.titleSectionName = mIndexer.computeSectionName(pInfo.title); + mWidgetsList.put(pInfo, widgetsShortcutsList); + tmpPackageItemInfos.put(packageName, pInfo); + mPackageItemInfos.add(pInfo); + } + } + + // sort. + Collections.sort(mPackageItemInfos, mAppNameComparator); + for (PackageItemInfo p: mPackageItemInfos) { + Collections.sort(mWidgetsList.get(p), mWidgetAndShortcutNameComparator); + } + } + + /** + * Create a snapshot of the widgets model. + * <p> + * Usage case: view binding without being modified from package updates. + */ + @Override + public WidgetsModel clone(){ + return new WidgetsModel(this); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java index b264042cf..34492e4ca 100644 --- a/src/com/android/launcher3/LauncherExtension.java +++ b/src/com/android/launcher3/testing/LauncherExtension.java @@ -1,7 +1,6 @@ -package com.android.launcher3; +package com.android.launcher3.testing; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.ComponentName; @@ -12,9 +11,19 @@ import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.AppInfo; +import com.android.launcher3.InsettableFrameLayout; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.LauncherCallbacks; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsSearchBarController; +import com.android.launcher3.util.ComponentKey; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * This class represents a very trivial LauncherExtension. It primarily serves as a simple @@ -82,6 +91,11 @@ public class LauncherExtension extends Launcher { } @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + } + + @Override public void onWindowFocusChanged(boolean hasFocus) { } @@ -108,6 +122,10 @@ public class LauncherExtension extends Launcher { } @Override + public void onTrimMemory(int level) { + } + + @Override public void onLauncherProviderChange() { } @@ -242,6 +260,16 @@ public class LauncherExtension extends Launcher { } @Override + public AllAppsSearchBarController getAllAppsSearchBarController() { + return null; + } + + @Override + public List<ComponentKey> getPredictedApps() { + return new ArrayList<>(); + } + + @Override public boolean isLauncherPreinstalled() { return false; } @@ -261,6 +289,11 @@ public class LauncherExtension extends Launcher { return mLauncherOverlay; } + @Override + public void setLauncherSearchCallback(Object callbacks) { + // Do nothing + } + class LauncherExtensionOverlay implements LauncherOverlay { LauncherOverlayCallbacks mLauncherOverlayCallbacks; ViewGroup mOverlayView; diff --git a/src/com/android/launcher3/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 000000000..6a7df4318 --- /dev/null +++ b/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,81 @@ +package com.android.launcher3.util; + +/** + * 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. + */ + +import android.content.ComponentName; +import android.content.Context; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Arrays; + +public class ComponentKey { + + public final ComponentName componentName; + public final UserHandleCompat user; + + private final int mHashCode; + + public ComponentKey(ComponentName componentName, UserHandleCompat user) { + assert (componentName != null); + assert (user != null); + this.componentName = componentName; + this.user = user; + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + + } + + /** + * Creates a new component key from an encoded component key string in the form of + * [flattenedComponentString#userId]. If the userId is not present, then it defaults + * to the current user. + */ + public ComponentKey(Context context, String componentKeyStr) { + int userDelimiterIndex = componentKeyStr.indexOf("#"); + if (userDelimiterIndex != -1) { + String componentStr = componentKeyStr.substring(0, userDelimiterIndex); + Long componentUser = Long.valueOf(componentKeyStr.substring(userDelimiterIndex + 1)); + componentName = ComponentName.unflattenFromString(componentStr); + user = UserManagerCompat.getInstance(context) + .getUserForSerialNumber(componentUser.longValue()); + } else { + // No user provided, default to the current user + componentName = ComponentName.unflattenFromString(componentKeyStr); + user = UserHandleCompat.myUserHandle(); + } + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + } + + /** + * Encodes a component key as a string of the form [flattenedComponentString#userId]. + */ + public String flattenToString(Context context) { + return componentName.flattenToString() + "#" + + UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + ComponentKey other = (ComponentKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/util/CursorIconInfo.java b/src/com/android/launcher3/util/CursorIconInfo.java new file mode 100644 index 000000000..cdf9e3c60 --- /dev/null +++ b/src/com/android/launcher3/util/CursorIconInfo.java @@ -0,0 +1,70 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.Intent.ShortcutIconResource; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.text.TextUtils; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; + +/** + * Utility class to load icon from a cursor. + */ +public class CursorIconInfo { + public final int iconTypeIndex; + public final int iconPackageIndex; + public final int iconResourceIndex; + public final int iconIndex; + + public CursorIconInfo(Cursor c) { + iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); + iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); + iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); + iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); + } + + public Bitmap loadIcon(Cursor c, ShortcutInfo info, Context context) { + Bitmap icon = null; + int iconType = c.getInt(iconTypeIndex); + switch (iconType) { + case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: + String packageName = c.getString(iconPackageIndex); + String resourceName = c.getString(iconResourceIndex); + if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { + info.iconResource = new ShortcutIconResource(); + info.iconResource.packageName = packageName; + info.iconResource.resourceName = resourceName; + icon = Utilities.createIconBitmap(packageName, resourceName, context); + } + if (icon == null) { + // Failed to load from resource, try loading from DB. + icon = Utilities.createIconBitmap(c, iconIndex, context); + } + break; + case LauncherSettings.Favorites.ICON_TYPE_BITMAP: + icon = Utilities.createIconBitmap(c, iconIndex, context); + info.customIcon = icon != null; + break; + } + return icon; + } +} diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java new file mode 100644 index 000000000..55c5d7dc2 --- /dev/null +++ b/src/com/android/launcher3/util/FlingAnimation.java @@ -0,0 +1,104 @@ +package com.android.launcher3.util; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragView; +import com.android.launcher3.DropTarget.DragObject; + +public class FlingAnimation implements AnimatorUpdateListener { + + /** + * Maximum acceleration in one dimension (pixels per milliseconds) + */ + private static final float MAX_ACCELERATION = 0.5f; + private static final int DRAG_END_DELAY = 300; + + protected final DragObject mDragObject; + protected final Rect mIconRect; + protected final DragLayer mDragLayer; + protected final Rect mFrom; + protected final int mDuration; + protected final float mUX, mUY; + protected final float mAnimationTimeFraction; + protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); + + protected float mAX, mAY; + + /** + * @param vel initial fling velocity in pixels per second. + */ + public FlingAnimation(DragObject d, PointF vel, Rect iconRect, DragLayer dragLayer) { + mDragObject = d; + mUX = vel.x / 1000; + mUY = vel.y / 1000; + mIconRect = iconRect; + + mDragLayer = dragLayer; + mFrom = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, mFrom); + + float scale = d.dragView.getScaleX(); + float xOffset = ((scale - 1f) * d.dragView.getMeasuredWidth()) / 2f; + float yOffset = ((scale - 1f) * d.dragView.getMeasuredHeight()) / 2f; + mFrom.left += xOffset; + mFrom.right -= xOffset; + mFrom.top += yOffset; + mFrom.bottom -= yOffset; + + mDuration = initDuration(); + mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY); + } + + /** + * The fling animation is based on the following system + * - Apply a constant force in the y direction to causing the fling to decelerate. + * - The animation runs for the time taken by the object to go out of the screen. + * - Calculate a constant acceleration in x direction such that the object reaches + * {@link #mIconRect} in the given time. + */ + protected int initDuration() { + float sY = -mFrom.bottom; + + float d = mUY * mUY + 2 * sY * MAX_ACCELERATION; + if (d >= 0) { + // sY can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for y direction. + mAY = MAX_ACCELERATION; + } else { + // sY is not reachable, decrease the acceleration so that sY is almost reached. + d = 0; + mAY = mUY * mUY / (2 * -sY); + } + double t = (-mUY - Math.sqrt(d)) / mAY; + + float sX = -mFrom.exactCenterX() + mIconRect.exactCenterX(); + + // Find horizontal acceleration such that: u*t + a*t*t/2 = s + mAX = (float) ((sX - t * mUX) * 2 / (t * t)); + return (int) Math.round(t); + } + + public final int getDuration() { + return mDuration + DRAG_END_DELAY; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = animation.getAnimatedFraction(); + if (t > mAnimationTimeFraction) { + t = 1; + } else { + t = t / mAnimationTimeFraction; + } + final DragView dragView = (DragView) mDragLayer.getAnimatedView(); + final float time = t * mDuration; + dragView.setTranslationX(time * mUX + mFrom.left + mAX * time * time / 2); + dragView.setTranslationY(time * mUY + mFrom.top + mAY * time * time / 2); + dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + } +} diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java new file mode 100644 index 000000000..696eabe00 --- /dev/null +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -0,0 +1,507 @@ +/* + * 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.util; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.ShortcutAndWidgetContainer; + +import java.util.Arrays; + +/** + * Calculates the next item that a {@link KeyEvent} should change the focus to. + *<p> + * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates. + * Currently supports: + * <ul> + * <li> full matrix of cells that are 1x1 + * <li> sparse matrix of cells that are 1x1 + * [ 1][ ][ 2][ ] + * [ ][ ][ 3][ ] + * [ ][ 4][ ][ ] + * [ ][ 5][ 6][ 7] + * </ul> + * *<p> + * For testing, one can use a BT keyboard, or use following adb command. + * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT + */ +public class FocusLogic { + + private static final String TAG = "FocusLogic"; + private static final boolean DEBUG = false; + + // Item and page index related constant used by {@link #handleKeyEvent}. + public static final int NOOP = -1; + + public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; + public static final int PREVIOUS_PAGE_FIRST_ITEM = -3; + public static final int PREVIOUS_PAGE_LAST_ITEM = -4; + public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5; + + public static final int CURRENT_PAGE_FIRST_ITEM = -6; + public static final int CURRENT_PAGE_LAST_ITEM = -7; + + public static final int NEXT_PAGE_FIRST_ITEM = -8; + public static final int NEXT_PAGE_LEFT_COLUMN = -9; + public static final int NEXT_PAGE_RIGHT_COLUMN = -10; + + // Matrix related constant. + public static final int EMPTY = -1; + public static final int PIVOT = 100; + + /** + * Returns true only if this utility class handles the key code. + */ + public static boolean shouldConsume(int keyCode) { + return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || + keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || + keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END || + keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || + keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL); + } + + public static int handleKeyEvent(int keyCode, int cntX, int cntY, + int [][] map, int iconIdx, int pageIndex, int pageCount, boolean isRtl) { + + if (DEBUG) { + Log.v(TAG, String.format( + "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d", + cntX, cntY, iconIdx, pageIndex, pageCount)); + } + + int newIndex = NOOP; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_RIGHT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_LEFT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_LEFT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/); + break; + case KeyEvent.KEYCODE_DPAD_UP: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/); + break; + case KeyEvent.KEYCODE_MOVE_HOME: + newIndex = handleMoveHome(); + break; + case KeyEvent.KEYCODE_MOVE_END: + newIndex = handleMoveEnd(); + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + newIndex = handlePageDown(pageIndex, pageCount); + break; + case KeyEvent.KEYCODE_PAGE_UP: + newIndex = handlePageUp(pageIndex); + break; + default: + break; + } + + if (DEBUG) { + Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]", + iconIdx, getStringIndex(newIndex))); + } + return newIndex; + } + + /** + * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}. + * + * @param m number of columns in the matrix + * @param n number of rows in the matrix + */ + // TODO: get rid of dynamic matrix creation. + private static int[][] createFullMatrix(int m, int n) { + int[][] matrix = new int [m][n]; + + for (int i=0; i < m;i++) { + Arrays.fill(matrix[i], EMPTY); + } + return matrix; + } + + /** + * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the + * index of the child view. + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout layout) { + ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets(); + final int m = layout.getCountX(); + final int n = layout.getCountY(); + final boolean invert = parent.invertLayoutHorizontally(); + + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children. + for (int i = 0; i < parent.getChildCount(); i++ ) { + int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY; + matrix[invert ? (m - cx - 1) : cx][cy] = i; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout. + * The size of the returning matrix is [icon column count x (icon + hotseat row count)] + * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout, + boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + + int m, n; + if (isHorizontal) { + m = iconLayout.getCountX(); + n = iconLayout.getCountY() + hotseatLayout.getCountY(); + } else { + m = iconLayout.getCountX() + hotseatLayout.getCountX(); + n = iconLayout.getCountY(); + } + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + matrix[cx][cy] = i; + } + + // Iterate thru the children of the bottom parent + // The hotseat view group contains one more item than iconLayout column count. + // If {@param allappsiconRank} not negative, then the last icon in the hotseat + // is truncated. If it is negative, then all apps icon index is not inserted. + for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) { + int delta = 0; + if (isHorizontal) { + int cx = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellX; + if ((includeAllappsicon && cx >= allappsiconRank) || + (!includeAllappsicon && cx > allappsiconRank)) { + delta = -1; + } + matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i; + } else { + int cy = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellY; + if ((includeAllappsicon && cy >= allappsiconRank) || + (!includeAllappsicon && cy > allappsiconRank)) { + delta = -1; + } + matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i; + } + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon of previous/next page and last column of + * current page. When left key is triggered on the leftmost column, sparse matrix is created + * that combines previous page matrix and an extra column on the right. Likewise, when right + * key is triggered on the rightmost column, sparse matrix is created that combines this column + * on the 0th column and the next page matrix. + * + * @param pivotX x coordinate of the focused item in the current page + * @param pivotY y coordinate of the focused item in the current page + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY()); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + if (pivotX < 0) { + matrix[cx - pivotX][cy] = i; + } else { + matrix[cx][cy] = i; + } + } + + if (pivotX < 0) { + matrix[0][pivotY] = PIVOT; + } else { + matrix[pivotX][pivotY] = PIVOT; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + // + // key event handling methods. + // + + /** + * Calculates icon that has is closest to the horizontal axis in reference to the cur icon. + * + * Example of the check order for KEYCODE_DPAD_RIGHT: + * [ ][ ][13][14][15] + * [ ][ 6][ 8][10][12] + * [ X][ 1][ 2][ 3][ 4] + * [ ][ 5][ 7][ 9][11] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, + int[][] matrix, int increment) { + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + int newIconIndex = NOOP; + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i < cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIdx) { + xPos = i; + yPos = j; + } + } + } + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIdx)); + } + + // Rule1: check first in the horizontal direction + for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment) + // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) + int nextYPos1; + int nextYPos2; + int i = -1; + for (int coeff = 1; coeff < cntY; coeff++) { + nextYPos1 = yPos + coeff * increment; + nextYPos2 = yPos - coeff * increment; + for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + /** + * Calculates icon that is closest to the vertical axis in reference to the current icon. + * + * Example of the check order for KEYCODE_DPAD_DOWN: + * [ ][ ][ ][ X][ ][ ][ ] + * [ ][ ][ 5][ 1][ 4][ ][ ] + * [ ][10][ 7][ 2][ 6][ 9][ ] + * [14][12][ 9][ 3][ 8][11][13] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadVertical(int iconIndex, int cntX, int cntY, + int [][] matrix, int increment) { + int newIconIndex = NOOP; + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i< cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIndex) { + xPos = i; + yPos = j; + } + } + } + + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIndex)); + } + + // Rule1: check first in the dpad direction + for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) { + if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (xPos + increment, y_(1-n)), (xPos - increment, y_(1-n)) + // (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n)) + int nextXPos1; + int nextXPos2; + int j = -1; + for (int coeff = 1; coeff < cntX; coeff++) { + nextXPos1 = xPos + coeff * increment; + nextXPos2 = xPos - coeff * increment; + for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) { + if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + private static int handleMoveHome() { + return CURRENT_PAGE_FIRST_ITEM; + } + + private static int handleMoveEnd() { + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageDown(int pageIndex, int pageCount) { + if (pageIndex < pageCount -1) { + return NEXT_PAGE_FIRST_ITEM; + } + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageUp(int pageIndex) { + if (pageIndex > 0) { + return PREVIOUS_PAGE_FIRST_ITEM; + } else { + return CURRENT_PAGE_FIRST_ITEM; + } + } + + // + // Helper methods. + // + + private static boolean isValid(int xPos, int yPos, int countX, int countY) { + return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY); + } + + private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) { + int newIconIndex = NOOP; + if (isValid(x, y, cntX, cntY)) { + if (matrix[x][y] != -1) { + newIconIndex = matrix[x][y]; + if (DEBUG) { + Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d", + x, y, matrix[x][y])); + } + return newIconIndex; + } + } + return newIconIndex; + } + + /** + * Only used for debugging. + */ + private static String getStringIndex(int index) { + switch(index) { + case NOOP: return "NOOP"; + case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST"; + case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST"; + case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN"; + case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST"; + case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST"; + case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST"; + case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN"; + default: + return Integer.toString(index); + } + } + + /** + * Only used for debugging. + */ + private static void printMatrix(int[][] matrix) { + Log.v(TAG, "\tprintMap:"); + int m = matrix.length; + int n = matrix[0].length; + + for (int j=0; j < n; j++) { + String colY = "\t\t"; + for (int i=0; i < m; i++) { + colY += String.format("%3d",matrix[i][j]); + } + Log.v(TAG, colY); + } + } + + /** + * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or + * {@link #NEXT_PAGE_RIGHT_COLUMN} + * @return the view adjacent to {@param oldView} in the {@param nextPage}. + */ + public static View getAdjacentChildInNextPage( + ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) { + final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY; + + int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally() + ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1); + + for (; column >= 0; column--) { + for (int row = newRow; row >= 0; row--) { + View newView = nextPage.getChildAt(column, row); + if (newView != null) { + return newView; + } + } + } + return null; + } +} diff --git a/src/com/android/launcher3/util/LauncherEdgeEffect.java b/src/com/android/launcher3/util/LauncherEdgeEffect.java new file mode 100644 index 000000000..3e3b255ad --- /dev/null +++ b/src/com/android/launcher3/util/LauncherEdgeEffect.java @@ -0,0 +1,365 @@ +/* + * 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.util; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +/** + * This class differs from the framework {@link android.widget.EdgeEffect}: + * 1) It does not use PorterDuffXfermode + * 2) The width to radius factor is smaller (0.5 instead of 0.75) + */ +public class LauncherEdgeEffect { + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 600; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 2000; + + private static final float MAX_ALPHA = 0.5f; + + private static final float MAX_GLOW_SCALE = 2.f; + + private static final float PULL_GLOW_BEGIN = 0.f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + // Maximum velocity, clamps at this value + private static final int MAX_VELOCITY = 10000; + + private static final float EPSILON = 0.001f; + + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + + private float mGlowAlpha; + private float mGlowScaleY; + + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_GLOW_FACTOR = 6; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + private final Rect mBounds = new Rect(); + private final Paint mPaint = new Paint(); + private float mRadius; + private float mBaseGlowScale; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + */ + public LauncherEdgeEffect() { + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + final float r = width * 0.5f / SIN; + final float y = COS * r; + final float h = r - y; + final float or = height * 0.75f / SIN; + final float oy = COS * or; + final float oh = or - oy; + + mRadius = r; + mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; + + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.</p> + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + + final float absdd = Math.abs(deltaDistance); + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + if (mPullDistance == 0) { + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = (float) (Math.max(0, 1 - 1 / + Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d); + + mGlowScaleY = mGlowScaleYStart = scale; + } + + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method <code>getCurrVelocity</code> will provide a reasonable approximation + * to use here.</p> + * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.15f + (velocity * 0.02f); + + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.3f; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); + + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; + } + + /** + * Set the color of this edge effect in argb. + * + * @param color Color in argb + */ + public void setColor(int color) { + mPaint.setColor(color); + } + + /** + * Return the color of this edge effect in argb. + * @return The color of this edge effect in argb + */ + public int getColor() { + return mPaint.getColor(); + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(Canvas canvas) { + update(); + + final float centerX = mBounds.centerX(); + final float centerY = mBounds.height() - mRadius; + + canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0); + + final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; + float translateX = mBounds.width() * displacement / 2; + mPaint.setAlpha((int) (0xff * mGlowAlpha)); + canvas.drawCircle(centerX + translateX, centerY, mRadius, mPaint); + + boolean oneLastFrame = false; + if (mState == STATE_RECEDE && mGlowScaleY == 0) { + mState = STATE_IDLE; + oneLastFrame = true; + } + + return mState != STATE_IDLE || oneLastFrame; + } + + /** + * Return the maximum height that the edge effect will be drawn at given the original + * {@link #setSize(int, int) input size}. + * @return The maximum height of the edge effect + */ + public int getMaxHeight() { + return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f); + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + mDisplacement = (mDisplacement + mTargetDisplacement) / 2; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } +} diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/LongArrayMap.java new file mode 100644 index 000000000..a337e85bd --- /dev/null +++ b/src/com/android/launcher3/util/LongArrayMap.java @@ -0,0 +1,65 @@ +/* + * 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.util; + +import android.util.LongSparseArray; + +import java.util.Iterator; + +/** + * Extension of {@link LongSparseArray} with some utility methods. + */ +public class LongArrayMap<E> extends LongSparseArray<E> implements Iterable<E> { + + public boolean containsKey(long key) { + return indexOfKey(key) >= 0; + } + + public boolean isEmpty() { + return size() <= 0; + } + + @Override + public LongArrayMap<E> clone() { + return (LongArrayMap<E>) super.clone(); + } + + @Override + public Iterator<E> iterator() { + return new ValueIterator(); + } + + @Thunk class ValueIterator implements Iterator<E> { + + private int mNextIndex = 0; + + @Override + public boolean hasNext() { + return mNextIndex < size(); + } + + @Override + public E next() { + return valueAt(mNextIndex ++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java new file mode 100644 index 000000000..b37f44719 --- /dev/null +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -0,0 +1,350 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.util.Log; + +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Handles addition of app shortcuts for managed profiles. + * Methods of class should only be called on {@link LauncherModel#sWorkerThread}. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class ManagedProfileHeuristic { + + private static final String TAG = "ManagedProfileHeuristic"; + + /** + * Maintain a set of packages installed per user. + */ + private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_"; + + private static final String USER_FOLDER_ID_PREFIX = "user_folder_"; + + /** + * Duration (in milliseconds) for which app shortcuts will be added to work folder. + */ + private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000; + + public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) { + if (Utilities.isLmpOrAbove() && !UserHandleCompat.myUserHandle().equals(user)) { + return new ManagedProfileHeuristic(context, user); + } + return null; + } + + private final Context mContext; + private final UserHandleCompat mUser; + private final LauncherModel mModel; + + private final SharedPreferences mPrefs; + private final long mUserSerial; + private final long mUserCreationTime; + private final String mPackageSetKey; + + private ArrayList<ShortcutInfo> mHomescreenApps; + private ArrayList<ShortcutInfo> mWorkFolderApps; + + private ManagedProfileHeuristic(Context context, UserHandleCompat user) { + mContext = context; + mUser = user; + mModel = LauncherAppState.getInstance().getModel(); + + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + mUserSerial = userManager.getSerialNumberForUser(user); + mUserCreationTime = userManager.getUserCreationTime(user); + mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial; + + mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + + /** + * Checks the list of user apps and adds icons for newly installed apps on the homescreen or + * workfolder. + */ + public void processUserApps(List<LauncherActivityInfoCompat> apps) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet<String> packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + + for (LauncherActivityInfoCompat info : apps) { + String packageName = info.getComponentName().getPackageName(); + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + try { + PackageInfo pkgInfo = mContext.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + markForAddition(info, pkgInfo.firstInstallTime); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unknown package " + packageName, e); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + // Do not add shortcuts on the homescreen for the first time. This prevents the launcher + // getting filled with the managed user apps, when it start with a fresh DB (or after + // a very long time). + finalizeAdditions(userAppsExisted); + } + } + + private void markForAddition(LauncherActivityInfoCompat info, long installTime) { + ArrayList<ShortcutInfo> targetList = + (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ? + mWorkFolderApps : mHomescreenApps; + targetList.add(ShortcutInfo.fromActivityInfo(info, mContext)); + } + + /** + * Adds and binds shortcuts marked to be added to the work folder. + */ + private void finalizeWorkFolder() { + if (mWorkFolderApps.isEmpty()) { + return; + } + Collections.sort(mWorkFolderApps, new Comparator<ShortcutInfo>() { + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + return Long.compare(lhs.firstInstallTime, rhs.firstInstallTime); + } + }); + + // Try to get a work folder. + String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial; + if (mPrefs.contains(folderIdKey)) { + long folderId = mPrefs.getLong(folderIdKey, 0); + final FolderInfo workFolder = mModel.findFolderById(folderId); + + if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) { + // Could not get a work folder. Add all the icons to homescreen. + mHomescreenApps.addAll(mWorkFolderApps); + return; + } + saveWorkFolderShortcuts(folderId, workFolder.contents.size()); + + final ArrayList<ShortcutInfo> shortcuts = mWorkFolderApps; + // FolderInfo could already be bound. We need to add shortcuts on the UI thread. + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + for (ShortcutInfo info : shortcuts) { + workFolder.add(info); + } + } + }); + } else { + // Create a new folder. + final FolderInfo workFolder = new FolderInfo(); + workFolder.title = mContext.getText(R.string.work_folder_name); + workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null); + + // Add all shortcuts before adding it to the UI, as an empty folder might get deleted. + for (ShortcutInfo info : mWorkFolderApps) { + workFolder.add(info); + } + + // Add the item to home screen and DB. This also generates an item id synchronously. + ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1); + itemList.add(workFolder); + mModel.addAndBindAddedWorkspaceItems(mContext, itemList); + mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply(); + + saveWorkFolderShortcuts(workFolder.id, 0); + } + } + + /** + * Add work folder shortcuts to the DB. + */ + private void saveWorkFolderShortcuts(long workFolderId, int startingRank) { + for (ItemInfo info : mWorkFolderApps) { + info.rank = startingRank++; + LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0); + } + } + + /** + * Adds and binds all shortcuts marked for addition. + */ + private void finalizeAdditions(boolean addHomeScreenShortcuts) { + finalizeWorkFolder(); + + if (addHomeScreenShortcuts && !mHomescreenApps.isEmpty()) { + mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps); + } + } + + /** + * Updates the list of installed apps and adds any new icons on homescreen or work folder. + */ + public void processPackageAdd(String[] packages) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet<String> packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + long installTime = System.currentTimeMillis(); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext); + + for (String packageName : packages) { + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + List<LauncherActivityInfoCompat> activities = + launcherApps.getActivityList(packageName, mUser); + if (!activities.isEmpty()) { + markForAddition(activities.get(0), installTime); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + finalizeAdditions(userAppsExisted); + } + } + + /** + * Updates the list of installed packages for the user. + */ + public void processPackageRemoved(String[] packages) { + HashSet<String> packageSet = new HashSet<String>(); + getUserApps(packageSet); + boolean packageRemoved = false; + + for (String packageName : packages) { + if (packageSet.remove(packageName)) { + packageRemoved = true; + } + } + + if (packageRemoved) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + } + } + + /** + * Reads the list of user apps which have already been processed. + * @return false if the list didn't exist, true otherwise + */ + private boolean getUserApps(HashSet<String> outExistingApps) { + Set<String> userApps = mPrefs.getStringSet(mPackageSetKey, null); + if (userApps == null) { + return false; + } else { + outExistingApps.addAll(userApps); + return true; + } + } + + /** + * Verifies that entries corresponding to {@param users} exist and removes all invalid entries. + */ + public static void processAllUsers(List<UserHandleCompat> users, Context context) { + if (!Utilities.isLmpOrAbove()) { + return; + } + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + HashSet<String> validKeys = new HashSet<String>(); + for (UserHandleCompat user : users) { + addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys); + } + + SharedPreferences prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + for (String key : prefs.getAll().keySet()) { + if (!validKeys.contains(key)) { + editor.remove(key); + } + } + editor.apply(); + } + + private static void addAllUserKeys(long userSerial, HashSet<String> keysOut) { + keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial); + keysOut.add(USER_FOLDER_ID_PREFIX + userSerial); + } + + /** + * For each user, if a work folder has not been created, mark it such that the folder will + * never get created. + */ + public static void markExistingUsersForNoFolderCreation(Context context) { + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + + SharedPreferences prefs = null; + for (UserHandleCompat user : userManager.getUserProfiles()) { + if (myUser.equals(user)) { + continue; + } + + if (prefs == null) { + prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + String folderIdKey = USER_FOLDER_ID_PREFIX + userManager.getSerialNumberForUser(user); + if (!prefs.contains(folderIdKey)) { + prefs.edit().putLong(folderIdKey, ItemInfo.NO_ID).apply(); + } + } + } +} diff --git a/src/com/android/launcher3/util/RevealOutlineProvider.java b/src/com/android/launcher3/util/RevealOutlineProvider.java new file mode 100644 index 000000000..0db3984f8 --- /dev/null +++ b/src/com/android/launcher3/util/RevealOutlineProvider.java @@ -0,0 +1,49 @@ +package com.android.launcher3.util; + +import android.annotation.TargetApi; +import android.graphics.Outline; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RevealOutlineProvider extends ViewOutlineProvider { + + private int mCenterX; + private int mCenterY; + private float mRadius0; + private float mRadius1; + private int mCurrentRadius; + + private final Rect mOval; + + /** + * @param x reveal center x + * @param y reveal center y + * @param r0 initial radius + * @param r1 final radius + */ + public RevealOutlineProvider(int x, int y, float r0, float r1) { + mCenterX = x; + mCenterY = y; + mRadius0 = r0; + mRadius1 = r1; + + mOval = new Rect(); + } + + public void setProgress(float progress) { + mCurrentRadius = (int) ((1 - progress) * mRadius0 + progress * mRadius1); + + mOval.left = mCenterX - mCurrentRadius; + mOval.top = mCenterY - mCurrentRadius; + mOval.right = mCenterX + mCurrentRadius; + mOval.bottom = mCenterY + mCurrentRadius; + } + + @Override + public void getOutline(View v, Outline outline) { + outline.setRoundRect(mOval, mCurrentRadius); + } +} diff --git a/src/com/android/launcher3/util/Thunk.java b/src/com/android/launcher3/util/Thunk.java new file mode 100644 index 000000000..de350b068 --- /dev/null +++ b/src/com/android/launcher3/util/Thunk.java @@ -0,0 +1,43 @@ +/* + * 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.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the given field or method has package visibility solely to prevent the creation + * of a synthetic method. In practice, you should treat this field/method as if it were private. + * <p> + * + * When a private method is called from an inner class, the Java compiler generates a simple + * package private shim method that the class generated from the inner class can call. This results + * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method + * count limit. + * <p> + * + * If you'd like to see warnings for these synthetic methods in eclipse, turn on: + * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member + * of an enclosing type". + * <p> + * + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Thunk { }
\ No newline at end of file diff --git a/src/com/android/launcher3/util/UiThreadCircularReveal.java b/src/com/android/launcher3/util/UiThreadCircularReveal.java new file mode 100644 index 000000000..3ca3aeeee --- /dev/null +++ b/src/com/android/launcher3/util/UiThreadCircularReveal.java @@ -0,0 +1,57 @@ +package com.android.launcher3.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.android.launcher3.Utilities; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class UiThreadCircularReveal { + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1) { + return createCircularReveal(v, x, y, r0, r1, ViewOutlineProvider.BACKGROUND); + } + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1, + final ViewOutlineProvider originalProvider) { + ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + + final View revealView = v; + final RevealOutlineProvider outlineProvider = new RevealOutlineProvider(x, y, r0, r1); + final float elevation = v.getElevation(); + + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + revealView.setOutlineProvider(outlineProvider); + revealView.setClipToOutline(true); + revealView.setTranslationZ(-elevation); + } + + public void onAnimationEnd(Animator animation) { + revealView.setOutlineProvider(originalProvider); + revealView.setClipToOutline(false); + revealView.setTranslationZ(0); + } + + }); + + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator arg0) { + float progress = arg0.getAnimatedFraction(); + outlineProvider.setProgress(progress); + revealView.invalidateOutline(); + if (!Utilities.isLmpMR1OrAbove()) { + revealView.invalidate(); + } + } + }); + return va; + } +} diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java new file mode 100644 index 000000000..53b2acd84 --- /dev/null +++ b/src/com/android/launcher3/util/WallpaperUtils.java @@ -0,0 +1,123 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.app.WallpaperManager; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.view.WindowManager; + +/** + * Utility methods for wallpaper management. + */ +public final class WallpaperUtils { + + public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + public static final float WALLPAPER_SCREENS_SPAN = 2f; + + public static void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { + final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); + + if (savedWidth == -1 || savedHeight == -1) { + if (!fallBackToDefaults) { + return; + } else { + savedWidth = defaultWallpaperSize.x; + savedHeight = defaultWallpaperSize.y; + } + } + + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + public static float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + private static Point sDefaultWallpaperSize; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { + if (sDefaultWallpaperSize == null) { + Point minDims = new Point(); + Point maxDims = new Point(); + windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + int maxDim = Math.max(maxDims.x, maxDims.y); + int minDim = Math.max(minDims.x, minDims.y); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + Point realSize = new Point(); + windowManager.getDefaultDisplay().getRealSize(realSize); + maxDim = Math.max(realSize.x, realSize.y); + minDim = Math.min(realSize.x, realSize.y); + } + + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + final int defaultWidth, defaultHeight; + if (res.getConfiguration().smallestScreenWidthDp >= 720) { + defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + defaultHeight = maxDim; + } else { + defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; + } +} diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java new file mode 100644 index 000000000..a56985083 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java @@ -0,0 +1,44 @@ +/* + * 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.widget; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; + +/** + * Meta data used for late binding of the short cuts. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddShortcutInfo extends PendingAddItemInfo { + + ActivityInfo activityInfo; + + public PendingAddShortcutInfo(ActivityInfo activityInfo) { + this.activityInfo = activityInfo; + componentName = new ComponentName(activityInfo.packageName, activityInfo.name); + itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + } + + @Override + public String toString() { + return String.format("PendingAddShortcutInfo package=%s, name=%s", + activityInfo.packageName, activityInfo.name); + } +} diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java new file mode 100644 index 000000000..758287af3 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -0,0 +1,75 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetHostView; +import android.os.Bundle; +import android.os.Parcelable; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddWidgetInfo extends PendingAddItemInfo { + public int minWidth; + public int minHeight; + public int minResizeWidth; + public int minResizeHeight; + public int previewImage; + public int icon; + public LauncherAppWidgetProviderInfo info; + public AppWidgetHostView boundWidget; + public Bundle bindOptions = null; + + public PendingAddWidgetInfo(Launcher launcher, LauncherAppWidgetProviderInfo i, Parcelable data) { + if (i.isCustomWidget) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.info = i; + user = AppWidgetManagerCompat.getInstance(launcher).getUser(i); + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; + previewImage = i.previewImage; + icon = i.icon; + + spanX = i.getSpanX(launcher); + spanY = i.getSpanY(launcher); + minSpanX = i.getMinSpanX(launcher); + minSpanY = i.getMinSpanY(launcher); + } + + public boolean isCustomWidget() { + return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } + + @Override + public String toString() { + return String.format("PendingAddWidgetInfo package=%s, name=%s", + componentName.getPackageName(), componentName.getShortClassName()); + } +} diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java new file mode 100644 index 000000000..7496ea2ef --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -0,0 +1,230 @@ +/* + * 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.widget; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewPropertyAnimator; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.StylusEventHelper; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Represents the individual cell of the widget inside the widget tray. The preview is drawn + * horizontally centered, and scaled down if needed. + * + * This view does not support padding. Since the image is scaled down to fit the view, padding will + * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth + * transition from the view to drag view, so when adding padding support, DnD would need to + * consider the appropriate scaling factor. + */ +public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { + + private static final String TAG = "WidgetCell"; + private static final boolean DEBUG = false; + + private static final int FADE_IN_DURATION_MS = 90; + + /** Widget cell width is calculated by multiplying this factor to grid cell width. */ + private static final float WIDTH_SCALE = 2.6f; + + /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ + private static final float PREVIEW_SCALE = 0.8f; + + private int mPresetPreviewSize; + int cellSize; + + private WidgetImageView mWidgetImage; + private TextView mWidgetName; + private TextView mWidgetDims; + + private String mDimensionsFormatString; + private Object mInfo; + + private WidgetPreviewLoader mWidgetPreviewLoader; + private PreviewLoadRequest mActiveRequest; + private StylusEventHelper mStylusEventHelper; + + private Launcher mLauncher; + + public WidgetCell(Context context) { + this(context, null); + } + + public WidgetCell(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetCell(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final Resources r = context.getResources(); + mLauncher = (Launcher) context; + mStylusEventHelper = new StylusEventHelper(this); + + mDimensionsFormatString = r.getString(R.string.widget_dims_format); + setContainerWidth(); + setWillNotDraw(false); + setClipToPadding(false); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } + + private void setContainerWidth() { + DeviceProfile profile = mLauncher.getDeviceProfile(); + cellSize = (int) (profile.cellWidthPx * WIDTH_SCALE); + mPresetPreviewSize = (int) (cellSize * PREVIEW_SCALE); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview); + mWidgetName = ((TextView) findViewById(R.id.widget_name)); + mWidgetDims = ((TextView) findViewById(R.id.widget_dims)); + } + + /** + * Called to clear the view and free attached resources. (e.g., {@link Bitmap} + */ + public void clear() { + if (DEBUG) { + Log.d(TAG, "reset called on:" + mWidgetName.getText()); + } + mWidgetImage.animate().cancel(); + mWidgetImage.setBitmap(null); + mWidgetName.setText(null); + mWidgetDims.setText(null); + + if (mActiveRequest != null) { + mActiveRequest.cleanup(); + mActiveRequest = null; + } + } + + /** + * Apply the widget provider info to the view. + */ + public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info, + WidgetPreviewLoader loader) { + + InvariantDeviceProfile profile = + LauncherAppState.getInstance().getInvariantDeviceProfile(); + mInfo = info; + // TODO(hyunyoungs): setup a cache for these labels. + mWidgetName.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); + int hSpan = Math.min(info.getSpanX(mLauncher), profile.numColumns); + int vSpan = Math.min(info.getSpanY(mLauncher), profile.numRows); + mWidgetDims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); + mWidgetPreviewLoader = loader; + } + + /** + * Apply the resolve info to the view. + */ + public void applyFromResolveInfo( + PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { + mInfo = info; + CharSequence label = info.loadLabel(pm); + mWidgetName.setText(label); + mWidgetDims.setText(String.format(mDimensionsFormatString, 1, 1)); + mWidgetPreviewLoader = loader; + } + + public int[] getPreviewSize() { + int[] maxSize = new int[2]; + + maxSize[0] = mPresetPreviewSize; + maxSize[1] = mPresetPreviewSize; + return maxSize; + } + + public void applyPreview(Bitmap bitmap) { + if (bitmap != null) { + mWidgetImage.setBitmap(bitmap); + mWidgetImage.setAlpha(0f); + ViewPropertyAnimator anim = mWidgetImage.animate(); + anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); + } + } + + public void ensurePreview() { + if (mActiveRequest != null) { + return; + } + int[] size = getPreviewSize(); + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):", + getTagToString(), size[0], size[1])); + } + mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + removeOnLayoutChangeListener(this); + ensurePreview(); + } + + public int getActualItemWidth() { + ItemInfo info = (ItemInfo) getTag(); + int[] size = getPreviewSize(); + int cellWidth = mLauncher.getDeviceProfile().cellWidthPx; + + return Math.min(size[0], info.spanX * cellWidth); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = super.onTouchEvent(ev); + if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + return true; + } + return handled; + } + + /** + * Helper method to get the string info of the tag. + */ + private String getTagToString() { + if (getTag() instanceof PendingAddWidgetInfo || + getTag() instanceof PendingAddShortcutInfo) { + return getTag().toString(); + } + return ""; + } +} diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java new file mode 100644 index 000000000..30b3d581a --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -0,0 +1,155 @@ +package com.android.launcher3.widget; + +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; + +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragSource; +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 implements DragListener { + + /* Runnables to handle inflation and binding. */ + @Thunk Runnable mInflateWidgetRunnable = null; + private Runnable mBindWidgetRunnable = null; + + // TODO: technically, this class should not have to know the existence of the launcher. + @Thunk Launcher mLauncher; + @Thunk Handler mHandler; + @Thunk final View mView; + @Thunk final PendingAddWidgetInfo mInfo; + + // Widget id generated for binding a widget host view or -1 for invalid id. The id is + // not is use as long as it is stored here and can be deleted safely. Once its used, this value + // to be set back to -1. + @Thunk int mWidgetLoadingId = -1; + + public WidgetHostViewLoader(Launcher launcher, View view) { + mLauncher = launcher; + mHandler = new Handler(); + mView = view; + mInfo = (PendingAddWidgetInfo) view.getTag(); + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + // Cleanup up preloading state. + mLauncher.getDragController().removeDragListener(this); + + mHandler.removeCallbacks(mBindWidgetRunnable); + mHandler.removeCallbacks(mInflateWidgetRunnable); + + // Cleanup widget id + if (mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + mWidgetLoadingId = -1; + } + + // The widget was inflated and added to the DragLayer -- remove it. + if (mInfo.boundWidget != null) { + mLauncher.getDragLayer().removeView(mInfo.boundWidget); + mLauncher.getAppWidgetHost().deleteAppWidgetId(mInfo.boundWidget.getAppWidgetId()); + mInfo.boundWidget = null; + } + } + + /** + * Start preloading the widget. + */ + public boolean preloadWidget() { + final LauncherAppWidgetProviderInfo pInfo = mInfo.info; + + if (pInfo.isCustomWidget) { + return false; + } + final Bundle options = getDefaultOptionsForWidget(mLauncher, mInfo); + + // If there is a configuration activity, do not follow thru bound and inflate. + if (pInfo.configure != null) { + mInfo.bindOptions = options; + return false; + } + + mBindWidgetRunnable = new Runnable() { + @Override + public void run() { + mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, pInfo, options)) { + + // Widget id bound. Inflate the widget. + mHandler.post(mInflateWidgetRunnable); + } + } + }; + + mInflateWidgetRunnable = new Runnable() { + @Override + public void run() { + if (mWidgetLoadingId == -1) { + return; + } + AppWidgetHostView hostView = mLauncher.getAppWidgetHost().createView( + (Context) mLauncher, mWidgetLoadingId, pInfo); + mInfo.boundWidget = hostView; + + // We used up the widget Id in binding the above view. + mWidgetLoadingId = -1; + + hostView.setVisibility(View.INVISIBLE); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(mInfo, false); + // We want the first widget layout to be the correct size. This will be important + // for width size reporting to the AppWidgetManager. + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], + unScaledSize[1]); + lp.x = lp.y = 0; + lp.customPosition = true; + hostView.setLayoutParams(lp); + mLauncher.getDragLayer().addView(hostView); + mView.setTag(mInfo); + } + }; + + mHandler.post(mBindWidgetRunnable); + return true; + } + + public static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + Bundle options = null; + Rect rect = new Rect(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, rect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, + info.componentName, null); + + float density = launcher.getResources().getDisplayMetrics().density; + int xPaddingDips = (int) ((padding.left + padding.right) / density); + int yPaddingDips = (int) ((padding.top + padding.bottom) / density); + + options = new Bundle(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + rect.left - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + rect.top - yPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + rect.right - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + rect.bottom - yPaddingDips); + } + return options; + } +} diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java new file mode 100644 index 000000000..b0fbe1ed9 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +/** + * View that draws a bitmap horizontally centered. If the image width is greater than the view + * width, the image is scaled down appropriately. + */ +public class WidgetImageView extends View { + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private final RectF mDstRectF = new RectF(); + private Bitmap mBitmap; + + public WidgetImageView(Context context) { + super(context); + } + + public WidgetImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WidgetImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + invalidate(); + } + + public Bitmap getBitmap() { + return mBitmap; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + updateDstRectF(); + canvas.drawBitmap(mBitmap, null, mDstRectF, mPaint); + } + } + + /** + * Prevents the inefficient alpha view rendering. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private void updateDstRectF() { + if (mBitmap.getWidth() > getWidth()) { + float scale = ((float) getWidth()) / mBitmap.getWidth(); + mDstRectF.set(0, 0, getWidth(), scale * mBitmap.getHeight()); + } else { + mDstRectF.set( + (getWidth() - mBitmap.getWidth()) * 0.5f, + 0, + (getWidth() + mBitmap.getWidth()) * 0.5f, + mBitmap.getHeight()); + } + } + + /** + * @return the bounds where the image was drawn. + */ + public Rect getBitmapBounds() { + updateDstRectF(); + Rect rect = new Rect(); + mDstRectF.round(rect); + return rect; + } +} diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java new file mode 100644 index 000000000..5afd7c493 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -0,0 +1,370 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView.State; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.android.launcher3.BaseContainerView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DragController; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.Folder; +import com.android.launcher3.IconCache; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.R; +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. + */ +public class WidgetsContainerView extends BaseContainerView + implements View.OnLongClickListener, View.OnClickListener, DragSource{ + + private static final String TAG = "WidgetsContainerView"; + private static final boolean DEBUG = false; + + /* Coefficient multiplied to the screen height for preloading widgets. */ + private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1; + + /* Global instances that are used inside this container. */ + @Thunk Launcher mLauncher; + private DragController mDragController; + private IconCache mIconCache; + + /* Recycler view related member variables */ + private View mContent; + private WidgetsRecyclerView mView; + private WidgetsListAdapter mAdapter; + + /* Touch handling related member variables. */ + private Toast mWidgetInstructionToast; + + /* Rendering related. */ + private WidgetPreviewLoader mWidgetPreviewLoader; + + private Rect mPadding = new Rect(); + + public WidgetsContainerView(Context context) { + this(context, null); + } + + public WidgetsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = (Launcher) context; + mDragController = mLauncher.getDragController(); + mAdapter = new WidgetsListAdapter(context, this, this, mLauncher); + mIconCache = (LauncherAppState.getInstance()).getIconCache(); + if (DEBUG) { + Log.d(TAG, "WidgetsContainerView constructor"); + } + } + + @Override + protected void onFinishInflate() { + mContent = findViewById(R.id.content); + mView = (WidgetsRecyclerView) findViewById(R.id.widgets_list_view); + mView.setAdapter(mAdapter); + + // This extends the layout space so that preloading happen for the {@link RecyclerView} + mView.setLayoutManager(new LinearLayoutManager(getContext()) { + @Override + protected int getExtraLayoutSpace(State state) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + return super.getExtraLayoutSpace(state) + + grid.availableHeightPx * PRELOAD_SCREEN_HEIGHT_MULTIPLE; + } + }); + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); + } + + // + // Returns views used for launcher transitions. + // + + public View getContentView() { + return mView; + } + + public View getRevealView() { + // TODO(hyunyoungs): temporarily use apps view transition. + return findViewById(R.id.widgets_reveal_view); + } + + public void scrollToTop() { + mView.scrollToPosition(0); + } + + // + // Touch related handling. + // + + @Override + public void onClick(View v) { + // When we have exited widget tray or are in transition, disregard clicks + if (!mLauncher.isWidgetsViewVisible() + || mLauncher.getWorkspace().isSwitchingState() + || !(v instanceof WidgetCell)) return; + + // Let the user know that they have to long press to add a widget + if (mWidgetInstructionToast != null) { + mWidgetInstructionToast.cancel(); + } + mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, + Toast.LENGTH_SHORT); + mWidgetInstructionToast.show(); + } + + @Override + public boolean onLongClick(View v) { + if (DEBUG) { + Log.d(TAG, String.format("onLonglick [v=%s]", v)); + } + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isWidgetsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + Log.d(TAG, String.format("onLonglick dragging enabled?.", v)); + if (!mLauncher.isDraggingEnabled()) return false; + + boolean status = beginDragging(v); + if (status && v.getTag() instanceof PendingAddWidgetInfo) { + WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v); + boolean preloadStatus = hostLoader.preloadWidget(); + if (DEBUG) { + Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus)); + } + mLauncher.getDragController().addDragListener(hostLoader); + } + return status; + } + + private boolean beginDragging(View v) { + if (v instanceof WidgetCell) { + if (!beginDraggingWidget((WidgetCell) v)) { + return false; + } + } else { + Log.e(TAG, "Unexpected dragging view: " + v); + } + + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + + return true; + } + + private boolean beginDraggingWidget(WidgetCell v) { + // Get the widget preview as the drag representation + WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview); + PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); + + // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and + // we abort the drag. + if (image.getBitmap() == null) { + return false; + } + + // Compose the drag image + Bitmap preview; + float scale = 1f; + final Rect bounds = image.getBitmapBounds(); + + if (createItemInfo instanceof PendingAddWidgetInfo) { + // This can happen in some weird cases involving multi-touch. We can't start dragging + // the widget if this is null, so we break out. + + PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; + int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true); + + Bitmap icon = image.getBitmap(); + float minScale = 1.25f; + int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]); + + int[] previewSizeBeforeScale = new int[1]; + preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher, + createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale); + + if (previewSizeBeforeScale[0] < icon.getWidth()) { + // The icon has extra padding around it. + int padding = (icon.getWidth() - previewSizeBeforeScale[0]) / 2; + if (icon.getWidth() > image.getWidth()) { + padding = padding * image.getWidth() / icon.getWidth(); + } + + bounds.left += padding; + bounds.right -= padding; + } + scale = bounds.width() / (float) preview.getWidth(); + } else { + PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); + Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); + preview = Utilities.createIconBitmap(icon, mLauncher); + createItemInfo.spanX = createItemInfo.spanY = 1; + scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth(); + } + + // Don't clip alpha values for the drag outline if we're using the default widget preview + boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && + (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); + + // Start the drag + mLauncher.lockScreenOrientation(); + mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, preview, clipAlpha); + mDragController.startDrag(image, preview, this, createItemInfo, + bounds, DragController.DRAG_ACTION_COPY, scale); + + preview.recycle(); + return true; + } + + // + // Drag related handling methods that implement {@link DragSource} interface. + // + + @Override + public boolean supportsFlingToDelete() { + return false; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + /* + * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the + * {@link DeleteDropTarget} to be invisible.) + */ + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return 0; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + d.deferDragViewCleanupPostAnimation = false; + } + } + + // + // Container rendering related. + // + + @Override + protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + // Apply the top-bottom padding to the content itself so that the launcher transition is + // clipped correctly + mContent.setPadding(0, padding.top, 0, padding.bottom); + + // TODO: Use quantum_panel_dark instead of quantum_panel_shape_dark. + InsetDrawable background = new InsetDrawable( + getResources().getDrawable(R.drawable.quantum_panel_shape_dark), padding.left, 0, + padding.right, 0); + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + mView.setBackground(background); + getRevealView().setBackground(background.getConstantState().newDrawable()); + mView.updateBackgroundPadding(bgPadding); + } + + /** + * Initialize the widget data model. + */ + public void addWidgets(WidgetsModel model) { + mView.setWidgets(model); + mAdapter.setWidgetsModel(model); + mAdapter.notifyDataSetChanged(); + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java new file mode 100644 index 000000000..d2ea25230 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -0,0 +1,215 @@ +/* + * 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.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.LinearLayout; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +import java.util.List; + +/** + * List view adapter for the widget tray. + * + * <p>Memory vs. Performance: + * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling + * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is + * only a single type of view. + */ +public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { + + private static final String TAG = "WidgetsListAdapter"; + private static final boolean DEBUG = false; + + private Launcher mLauncher; + private LayoutInflater mLayoutInflater; + + private WidgetsModel mWidgetsModel; + private WidgetPreviewLoader mWidgetPreviewLoader; + + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + + private static final int PRESET_INDENT_SIZE_TABLET = 56; + private int mIndent = 0; + + public WidgetsListAdapter(Context context, + View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener, + Launcher launcher) { + mLayoutInflater = LayoutInflater.from(context); + + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mLauncher = launcher; + + setContainerHeight(); + } + + public void setWidgetsModel(WidgetsModel w) { + mWidgetsModel = w; + } + + @Override + public int getItemCount() { + return mWidgetsModel.getPackageSize(); + } + + @Override + public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) { + List<Object> infoList = mWidgetsModel.getSortedWidgets(pos); + + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + if (DEBUG) { + Log.d(TAG, String.format( + "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]", + pos, infoList.size(), row.getChildCount())); + } + + // Add more views. + // if there are too many, hide them. + int diff = infoList.size() - row.getChildCount(); + + if (diff > 0) { + for (int i = 0; i < diff; i++) { + WidgetCell widget = (WidgetCell) mLayoutInflater.inflate( + R.layout.widget_cell, row, false); + + // set up touch. + widget.setOnClickListener(mIconClickListener); + widget.setOnLongClickListener(mIconLongClickListener); + LayoutParams lp = widget.getLayoutParams(); + lp.height = widget.cellSize; + lp.width = widget.cellSize; + widget.setLayoutParams(lp); + + row.addView(widget); + } + } else if (diff < 0) { + for (int i=infoList.size() ; i < row.getChildCount(); i++) { + row.getChildAt(i).setVisibility(View.GONE); + } + } + + // Bind the views in the application info section. + PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos); + BubbleTextView tv = ((BubbleTextView) holder.getContent().findViewById(R.id.section)); + tv.applyFromPackageItemInfo(infoOut); + + // Bind the view in the widget horizontal tray region. + if (getWidgetPreviewLoader() == null) { + return; + } + for (int i=0; i < infoList.size(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i); + PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(mLauncher, info, null); + widget.setTag(pawi); + widget.applyFromAppWidgetProviderInfo(info, mWidgetPreviewLoader); + } else if (infoList.get(i) instanceof ResolveInfo) { + ResolveInfo info = (ResolveInfo) infoList.get(i); + PendingAddShortcutInfo pasi = new PendingAddShortcutInfo(info.activityInfo); + widget.setTag(pasi); + widget.applyFromResolveInfo(mLauncher.getPackageManager(), info, mWidgetPreviewLoader); + } + widget.ensurePreview(); + widget.setVisibility(View.VISIBLE); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (DEBUG) { + Log.v(TAG, "\nonCreateViewHolder"); + } + + ViewGroup container = (ViewGroup) mLayoutInflater.inflate( + R.layout.widgets_list_row_view, parent, false); + LinearLayout cellList = (LinearLayout) container.findViewById(R.id.widgets_cell_list); + + // if the end padding is 0, then container view (horizontal scroll view) doesn't respect + // the end of the linear layout width + the start padding and doesn't allow scrolling. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + cellList.setPaddingRelative(mIndent, 0, 1, 0); + } else { + cellList.setPadding(mIndent, 0, 1, 0); + } + + return new WidgetsRowViewHolder(container); + } + + @Override + public void onViewRecycled(WidgetsRowViewHolder holder) { + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + + for (int i = 0; i < row.getChildCount(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + widget.clear(); + } + } + + public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) { + // If child views are animating, then the RecyclerView may choose not to recycle the view, + // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue + // recycling this view, and take care in onViewRecycled() to cancel any existing + // animations. + return true; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } + + private void setContainerHeight() { + Resources r = mLauncher.getResources(); + DeviceProfile profile = mLauncher.getDeviceProfile(); + if (profile.isLargeTablet || profile.isTablet) { + mIndent = Utilities.pxFromDp(PRESET_INDENT_SIZE_TABLET, r.getDisplayMetrics()); + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java new file mode 100644 index 000000000..61e63cdb7 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -0,0 +1,158 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.support.v7.widget.LinearLayoutManager; +import android.util.AttributeSet; +import android.view.View; +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +/** + * The widgets recycler view. + */ +public class WidgetsRecyclerView extends BaseRecyclerView { + + private static final String TAG = "WidgetsRecyclerView"; + private WidgetsModel mWidgets; + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + + public WidgetsRecyclerView(Context context) { + this(context, null); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + // API 21 and below only support 3 parameter ctor. + super(context, attrs, defStyleAttr); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + this(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + public int getFastScrollerTrackColor(int defaultTrackColor) { + return Color.WHITE; + } + + public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { + return getResources().getColor(R.color.widgets_view_fastscroll_thumb_inactive_color); + } + + /** + * Sets the widget model in this view, used to determine the fast scroll position. + */ + public void setWidgets(WidgetsModel widgets) { + mWidgets = widgets; + } + + /** + * We need to override the draw to ensure that we don't draw the overscroll effect beyond the + * background bounds. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + super.dispatchDraw(canvas); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + */ + @Override + public String scrollToPositionAtProgress(float touchFraction) { + int rowCount = mWidgets.getPackageSize(); + if (rowCount == 0) { + return ""; + } + + // Stop the scroller if it is scrolling + stopScroll(); + + getCurScrollState(mScrollPosState); + float pos = rowCount * touchFraction; + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + + int posInt = (int) ((touchFraction == 1)? pos -1 : pos); + PackageItemInfo p = mWidgets.getPackageItemInfo(posInt); + return p.titleSectionName; + } + + /** + * Updates the bounds for the scrollbar. + */ + @Override + public void onUpdateScrollbar() { + int rowCount = mWidgets.getPackageSize(); + + // Skip early if, there are no items. + if (rowCount == 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Skip early if, there no child laid out in the container. + getCurScrollState(mScrollPosState); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + } + + /** + * Returns the current scroll state. + */ + private void getCurScrollState(ScrollPositionState stateOut) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + int rowCount = mWidgets.getPackageSize(); + + // Return early if there are no items + if (rowCount == 0) { + return; + } + View child = getChildAt(0); + int position = getChildPosition(child); + + stateOut.rowIndex = position; + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java index 8b179f1e7..249559ab9 100644 --- a/src/com/android/launcher3/LauncherApplication.java +++ b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * 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. @@ -13,22 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.launcher3.widget; -package com.android.launcher3; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; -import android.app.Application; +public class WidgetsRowViewHolder extends ViewHolder { -public class LauncherApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); - LauncherAppState.setApplicationContext(this); - LauncherAppState.getInstance(); + ViewGroup mContent; + + public WidgetsRowViewHolder(ViewGroup v) { + super(v); + mContent = v; } - @Override - public void onTerminate() { - super.onTerminate(); - LauncherAppState.getInstance().onTerminate(); + ViewGroup getContent() { + return mContent; } -}
\ No newline at end of file +} |