Launcher2 is now Launcher3.

Changes include
  - moving from com.android.launcher{,2} to
    com.android.launcher3
  - removing wallpapers
  - new temporary icon

Change-Id: I1eabd06059e94a8f3bdf6b620777bd1d2b7c212b
diff --git a/src/com/android/launcher3/AccessibleTabView.java b/src/com/android/launcher3/AccessibleTabView.java
new file mode 100644
index 0000000..90a7865
--- /dev/null
+++ b/src/com/android/launcher3/AccessibleTabView.java
@@ -0,0 +1,51 @@
+/*
+ * 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.KeyEvent;
+import android.widget.TextView;
+
+/**
+ * We use a custom tab view to process our own focus traversals.
+ */
+public class AccessibleTabView extends TextView {
+    public AccessibleTabView(Context context) {
+        super(context);
+    }
+
+    public AccessibleTabView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AccessibleTabView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return FocusHelper.handleTabKeyEvent(this, keyCode, event)
+                || super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return FocusHelper.handleTabKeyEvent(this, keyCode, event)
+                || super.onKeyUp(keyCode, event);
+    }
+}
diff --git a/src/com/android/launcher3/AddAdapter.java b/src/com/android/launcher3/AddAdapter.java
new file mode 100644
index 0000000..ad15e75
--- /dev/null
+++ b/src/com/android/launcher3/AddAdapter.java
@@ -0,0 +1,103 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+import com.android.launcher3.R;
+
+/**
+ * Adapter showing the types of items that can be added to a {@link Workspace}.
+ */
+public class AddAdapter extends BaseAdapter {
+
+    private final LayoutInflater mInflater;
+
+    private final ArrayList<ListItem> mItems = new ArrayList<ListItem>();
+
+    public static final int ITEM_SHORTCUT = 0;
+    public static final int ITEM_APPWIDGET = 1;
+    public static final int ITEM_APPLICATION = 2;
+    public static final int ITEM_WALLPAPER = 3;
+
+    /**
+     * Specific item in our list.
+     */
+    public class ListItem {
+        public final CharSequence text;
+        public final Drawable image;
+        public final int actionTag;
+
+        public ListItem(Resources res, int textResourceId, int imageResourceId, int actionTag) {
+            text = res.getString(textResourceId);
+            if (imageResourceId != -1) {
+                image = res.getDrawable(imageResourceId);
+            } else {
+                image = null;
+            }
+            this.actionTag = actionTag;
+        }
+    }
+    
+    public AddAdapter(Launcher launcher) {
+        super();
+
+        mInflater = (LayoutInflater) launcher.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        // Create default actions
+        Resources res = launcher.getResources();
+
+        mItems.add(new ListItem(res, R.string.group_wallpapers,
+                R.mipmap.ic_launcher_wallpaper, ITEM_WALLPAPER));
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        ListItem item = (ListItem) getItem(position);
+
+        if (convertView == null) {
+            convertView = mInflater.inflate(R.layout.add_list_item, parent, false);
+        }
+
+        TextView textView = (TextView) convertView;
+        textView.setTag(item);
+        textView.setText(item.text);
+        textView.setCompoundDrawablesWithIntrinsicBounds(item.image, null, null, null);
+
+        return convertView;
+    }
+
+    public int getCount() {
+        return mItems.size();
+    }
+
+    public Object getItem(int position) {
+        return mItems.get(position);
+    }
+
+    public long getItemId(int position) {
+        return position;
+    }
+}
diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java
new file mode 100644
index 0000000..91f9bd0
--- /dev/null
+++ b/src/com/android/launcher3/Alarm.java
@@ -0,0 +1,84 @@
+/*
+ * 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.os.Handler;
+
+public class Alarm implements Runnable{
+    // if we reach this time and the alarm hasn't been cancelled, call the listener
+    private long mAlarmTriggerTime;
+
+    // if we've scheduled a call to run() (ie called mHandler.postDelayed), this variable is true.
+    // We use this to avoid having multiple pending callbacks
+    private boolean mWaitingForCallback;
+
+    private Handler mHandler;
+    private OnAlarmListener mAlarmListener;
+    private boolean mAlarmPending = false;
+
+    public Alarm() {
+        mHandler = new Handler();
+    }
+
+    public void setOnAlarmListener(OnAlarmListener alarmListener) {
+        mAlarmListener = alarmListener;
+    }
+
+    // Sets the alarm to go off in a certain number of milliseconds. If the alarm is already set,
+    // it's overwritten and only the new alarm setting is used
+    public void setAlarm(long millisecondsInFuture) {
+        long currentTime = System.currentTimeMillis();
+        mAlarmPending = true;
+        mAlarmTriggerTime = currentTime + millisecondsInFuture;
+        if (!mWaitingForCallback) {
+            mHandler.postDelayed(this, mAlarmTriggerTime - currentTime);
+            mWaitingForCallback = true;
+        }
+    }
+
+    public void cancelAlarm() {
+        mAlarmTriggerTime = 0;
+        mAlarmPending = false;
+    }
+
+    // this is called when our timer runs out
+    public void run() {
+        mWaitingForCallback = false;
+        if (mAlarmTriggerTime != 0) {
+            long currentTime = System.currentTimeMillis();
+            if (mAlarmTriggerTime > currentTime) {
+                // We still need to wait some time to trigger spring loaded mode--
+                // post a new callback
+                mHandler.postDelayed(this, Math.max(0, mAlarmTriggerTime - currentTime));
+                mWaitingForCallback = true;
+            } else {
+                mAlarmPending = false;
+                if (mAlarmListener != null) {
+                    mAlarmListener.onAlarm(this);
+                }
+            }
+        }
+    }
+
+    public boolean alarmPending() {
+        return mAlarmPending;
+    }
+}
+
+interface OnAlarmListener {
+    public void onAlarm(Alarm alarm);
+}
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
new file mode 100644
index 0000000..e74dc21
--- /dev/null
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -0,0 +1,221 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+
+/**
+ * Stores the list of all applications for the all apps view.
+ */
+class AllAppsList {
+    public static final int DEFAULT_APPLICATIONS_NUMBER = 42;
+    
+    /** The list off all apps. */
+    public ArrayList<ApplicationInfo> data =
+            new ArrayList<ApplicationInfo>(DEFAULT_APPLICATIONS_NUMBER);
+    /** The list of apps that have been added since the last notify() call. */
+    public ArrayList<ApplicationInfo> added =
+            new ArrayList<ApplicationInfo>(DEFAULT_APPLICATIONS_NUMBER);
+    /** The list of apps that have been removed since the last notify() call. */
+    public ArrayList<ApplicationInfo> removed = new ArrayList<ApplicationInfo>();
+    /** The list of apps that have been modified since the last notify() call. */
+    public ArrayList<ApplicationInfo> modified = new ArrayList<ApplicationInfo>();
+
+    private IconCache mIconCache;
+
+    /**
+     * Boring constructor.
+     */
+    public AllAppsList(IconCache iconCache) {
+        mIconCache = iconCache;
+    }
+
+    /**
+     * Add the supplied ApplicationInfo objects to the list, and enqueue it into the
+     * list to broadcast when notify() is called.
+     *
+     * If the app is already in the list, doesn't add it.
+     */
+    public void add(ApplicationInfo info) {
+        if (findActivity(data, info.componentName)) {
+            return;
+        }
+        data.add(info);
+        added.add(info);
+    }
+    
+    public void clear() {
+        data.clear();
+        // TODO: do we clear these too?
+        added.clear();
+        removed.clear();
+        modified.clear();
+    }
+
+    public int size() {
+        return data.size();
+    }
+
+    public ApplicationInfo get(int index) {
+        return data.get(index);
+    }
+
+    /**
+     * Add the icons for the supplied apk called packageName.
+     */
+    public void addPackage(Context context, String packageName) {
+        final List<ResolveInfo> matches = findActivitiesForPackage(context, packageName);
+
+        if (matches.size() > 0) {
+            for (ResolveInfo info : matches) {
+                add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null));
+            }
+        }
+    }
+
+    /**
+     * Remove the apps for the given apk identified by packageName.
+     */
+    public void removePackage(String packageName) {
+        final List<ApplicationInfo> data = this.data;
+        for (int i = data.size() - 1; i >= 0; i--) {
+            ApplicationInfo info = data.get(i);
+            final ComponentName component = info.intent.getComponent();
+            if (packageName.equals(component.getPackageName())) {
+                removed.add(info);
+                data.remove(i);
+            }
+        }
+        // This is more aggressive than it needs to be.
+        mIconCache.flush();
+    }
+
+    /**
+     * Add and remove icons for this package which has been updated.
+     */
+    public void updatePackage(Context context, String packageName) {
+        final List<ResolveInfo> matches = findActivitiesForPackage(context, packageName);
+        if (matches.size() > 0) {
+            // Find disabled/removed activities and remove them from data and add them
+            // to the removed list.
+            for (int i = data.size() - 1; i >= 0; i--) {
+                final ApplicationInfo applicationInfo = data.get(i);
+                final ComponentName component = applicationInfo.intent.getComponent();
+                if (packageName.equals(component.getPackageName())) {
+                    if (!findActivity(matches, component)) {
+                        removed.add(applicationInfo);
+                        mIconCache.remove(component);
+                        data.remove(i);
+                    }
+                }
+            }
+
+            // Find enabled activities and add them to the adapter
+            // Also updates existing activities with new labels/icons
+            int count = matches.size();
+            for (int i = 0; i < count; i++) {
+                final ResolveInfo info = matches.get(i);
+                ApplicationInfo applicationInfo = findApplicationInfoLocked(
+                        info.activityInfo.applicationInfo.packageName,
+                        info.activityInfo.name);
+                if (applicationInfo == null) {
+                    add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null));
+                } else {
+                    mIconCache.remove(applicationInfo.componentName);
+                    mIconCache.getTitleAndIcon(applicationInfo, info, null);
+                    modified.add(applicationInfo);
+                }
+            }
+        } else {
+            // Remove all data for this package.
+            for (int i = data.size() - 1; i >= 0; i--) {
+                final ApplicationInfo applicationInfo = data.get(i);
+                final ComponentName component = applicationInfo.intent.getComponent();
+                if (packageName.equals(component.getPackageName())) {
+                    removed.add(applicationInfo);
+                    mIconCache.remove(component);
+                    data.remove(i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Query the package manager for MAIN/LAUNCHER activities in the supplied package.
+     */
+    private static List<ResolveInfo> findActivitiesForPackage(Context context, String packageName) {
+        final PackageManager packageManager = context.getPackageManager();
+
+        final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        mainIntent.setPackage(packageName);
+
+        final List<ResolveInfo> apps = packageManager.queryIntentActivities(mainIntent, 0);
+        return apps != null ? apps : new ArrayList<ResolveInfo>();
+    }
+
+    /**
+     * Returns whether <em>apps</em> contains <em>component</em>.
+     */
+    private static boolean findActivity(List<ResolveInfo> apps, ComponentName component) {
+        final String className = component.getClassName();
+        for (ResolveInfo info : apps) {
+            final ActivityInfo activityInfo = info.activityInfo;
+            if (activityInfo.name.equals(className)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether <em>apps</em> contains <em>component</em>.
+     */
+    private static boolean findActivity(ArrayList<ApplicationInfo> apps, ComponentName component) {
+        final int N = apps.size();
+        for (int i=0; i<N; i++) {
+            final ApplicationInfo info = apps.get(i);
+            if (info.componentName.equals(component)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Find an ApplicationInfo object for the given packageName and className.
+     */
+    private ApplicationInfo findApplicationInfoLocked(String packageName, String className) {
+        for (ApplicationInfo info: data) {
+            final ComponentName component = info.intent.getComponent();
+            if (packageName.equals(component.getPackageName())
+                    && className.equals(component.getClassName())) {
+                return info;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
new file mode 100644
index 0000000..8e968f8
--- /dev/null
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -0,0 +1,471 @@
+package com.android.launcher3;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.launcher3.R;
+
+public class AppWidgetResizeFrame extends FrameLayout {
+    private LauncherAppWidgetHostView mWidgetView;
+    private CellLayout mCellLayout;
+    private DragLayer mDragLayer;
+    private Workspace mWorkspace;
+    private ImageView mLeftHandle;
+    private ImageView mRightHandle;
+    private ImageView mTopHandle;
+    private ImageView mBottomHandle;
+
+    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;
+    private int mBaselineY;
+    private int mResizeMode;
+
+    private int mRunningHInc;
+    private int mRunningVInc;
+    private int mMinHSpan;
+    private int mMinVSpan;
+    private int mDeltaX;
+    private int mDeltaY;
+    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];
+
+    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) {
+
+        super(context);
+        mLauncher = (Launcher) context;
+        mCellLayout = cellLayout;
+        mWidgetView = widgetView;
+        mResizeMode = widgetView.getAppWidgetInfo().resizeMode;
+        mDragLayer = dragLayer;
+        mWorkspace = (Workspace) dragLayer.findViewById(R.id.workspace);
+
+        final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo();
+        int[] result = Launcher.getMinSpanForWidget(mLauncher, info);
+        mMinHSpan = result[0];
+        mMinVSpan = result[1];
+
+        setBackgroundResource(R.drawable.widget_resize_frame_holo);
+        setPadding(0, 0, 0, 0);
+
+        LayoutParams lp;
+        mLeftHandle = new ImageView(context);
+        mLeftHandle.setImageResource(R.drawable.widget_resize_handle_left);
+        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 
+                Gravity.START | Gravity.CENTER_VERTICAL);
+        addView(mLeftHandle, lp);
+
+        mRightHandle = new ImageView(context);
+        mRightHandle.setImageResource(R.drawable.widget_resize_handle_right);
+        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 
+                Gravity.END | Gravity.CENTER_VERTICAL);
+        addView(mRightHandle, lp);
+
+        mTopHandle = new ImageView(context);
+        mTopHandle.setImageResource(R.drawable.widget_resize_handle_top);
+        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 
+                Gravity.CENTER_HORIZONTAL | Gravity.TOP);
+        addView(mTopHandle, lp);
+
+        mBottomHandle = new ImageView(context);
+        mBottomHandle.setImageResource(R.drawable.widget_resize_handle_bottom);
+        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 
+                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
+        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 (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
+            mTopHandle.setVisibility(GONE);
+            mBottomHandle.setVisibility(GONE);
+        } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
+            mLeftHandle.setVisibility(GONE);
+            mRightHandle.setVisibility(GONE);
+        }
+
+        final float density = mLauncher.getResources().getDisplayMetrics().density;
+        mBackgroundPadding = (int) Math.ceil(density * BACKGROUND_PADDING);
+        mTouchTargetWidth = 2 * mBackgroundPadding;
+
+        // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
+        // cells (same if not resized, or different) will be marked as occupied when the resize
+        // frame is dismissed.
+        mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
+    }
+
+    public boolean beginResizeIfPointInRegion(int x, int y) {
+        boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
+        boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
+
+        mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
+        mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
+        mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
+        mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
+                && verticalActive;
+
+        boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
+                || mTopBorderActive || mBottomBorderActive;
+
+        mBaselineWidth = getMeasuredWidth();
+        mBaselineHeight = getMeasuredHeight();
+        mBaselineX = getLeft();
+        mBaselineY = getTop();
+
+        if (anyBordersActive) {
+            mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
+            mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
+            mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
+            mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
+        }
+        return anyBordersActive;
+    }
+
+    /**
+     *  Here we bound the deltas such that the frame cannot be stretched beyond the extents
+     *  of the CellLayout, and such that the frame's borders can't cross.
+     */
+    public void updateDeltas(int deltaX, int deltaY) {
+        if (mLeftBorderActive) {
+            mDeltaX = Math.max(-mBaselineX, deltaX); 
+            mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX);
+        } else if (mRightBorderActive) {
+            mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX);
+            mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX);
+        }
+
+        if (mTopBorderActive) {
+            mDeltaY = Math.max(-mBaselineY, deltaY);
+            mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY);
+        } else if (mBottomBorderActive) {
+            mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY);
+            mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY);
+        }
+    }
+
+    public void visualizeResizeForDelta(int deltaX, int deltaY) {
+        visualizeResizeForDelta(deltaX, deltaY, false);
+    }
+
+    /**
+     *  Based on the deltas, we resize the frame, and, if needed, we resize the widget.
+     */
+    private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) {
+        updateDeltas(deltaX, deltaY);
+        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+
+        if (mLeftBorderActive) {
+            lp.x = mBaselineX + mDeltaX;
+            lp.width = mBaselineWidth - mDeltaX;
+        } else if (mRightBorderActive) {
+            lp.width = mBaselineWidth + mDeltaX;
+        }
+
+        if (mTopBorderActive) {
+            lp.y = mBaselineY + mDeltaY;
+            lp.height = mBaselineHeight - mDeltaY;
+        } else if (mBottomBorderActive) {
+            lp.height = mBaselineHeight + mDeltaY;
+        }
+
+        resizeWidgetIfNeeded(onDismiss);
+        requestLayout();
+    }
+
+    /**
+     *  Based on the current deltas, we determine if and how to resize the widget.
+     */
+    private void resizeWidgetIfNeeded(boolean onDismiss) {
+        int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
+        int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
+
+        int deltaX = mDeltaX + mDeltaXAddOn;
+        int deltaY = mDeltaY + mDeltaYAddOn;
+
+        float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc;
+        float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc;
+
+        int hSpanInc = 0;
+        int vSpanInc = 0;
+        int cellXInc = 0;
+        int cellYInc = 0;
+
+        int countX = mCellLayout.getCountX();
+        int countY = mCellLayout.getCountY();
+
+        if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) {
+            hSpanInc = Math.round(hSpanIncF);
+        }
+        if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) {
+            vSpanInc = Math.round(vSpanIncF);
+        }
+
+        if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
+
+
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
+
+        int spanX = lp.cellHSpan;
+        int spanY = lp.cellVSpan;
+        int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
+        int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
+
+        int hSpanDelta = 0;
+        int vSpanDelta = 0;
+
+        // For each border, we bound the resizing based on the minimum width, and the maximum
+        // expandability.
+        if (mLeftBorderActive) {
+            cellXInc = Math.max(-cellX, hSpanInc);
+            cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc);
+            hSpanInc *= -1;
+            hSpanInc = Math.min(cellX, hSpanInc);
+            hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc);
+            hSpanDelta = -hSpanInc;
+
+        } else if (mRightBorderActive) {
+            hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc);
+            hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc);
+            hSpanDelta = hSpanInc;
+        }
+
+        if (mTopBorderActive) {
+            cellYInc = Math.max(-cellY, vSpanInc);
+            cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc);
+            vSpanInc *= -1;
+            vSpanInc = Math.min(cellY, vSpanInc);
+            vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc);
+            vSpanDelta = -vSpanInc;
+        } else if (mBottomBorderActive) {
+            vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc);
+            vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc);
+            vSpanDelta = vSpanInc;
+        }
+
+        mDirectionVector[0] = 0;
+        mDirectionVector[1] = 0;
+        // Update the widget's dimensions and position according to the deltas computed above
+        if (mLeftBorderActive || mRightBorderActive) {
+            spanX += hSpanInc;
+            cellX += cellXInc;
+            if (hSpanDelta != 0) {
+                mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
+            }
+        }
+
+        if (mTopBorderActive || mBottomBorderActive) {
+            spanY += vSpanInc;
+            cellY += cellYInc;
+            if (vSpanDelta != 0) {
+                mDirectionVector[1] = mTopBorderActive ? -1 : 1;
+            }
+        }
+
+        if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
+
+        // We always want the final commit to match the feedback, so we make sure to use the
+        // last used direction vector when committing the resize / reorder.
+        if (onDismiss) {
+            mDirectionVector[0] = mLastDirectionVector[0];
+            mDirectionVector[1] = mLastDirectionVector[1];
+        } else {
+            mLastDirectionVector[0] = mDirectionVector[0];
+            mLastDirectionVector[1] = mDirectionVector[1];
+        }
+
+        if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
+                mDirectionVector, onDismiss)) {
+            lp.tmpCellX = cellX;
+            lp.tmpCellY = cellY;
+            lp.cellHSpan = spanX;
+            lp.cellVSpan = spanY;
+            mRunningVInc += vSpanDelta;
+            mRunningHInc += hSpanDelta;
+            if (!onDismiss) {
+                updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
+            }
+        }
+        mWidgetView.requestLayout();
+    }
+
+    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);
+    }
+
+    static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) {
+        if (rect == null) {
+            rect = new Rect();
+        }
+        Rect landMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.LANDSCAPE);
+        Rect portMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.PORTRAIT);
+        final float density = launcher.getResources().getDisplayMetrics().density;
+
+        // Compute landscape size
+        int cellWidth = landMetrics.left;
+        int cellHeight = landMetrics.top;
+        int widthGap = landMetrics.right;
+        int heightGap = landMetrics.bottom;
+        int landWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density);
+        int landHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density);
+
+        // Compute portrait size
+        cellWidth = portMetrics.left;
+        cellHeight = portMetrics.top;
+        widthGap = portMetrics.right;
+        heightGap = portMetrics.bottom;
+        int portWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density);
+        int portHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density);
+        rect.set(portWidth, landHeight, landWidth, portHeight);
+        return rect;
+    }
+
+    /**
+     * This is the final step of the resize. Here we save the new widget size and position
+     * to LauncherModel and animate the resize frame.
+     */
+    public void commitResize() {
+        resizeWidgetIfNeeded(true);
+        requestLayout();
+    }
+
+    public void onTouchUp() {
+        int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
+        int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
+
+        mDeltaXAddOn = mRunningHInc * xThreshold; 
+        mDeltaYAddOn = mRunningVInc * yThreshold; 
+        mDeltaX = 0;
+        mDeltaY = 0;
+
+        post(new Runnable() {
+            @Override
+            public void run() {
+                snapToWidget(true);
+            }
+        });
+    }
+
+    public void snapToWidget(boolean animate) {
+        final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+        int xOffset = mCellLayout.getLeft() + mCellLayout.getPaddingLeft()
+                + mDragLayer.getPaddingLeft() - mWorkspace.getScrollX();
+        int yOffset = mCellLayout.getTop() + mCellLayout.getPaddingTop()
+                + mDragLayer.getPaddingTop() - mWorkspace.getScrollY();
+
+        int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft -
+                mWidgetPaddingRight;
+        int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop -
+                mWidgetPaddingBottom;
+
+        int newX = mWidgetView.getLeft() - mBackgroundPadding + xOffset + mWidgetPaddingLeft;
+        int newY = mWidgetView.getTop() - mBackgroundPadding + yOffset + mWidgetPaddingTop;
+
+        // 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) {
+            // In this case we shift the touch region down to start at the top of the DragLayer
+            mTopTouchRegionAdjustment = -newY;
+        } else {
+            mTopTouchRegionAdjustment = 0;
+        }
+        if (newY + newHeight > mDragLayer.getHeight()) {
+            // In this case we shift the touch region up to end at the bottom of the DragLayer
+            mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
+        } else {
+            mBottomTouchRegionAdjustment = 0;
+        }
+
+        if (!animate) {
+            lp.width = newWidth;
+            lp.height = newHeight;
+            lp.x = newX;
+            lp.y = newY;
+            mLeftHandle.setAlpha(1.0f);
+            mRightHandle.setAlpha(1.0f);
+            mTopHandle.setAlpha(1.0f);
+            mBottomHandle.setAlpha(1.0f);
+            requestLayout();
+        } else {
+            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
+            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
+                    newHeight);
+            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
+            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
+            ObjectAnimator oa =
+                    LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
+            ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f);
+            ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f);
+            ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f);
+            ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, "alpha", 1.0f);
+            oa.addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    requestLayout();
+                }
+            });
+            AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
+            if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
+                set.playTogether(oa, topOa, bottomOa);
+            } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
+                set.playTogether(oa, leftOa, rightOa);
+            } else {
+                set.playTogether(oa, leftOa, rightOa, topOa, bottomOa);
+            }
+
+            set.setDuration(SNAP_DURATION);
+            set.start();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/ApplicationInfo.java b/src/com/android/launcher3/ApplicationInfo.java
new file mode 100644
index 0000000..4659e7e
--- /dev/null
+++ b/src/com/android/launcher3/ApplicationInfo.java
@@ -0,0 +1,133 @@
+/*
+ * 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.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Represents an app in AllAppsView.
+ */
+class ApplicationInfo extends ItemInfo {
+    private static final String TAG = "Launcher2.ApplicationInfo";
+
+    /**
+     * The intent used to start the application.
+     */
+    Intent intent;
+
+    /**
+     * A bitmap version of the application icon.
+     */
+    Bitmap iconBitmap;
+
+    /**
+     * The time at which the app was first installed.
+     */
+    long firstInstallTime;
+
+    ComponentName componentName;
+
+    static final int DOWNLOADED_FLAG = 1;
+    static final int UPDATED_SYSTEM_APP_FLAG = 2;
+
+    int flags = 0;
+
+    ApplicationInfo() {
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
+    }
+
+    /**
+     * Must not hold the Context.
+     */
+    public ApplicationInfo(PackageManager pm, ResolveInfo info, IconCache iconCache,
+            HashMap<Object, CharSequence> labelCache) {
+        final String packageName = info.activityInfo.applicationInfo.packageName;
+
+        this.componentName = new ComponentName(packageName, info.activityInfo.name);
+        this.container = ItemInfo.NO_ID;
+        this.setActivity(componentName,
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+        try {
+            int appFlags = pm.getApplicationInfo(packageName, 0).flags;
+            if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) {
+                flags |= DOWNLOADED_FLAG;
+
+                if ((appFlags & android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
+                    flags |= UPDATED_SYSTEM_APP_FLAG;
+                }
+            }
+            firstInstallTime = pm.getPackageInfo(packageName, 0).firstInstallTime;
+        } catch (NameNotFoundException e) {
+            Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName);
+        }
+
+        iconCache.getTitleAndIcon(this, info, labelCache);
+    }
+
+    public ApplicationInfo(ApplicationInfo info) {
+        super(info);
+        componentName = info.componentName;
+        title = info.title.toString();
+        intent = new Intent(info.intent);
+        flags = info.flags;
+        firstInstallTime = info.firstInstallTime;
+    }
+
+    /**
+     * Creates the application intent based on a component name and various launch flags.
+     * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}.
+     *
+     * @param className the class name of the component representing the intent
+     * @param launchFlags the launch flags
+     */
+    final void setActivity(ComponentName className, int launchFlags) {
+        intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setComponent(className);
+        intent.setFlags(launchFlags);
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION;
+    }
+
+    @Override
+    public String toString() {
+        return "ApplicationInfo(title=" + title.toString() + ")";
+    }
+
+    public static void dumpApplicationInfoList(String tag, String label,
+            ArrayList<ApplicationInfo> list) {
+        Log.d(tag, label + " size=" + list.size());
+        for (ApplicationInfo info: list) {
+            Log.d(tag, "   title=\"" + info.title + "\" iconBitmap="
+                    + info.iconBitmap + " firstInstallTime="
+                    + info.firstInstallTime);
+        }
+    }
+
+    public ShortcutInfo makeShortcut() {
+        return new ShortcutInfo(this);
+    }
+}
diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java
new file mode 100644
index 0000000..43a5259
--- /dev/null
+++ b/src/com/android/launcher3/AppsCustomizePagedView.java
@@ -0,0 +1,1710 @@
+/*
+ * 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.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+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.view.animation.DecelerateInterpolator;
+import android.widget.GridLayout;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+import com.android.launcher3.DropTarget.DragObject;
+
+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,
+        PagedViewIcon.PressedCallback, PagedViewWidget.ShortPressListener,
+        LauncherTransitionable {
+    static final String TAG = "AppsCustomizePagedView";
+
+    /**
+     * The different content types that this paged view can show.
+     */
+    public enum ContentType {
+        Applications,
+        Widgets
+    }
+
+    // Refs
+    private Launcher mLauncher;
+    private DragController mDragController;
+    private final LayoutInflater mLayoutInflater;
+    private final PackageManager mPackageManager;
+
+    // Save and Restore
+    private int mSaveInstanceStateItemIndex = -1;
+    private PagedViewIcon mPressedIcon;
+
+    // Content
+    private ArrayList<ApplicationInfo> mApps;
+    private ArrayList<Object> mWidgets;
+
+    // Cling
+    private boolean mHasShownAllAppsCling;
+    private int mClingFocusedX;
+    private int mClingFocusedY;
+
+    // Caching
+    private Canvas mCanvas;
+    private IconCache mIconCache;
+
+    // Dimens
+    private int mContentWidth;
+    private int mMaxAppCellCountX, mMaxAppCellCountY;
+    private int mWidgetCountX, mWidgetCountY;
+    private int mWidgetWidthGap, mWidgetHeightGap;
+    private PagedViewCellLayout mWidgetSpacingLayout;
+    private int mNumAppsPages;
+    private int mNumWidgetPages;
+
+    // Relating to the scroll and overscroll effects
+    Workspace.ZInterpolator mZInterpolator = new Workspace.ZInterpolator(0.5f);
+    private static float CAMERA_DISTANCE = 6500;
+    private static float TRANSITION_SCALE_FACTOR = 0.74f;
+    private static float TRANSITION_PIVOT = 0.65f;
+    private static float TRANSITION_MAX_ROTATION = 22;
+    private static final boolean PERFORM_OVERSCROLL_ROTATION = true;
+    private AccelerateInterpolator mAlphaInterpolator = new AccelerateInterpolator(0.9f);
+    private DecelerateInterpolator mLeftScreenAlphaInterpolator = new DecelerateInterpolator(4);
+
+    // 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;
+
+    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>();
+
+    private Rect mTmpRect = new Rect();
+
+    // Used for drawing shortcut previews
+    BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache();
+    PaintCache mCachedShortcutPreviewPaint = new PaintCache();
+    CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache();
+
+    // Used for drawing widget previews
+    CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache();
+    RectCache mCachedAppWidgetPreviewSrcRect = new RectCache();
+    RectCache mCachedAppWidgetPreviewDestRect = new RectCache();
+    PaintCache mCachedAppWidgetPreviewPaint = new PaintCache();
+
+    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<ApplicationInfo>();
+        mWidgets = new ArrayList<Object>();
+        mIconCache = ((LauncherApplication) context.getApplicationContext()).getIconCache();
+        mCanvas = new Canvas();
+        mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>();
+
+        // Save the default widget preview background
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0);
+        mMaxAppCellCountX = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountX, -1);
+        mMaxAppCellCountY = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountY, -1);
+        mWidgetWidthGap =
+            a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellWidthGap, 0);
+        mWidgetHeightGap =
+            a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellHeightGap, 0);
+        mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2);
+        mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2);
+        mClingFocusedX = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedX, 0);
+        mClingFocusedY = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedY, 0);
+        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);
+        }
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+        mCenterPagesVertically = false;
+
+        Context context = getContext();
+        Resources r = context.getResources();
+        setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f);
+    }
+
+    /** 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 (currentPage < mNumAppsPages) {
+                PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(currentPage);
+                PagedViewCellLayoutChildren childrenLayout = layout.getChildrenLayout();
+                int numItemsPerPage = mCellCountX * mCellCountY;
+                int childCount = childrenLayout.getChildCount();
+                if (childCount > 0) {
+                    i = (currentPage * numItemsPerPage) + (childCount / 2);
+                }
+            } else {
+                int numApps = mApps.size();
+                PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage);
+                int numItemsPerPage = mWidgetCountX * mWidgetCountY;
+                int childCount = layout.getChildCount();
+                if (childCount > 0) {
+                    i = numApps +
+                        ((currentPage - mNumAppsPages) * numItemsPerPage) + (childCount / 2);
+                }
+            }
+        }
+        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 mNumAppsPages + ((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) {
+        if (mWidgetPreviewLoader == null) {
+            mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher);
+        }
+
+        // Note that we transpose the counts in portrait so that we get a similar layout
+        boolean isLandscape = getResources().getConfiguration().orientation ==
+            Configuration.ORIENTATION_LANDSCAPE;
+        int maxCellCountX = Integer.MAX_VALUE;
+        int maxCellCountY = Integer.MAX_VALUE;
+        if (LauncherApplication.isScreenLarge()) {
+            maxCellCountX = (isLandscape ? LauncherModel.getCellCountX() :
+                LauncherModel.getCellCountY());
+            maxCellCountY = (isLandscape ? LauncherModel.getCellCountY() :
+                LauncherModel.getCellCountX());
+        }
+        if (mMaxAppCellCountX > -1) {
+            maxCellCountX = Math.min(maxCellCountX, mMaxAppCellCountX);
+        }
+        // Temp hack for now: only use the max cell count Y for widget layout
+        int maxWidgetCellCountY = maxCellCountY;
+        if (mMaxAppCellCountY > -1) {
+            maxWidgetCellCountY = Math.min(maxWidgetCellCountY, mMaxAppCellCountY);
+        }
+
+        // Now that the data is ready, we can calculate the content width, the number of cells to
+        // use for each page
+        mWidgetSpacingLayout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap);
+        mWidgetSpacingLayout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop,
+                mPageLayoutPaddingRight, mPageLayoutPaddingBottom);
+        mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxCellCountY);
+        mCellCountX = mWidgetSpacingLayout.getCellCountX();
+        mCellCountY = mWidgetSpacingLayout.getCellCountY();
+        updatePageCounts();
+
+        // Force a measure to update recalculate the gaps
+        int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST);
+        int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST);
+        mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxWidgetCellCountY);
+        mWidgetSpacingLayout.measure(widthSpec, heightSpec);
+        mContentWidth = mWidgetSpacingLayout.getContentWidth();
+
+        AppsCustomizeTabHost host = (AppsCustomizeTabHost) getTabHost();
+        final boolean hostIsTransitioning = host.isTransitioning();
+
+        // Restore the page
+        int page = getPageForComponent(mSaveInstanceStateItemIndex);
+        invalidatePageData(Math.max(0, page), hostIsTransitioning);
+
+        // Show All Apps cling if we are finished transitioning, otherwise, we will try again when
+        // the transition completes in AppsCustomizeTabHost (otherwise the wrong offsets will be
+        // returned while animating)
+        if (!hostIsTransitioning) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    showAllAppsCling();
+                }
+            });
+        }
+    }
+
+    void showAllAppsCling() {
+        if (!mHasShownAllAppsCling && isDataReady()) {
+            mHasShownAllAppsCling = true;
+            // Calculate the position for the cling punch through
+            int[] offset = new int[2];
+            int[] pos = mWidgetSpacingLayout.estimateCellPosition(mClingFocusedX, mClingFocusedY);
+            mLauncher.getDragLayer().getLocationInDragLayer(this, offset);
+            // PagedViews are centered horizontally but top aligned
+            // Note we have to shift the items up now that Launcher sits under the status bar
+            pos[0] += (getMeasuredWidth() - mWidgetSpacingLayout.getMeasuredWidth()) / 2 +
+                    offset[0];
+            pos[1] += offset[1] - mLauncher.getDragLayer().getPaddingTop();
+            mLauncher.showFirstRunAllAppsCling(pos);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        if (!isDataReady()) {
+            if (!mApps.isEmpty() && !mWidgets.isEmpty()) {
+                setDataIsReady();
+                setMeasuredDimension(width, height);
+                onDataReady(width, height);
+            }
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) {
+        // Get the list of widgets and shortcuts
+        mWidgets.clear();
+        for (Object o : widgetsAndShortcuts) {
+            if (o instanceof AppWidgetProviderInfo) {
+                AppWidgetProviderInfo widget = (AppWidgetProviderInfo) o;
+                widget.label = widget.label.trim();
+                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 <= LauncherModel.getCellCountX() &&
+                        minSpanY <= LauncherModel.getCellCountY()) {
+                        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()) return;
+
+        if (v instanceof PagedViewIcon) {
+            // Animate some feedback to the click
+            final ApplicationInfo appInfo = (ApplicationInfo) v.getTag();
+
+            // Lock the drawable state to pressed until we return to Launcher
+            if (mPressedIcon != null) {
+                mPressedIcon.lockDrawableState();
+            }
+
+            // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled
+            // to be consistent.  So re-enable the flag here, and we will re-disable it as necessary
+            // when Launcher resumes and we are still in AllApps.
+            mLauncher.updateWallpaperVisibility(true);
+            mLauncher.startActivitySafely(v, appInfo.intent, appInfo);
+
+        } else if (v instanceof PagedViewWidget) {
+            // 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().onDragStartedWithItem(v);
+        mLauncher.getWorkspace().beginDragShared(v, this);
+    }
+
+    Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) {
+        Bundle options = null;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, mTmpRect);
+            Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(mLauncher,
+                    info.componentName, null);
+
+            float density = 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,
+                    mTmpRect.left - xPaddingDips);
+            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
+                    mTmpRect.top - yPaddingDips);
+            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH,
+                    mTmpRect.right - xPaddingDips);
+            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
+                    mTmpRect.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();
+                // Options will be null for platforms with JB or lower, so this serves as an
+                // SDK level check.
+                if (options == null) {
+                    if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed(
+                            mWidgetLoadingId, info.componentName)) {
+                        mWidgetCleanupState = WIDGET_BOUND;
+                    }
+                } else {
+                    if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed(
+                            mWidgetLoadingId, info.componentName, 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 = mWidgetPreviewLoader.generateWidgetPreview(createWidgetInfo.componentName,
+                    createWidgetInfo.previewImage, createWidgetInfo.icon, 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],
+                    mWidgetPreviewLoader.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 = Bitmap.createBitmap(icon.getIntrinsicWidth(),
+                    icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+
+            mCanvas.setBitmap(preview);
+            mCanvas.save();
+            WidgetPreviewLoader.renderDrawableToBitmap(icon, preview, 0, 0,
+                    icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+            mCanvas.restore();
+            mCanvas.setBitmap(null);
+            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 PagedViewIcon) {
+            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()) {
+                    // Dismiss the cling
+                    mLauncher.dismissAllAppsCling(null);
+
+                    // Reset the alpha on the dragged icon before we drag
+                    resetDrawableState();
+
+                    // 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))) {
+            // Exit spring loaded mode if we have not successfully dropped or have not handled the
+            // drop in Workspace
+            mLauncher.exitSpringLoadedDragMode();
+        }
+        mLauncher.unlockScreenOrientation(false);
+    }
+
+    @Override
+    public View getContent() {
+        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);
+        }
+        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
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        cancelAllTasks();
+    }
+
+    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) {
+        if (type == ContentType.Widgets) {
+            invalidatePageData(mNumAppsPages, true);
+        } else if (type == ContentType.Applications) {
+            invalidatePageData(0, true);
+        }
+    }
+
+    protected void snapToPage(int whichPage, int delta, int duration) {
+        super.snapToPage(whichPage, delta, duration);
+        updateCurrentTab(whichPage);
+
+        // 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);
+            }
+        }
+    }
+
+    private void updateCurrentTab(int currentPage) {
+        AppsCustomizeTabHost tabHost = getTabHost();
+        if (tabHost != null) {
+            String tag = tabHost.getCurrentTabTag();
+            if (tag != null) {
+                if (currentPage >= mNumAppsPages &&
+                        !tag.equals(tabHost.getTabTagForContentType(ContentType.Widgets))) {
+                    tabHost.setCurrentTabFromContent(ContentType.Widgets);
+                } else if (currentPage < mNumAppsPages &&
+                        !tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) {
+                    tabHost.setCurrentTabFromContent(ContentType.Applications);
+                }
+            }
+        }
+    }
+
+    /*
+     * 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(PagedViewCellLayout layout) {
+        layout.setCellCount(mCellCountX, mCellCountY);
+        layout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap);
+        layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop,
+                mPageLayoutPaddingRight, mPageLayoutPaddingBottom);
+
+        // 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(getMeasuredWidth(), MeasureSpec.AT_MOST);
+        int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST);
+        layout.setMinimumWidth(getPageContentWidth());
+        layout.measure(widthSpec, heightSpec);
+        setVisibilityOnChildren(layout, View.VISIBLE);
+    }
+
+    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());
+        PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(page);
+
+        layout.removeAllViewsOnPage();
+        ArrayList<Object> items = new ArrayList<Object>();
+        ArrayList<Bitmap> images = new ArrayList<Bitmap>();
+        for (int i = startIndex; i < endIndex; ++i) {
+            ApplicationInfo info = mApps.get(i);
+            PagedViewIcon icon = (PagedViewIcon) mLayoutInflater.inflate(
+                    R.layout.apps_customize_application, layout, false);
+            icon.applyFromApplicationInfo(info, true, this);
+            icon.setOnClickListener(this);
+            icon.setOnLongClickListener(this);
+            icon.setOnTouchListener(this);
+            icon.setOnKeyListener(this);
+
+            int index = i - startIndex;
+            int x = index % mCellCountX;
+            int y = index / mCellCountX;
+            if (isRtl) {
+                x = mCellCountX - x - 1;
+            }
+            layout.addViewToCellLayout(icon, -1, i, new PagedViewCellLayout.LayoutParams(x,y, 1,1));
+
+            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);
+                }
+            }, mWidgetPreviewLoader);
+
+        // 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) {
+        layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop,
+                mPageLayoutPaddingRight, mPageLayoutPaddingBottom);
+
+        // 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(getMeasuredWidth(), MeasureSpec.AT_MOST);
+        int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST);
+        layout.setMinimumWidth(getPageContentWidth());
+        layout.measure(widthSpec, heightSpec);
+    }
+
+    public void syncWidgetPageItems(final int page, final boolean immediate) {
+        int numItemsPerPage = mWidgetCountX * mWidgetCountY;
+
+        // Calculate the dimensions of each cell we are giving to each widget
+        final ArrayList<Object> items = new ArrayList<Object>();
+        int contentWidth = mWidgetSpacingLayout.getContentWidth();
+        final int cellWidth = ((contentWidth - mPageLayoutPaddingLeft - mPageLayoutPaddingRight
+                - ((mWidgetCountX - 1) * mWidgetWidthGap)) / mWidgetCountX);
+        int contentHeight = mWidgetSpacingLayout.getContentHeight();
+        final int cellHeight = ((contentHeight - mPageLayoutPaddingTop - mPageLayoutPaddingBottom
+                - ((mWidgetCountY - 1) * mWidgetHeightGap)) / mWidgetCountY);
+
+        // Prepare the set of widgets to load previews for in the background
+        int offset = (page - mNumAppsPages) * 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
+        final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page);
+        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, mWidgetPreviewLoader);
+                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, mWidgetPreviewLoader);
+                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;
+            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);
+            if (ix > 0) lp.leftMargin = mWidgetWidthGap;
+            if (iy > 0) lp.topMargin = mWidgetHeightGap;
+            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];
+                }
+
+                mWidgetPreviewLoader.setPreviewSize(
+                        maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout);
+                if (immediate) {
+                    AsyncTaskPageData data = new AsyncTaskPageData(page, items,
+                            maxPreviewWidth, maxPreviewHeight, null, null, mWidgetPreviewLoader);
+                    loadWidgetPreviewsInBackground(null, data);
+                    onSyncWidgetPageItems(data);
+                } 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(mWidgetPreviewLoader.getPreview(items.get(i)));
+        }
+    }
+
+    private void onSyncWidgetPageItems(AsyncTaskPageData data) {
+        if (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() {
+        removeAllViews();
+        cancelAllTasks();
+
+        Context context = getContext();
+        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));
+        }
+
+        for (int i = 0; i < mNumAppsPages; ++i) {
+            PagedViewCellLayout layout = new PagedViewCellLayout(context);
+            setupPage(layout);
+            addView(layout);
+        }
+    }
+
+    @Override
+    public void syncPageItems(int page, boolean immediate) {
+        if (page < mNumAppsPages) {
+            syncAppsPageItems(page, immediate);
+        } else {
+            syncWidgetPageItems(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) {
+        final boolean isRtl = isLayoutRtl();
+        super.screenScrolled(screenCenter);
+
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getPageAt(i);
+            if (v != null) {
+                float scrollProgress = getScrollProgress(screenCenter, v, i);
+
+                float interpolatedProgress;
+                float translationX;
+                float maxScrollProgress = Math.max(0, scrollProgress);
+                float minScrollProgress = Math.min(0, scrollProgress);
+
+                if (isRtl) {
+                    translationX = maxScrollProgress * v.getMeasuredWidth();
+                    interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(maxScrollProgress));
+                } else {
+                    translationX = minScrollProgress * v.getMeasuredWidth();
+                    interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(minScrollProgress));
+                }
+                float scale = (1 - interpolatedProgress) +
+                        interpolatedProgress * TRANSITION_SCALE_FACTOR;
+
+                float alpha;
+                if (isRtl && (scrollProgress > 0)) {
+                    alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(maxScrollProgress));
+                } else if (!isRtl && (scrollProgress < 0)) {
+                    alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(scrollProgress));
+                } else {
+                    //  On large screens we need to fade the page as it nears its leftmost position
+                    alpha = mLeftScreenAlphaInterpolator.getInterpolation(1 - scrollProgress);
+                }
+
+                v.setCameraDistance(mDensity * CAMERA_DISTANCE);
+                int pageWidth = v.getMeasuredWidth();
+                int pageHeight = v.getMeasuredHeight();
+
+                if (PERFORM_OVERSCROLL_ROTATION) {
+                    float xPivot = isRtl ? 1f - TRANSITION_PIVOT : TRANSITION_PIVOT;
+                    boolean isOverscrollingFirstPage = isRtl ? scrollProgress > 0 : scrollProgress < 0;
+                    boolean isOverscrollingLastPage = isRtl ? scrollProgress < 0 : scrollProgress > 0;
+
+                    if (i == 0 && isOverscrollingFirstPage) {
+                        // Overscroll to the left
+                        v.setPivotX(xPivot * pageWidth);
+                        v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress);
+                        scale = 1.0f;
+                        alpha = 1.0f;
+                        // On the first page, we don't want the page to have any lateral motion
+                        translationX = 0;
+                    } else if (i == getChildCount() - 1 && isOverscrollingLastPage) {
+                        // Overscroll to the right
+                        v.setPivotX((1 - xPivot) * pageWidth);
+                        v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress);
+                        scale = 1.0f;
+                        alpha = 1.0f;
+                        // On the last page, we don't want the page to have any lateral motion.
+                        translationX = 0;
+                    } else {
+                        v.setPivotY(pageHeight / 2.0f);
+                        v.setPivotX(pageWidth / 2.0f);
+                        v.setRotationY(0f);
+                    }
+                }
+
+                v.setTranslationX(translationX);
+                v.setScaleX(scale);
+                v.setScaleY(scale);
+                v.setAlpha(alpha);
+
+                // If the view has 0 alpha, we set it to be invisible so as to prevent
+                // it from accepting touches
+                if (alpha == 0) {
+                    v.setVisibility(INVISIBLE);
+                } else if (v.getVisibility() != VISIBLE) {
+                    v.setVisibility(VISIBLE);
+                }
+            }
+        }
+
+        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) {
+        acceleratedOverScroll(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<ApplicationInfo> list) {
+        mApps = list;
+        Collections.sort(mApps, LauncherModel.getAppNameComparator());
+        updatePageCountsAndInvalidateData();
+    }
+    private void addAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) {
+        // We add it in place, in alphabetical order
+        int count = list.size();
+        for (int i = 0; i < count; ++i) {
+            ApplicationInfo info = list.get(i);
+            int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator());
+            if (index < 0) {
+                mApps.add(-(index + 1), info);
+            }
+        }
+    }
+    public void addApps(ArrayList<ApplicationInfo> list) {
+        addAppsWithoutInvalidate(list);
+        updatePageCountsAndInvalidateData();
+    }
+    private int findAppByComponent(List<ApplicationInfo> list, ApplicationInfo item) {
+        ComponentName removeComponent = item.intent.getComponent();
+        int length = list.size();
+        for (int i = 0; i < length; ++i) {
+            ApplicationInfo info = list.get(i);
+            if (info.intent.getComponent().equals(removeComponent)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+    private void removeAppsWithoutInvalidate(ArrayList<ApplicationInfo> 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) {
+            ApplicationInfo info = list.get(i);
+            int removeIndex = findAppByComponent(mApps, info);
+            if (removeIndex > -1) {
+                mApps.remove(removeIndex);
+            }
+        }
+    }
+    public void removeApps(ArrayList<ApplicationInfo> appInfos) {
+        removeAppsWithoutInvalidate(appInfos);
+        updatePageCountsAndInvalidateData();
+    }
+    public void updateApps(ArrayList<ApplicationInfo> 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.
+        removeAppsWithoutInvalidate(list);
+        addAppsWithoutInvalidate(list);
+        updatePageCountsAndInvalidateData();
+    }
+
+    public void reset() {
+        // If we have reset, then we should not continue to restore the previous state
+        mSaveInstanceStateItemIndex = -1;
+
+        AppsCustomizeTabHost tabHost = getTabHost();
+        String tag = tabHost.getCurrentTabTag();
+        if (tag != null) {
+            if (!tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) {
+                tabHost.setCurrentTabFromContent(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.
+        ApplicationInfo.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();
+    }
+
+    @Override
+    public void iconPressed(PagedViewIcon icon) {
+        // Reset the previously pressed icon and store a reference to the pressed icon so that
+        // we can reset it on return to Launcher (in Launcher.onResume())
+        if (mPressedIcon != null) {
+            mPressedIcon.resetDrawableState();
+        }
+        mPressedIcon = icon;
+    }
+
+    public void resetDrawableState() {
+        if (mPressedIcon != null) {
+            mPressedIcon.resetDrawableState();
+            mPressedIcon = null;
+        }
+    }
+
+    /*
+     * 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;
+    }
+
+    @Override
+    protected String getCurrentPageDescription() {
+        int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
+        int stringId = R.string.default_scroll_format;
+        int count = 0;
+        
+        if (page < mNumAppsPages) {
+            stringId = R.string.apps_customize_apps_scroll_format;
+            count = mNumAppsPages;
+        } else {
+            page -= mNumAppsPages;
+            stringId = R.string.apps_customize_widgets_scroll_format;
+            count = mNumWidgetPages;
+        }
+
+        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
new file mode 100644
index 0000000..5d50fec
--- /dev/null
+++ b/src/com/android/launcher3/AppsCustomizeTabHost.java
@@ -0,0 +1,482 @@
+/*
+ * 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.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+
+public class AppsCustomizeTabHost extends TabHost implements LauncherTransitionable,
+        TabHost.OnTabChangeListener  {
+    static final String LOG_TAG = "AppsCustomizeTabHost";
+
+    private static final String APPS_TAB_TAG = "APPS";
+    private static final String WIDGETS_TAB_TAG = "WIDGETS";
+
+    private final LayoutInflater mLayoutInflater;
+    private ViewGroup mTabs;
+    private ViewGroup mTabsContainer;
+    private AppsCustomizePagedView mAppsCustomizePane;
+    private FrameLayout mAnimationBuffer;
+    private LinearLayout mContent;
+
+    private boolean mInTransition;
+    private boolean mTransitioningToWorkspace;
+    private boolean mResetAfterTransition;
+    private Runnable mRelayoutAndMakeVisible;
+
+    public AppsCustomizeTabHost(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mLayoutInflater = LayoutInflater.from(context);
+        mRelayoutAndMakeVisible = new Runnable() {
+                public void run() {
+                    mTabs.requestLayout();
+                    mTabsContainer.setAlpha(1f);
+                }
+            };
+    }
+
+    /**
+     * 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) {
+        setOnTabChangedListener(null);
+        onTabChangedStart();
+        onTabChangedEnd(type);
+        setCurrentTabByTag(getTabTagForContentType(type));
+        setOnTabChangedListener(this);
+    }
+    void selectAppsTab() {
+        setContentTypeImmediate(AppsCustomizePagedView.ContentType.Applications);
+    }
+    void selectWidgetsTab() {
+        setContentTypeImmediate(AppsCustomizePagedView.ContentType.Widgets);
+    }
+
+    /**
+     * Setup the tab host and create all necessary tabs.
+     */
+    @Override
+    protected void onFinishInflate() {
+        // Setup the tab host
+        setup();
+
+        final ViewGroup tabsContainer = (ViewGroup) findViewById(R.id.tabs_container);
+        final TabWidget tabs = getTabWidget();
+        final AppsCustomizePagedView appsCustomizePane = (AppsCustomizePagedView)
+                findViewById(R.id.apps_customize_pane_content);
+        mTabs = tabs;
+        mTabsContainer = tabsContainer;
+        mAppsCustomizePane = appsCustomizePane;
+        mAnimationBuffer = (FrameLayout) findViewById(R.id.animation_buffer);
+        mContent = (LinearLayout) findViewById(R.id.apps_customize_content);
+        if (tabs == null || mAppsCustomizePane == null) throw new Resources.NotFoundException();
+
+        // Configure the tabs content factory to return the same paged view (that we change the
+        // content filter on)
+        TabContentFactory contentFactory = new TabContentFactory() {
+            public View createTabContent(String tag) {
+                return appsCustomizePane;
+            }
+        };
+
+        // Create the tabs
+        TextView tabView;
+        String label;
+        label = getContext().getString(R.string.all_apps_button_label);
+        tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false);
+        tabView.setText(label);
+        tabView.setContentDescription(label);
+        addTab(newTabSpec(APPS_TAB_TAG).setIndicator(tabView).setContent(contentFactory));
+        label = getContext().getString(R.string.widgets_tab_label);
+        tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false);
+        tabView.setText(label);
+        tabView.setContentDescription(label);
+        addTab(newTabSpec(WIDGETS_TAB_TAG).setIndicator(tabView).setContent(contentFactory));
+        setOnTabChangedListener(this);
+
+        // Setup the key listener to jump between the last tab view and the market icon
+        AppsCustomizeTabKeyEventListener keyListener = new AppsCustomizeTabKeyEventListener();
+        View lastTab = tabs.getChildTabViewAt(tabs.getTabCount() - 1);
+        lastTab.setOnKeyListener(keyListener);
+        View shopButton = findViewById(R.id.market_button);
+        shopButton.setOnKeyListener(keyListener);
+
+        // Hide the tab bar until we measure
+        mTabsContainer.setAlpha(0f);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        boolean remeasureTabWidth = (mTabs.getLayoutParams().width <= 0);
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // Set the width of the tab list to the content width
+        if (remeasureTabWidth) {
+            int contentWidth = mAppsCustomizePane.getPageContentWidth();
+            if (contentWidth > 0 && mTabs.getLayoutParams().width != contentWidth) {
+                // Set the width and show the tab bar
+                mTabs.getLayoutParams().width = contentWidth;
+                mRelayoutAndMakeVisible.run();
+            }
+
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+     public boolean onInterceptTouchEvent(MotionEvent ev) {
+         // If we are mid transitioning to the workspace, then intercept touch events here so we
+         // can ignore them, otherwise we just let all apps handle the touch events.
+         if (mInTransition && mTransitioningToWorkspace) {
+             return true;
+         }
+         return super.onInterceptTouchEvent(ev);
+     };
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Allow touch events to fall through to the workspace if we are transitioning there
+        if (mInTransition && mTransitioningToWorkspace) {
+            return super.onTouchEvent(event);
+        }
+
+        // Intercept all touch events up to the bottom of the AppsCustomizePane so they do not fall
+        // through to the workspace and trigger showWorkspace()
+        if (event.getY() < mAppsCustomizePane.getBottom()) {
+            return true;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    private void onTabChangedStart() {
+        mAppsCustomizePane.hideScrollingIndicator(false);
+    }
+
+    private void reloadCurrentPage() {
+        if (!LauncherApplication.isScreenLarge()) {
+            mAppsCustomizePane.flashScrollingIndicator(true);
+        }
+        mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage());
+        mAppsCustomizePane.requestFocus();
+    }
+
+    private void onTabChangedEnd(AppsCustomizePagedView.ContentType type) {
+        mAppsCustomizePane.setContentType(type);
+    }
+
+    @Override
+    public void onTabChanged(String tabId) {
+        final AppsCustomizePagedView.ContentType type = getContentTypeForTabTag(tabId);
+
+        // Animate the changing of the tab content by fading pages in and out
+        final Resources res = getResources();
+        final int duration = res.getInteger(R.integer.config_tabTransitionDuration);
+
+        // We post a runnable here because there is a delay while the first page is loading and
+        // the feedback from having changed the tab almost feels better than having it stick
+        post(new Runnable() {
+            @Override
+            public void run() {
+                if (mAppsCustomizePane.getMeasuredWidth() <= 0 ||
+                        mAppsCustomizePane.getMeasuredHeight() <= 0) {
+                    reloadCurrentPage();
+                    return;
+                }
+
+                // Take the visible pages and re-parent them temporarily to mAnimatorBuffer
+                // and then cross fade to the new pages
+                int[] visiblePageRange = new int[2];
+                mAppsCustomizePane.getVisiblePages(visiblePageRange);
+                if (visiblePageRange[0] == -1 && visiblePageRange[1] == -1) {
+                    // If we can't get the visible page ranges, then just skip the animation
+                    reloadCurrentPage();
+                    return;
+                }
+                ArrayList<View> visiblePages = new ArrayList<View>();
+                for (int i = visiblePageRange[0]; i <= visiblePageRange[1]; i++) {
+                    visiblePages.add(mAppsCustomizePane.getPageAt(i));
+                }
+
+                // We want the pages to be rendered in exactly the same way as they were when
+                // their parent was mAppsCustomizePane -- so set the scroll on mAnimationBuffer
+                // to be exactly the same as mAppsCustomizePane, and below, set the left/top
+                // parameters to be correct for each of the pages
+                mAnimationBuffer.scrollTo(mAppsCustomizePane.getScrollX(), 0);
+
+                // mAppsCustomizePane renders its children in reverse order, so
+                // add the pages to mAnimationBuffer in reverse order to match that behavior
+                for (int i = visiblePages.size() - 1; i >= 0; i--) {
+                    View child = visiblePages.get(i);
+                    if (child instanceof PagedViewCellLayout) {
+                        ((PagedViewCellLayout) child).resetChildrenOnKeyListeners();
+                    } else if (child instanceof PagedViewGridLayout) {
+                        ((PagedViewGridLayout) child).resetChildrenOnKeyListeners();
+                    }
+                    PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(false);
+                    mAppsCustomizePane.removeView(child);
+                    PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(true);
+                    mAnimationBuffer.setAlpha(1f);
+                    mAnimationBuffer.setVisibility(View.VISIBLE);
+                    LayoutParams p = new FrameLayout.LayoutParams(child.getMeasuredWidth(),
+                            child.getMeasuredHeight());
+                    p.setMargins((int) child.getLeft(), (int) child.getTop(), 0, 0);
+                    mAnimationBuffer.addView(child, p);
+                }
+
+                // Toggle the new content
+                onTabChangedStart();
+                onTabChangedEnd(type);
+
+                // Animate the transition
+                ObjectAnimator outAnim = LauncherAnimUtils.ofFloat(mAnimationBuffer, "alpha", 0f);
+                outAnim.addListener(new AnimatorListenerAdapter() {
+                    private void clearAnimationBuffer() {
+                        mAnimationBuffer.setVisibility(View.GONE);
+                        PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(false);
+                        mAnimationBuffer.removeAllViews();
+                        PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(true);
+                    }
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        clearAnimationBuffer();
+                    }
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        clearAnimationBuffer();
+                    }
+                });
+                ObjectAnimator inAnim = LauncherAnimUtils.ofFloat(mAppsCustomizePane, "alpha", 1f);
+                inAnim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        reloadCurrentPage();
+                    }
+                });
+
+                final AnimatorSet animSet = LauncherAnimUtils.createAnimatorSet();
+                animSet.playTogether(outAnim, inAnim);
+                animSet.setDuration(duration);
+                animSet.start();
+            }
+        });
+    }
+
+    public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) {
+        setOnTabChangedListener(null);
+        setCurrentTabByTag(getTabTagForContentType(type));
+        setOnTabChangedListener(this);
+    }
+
+    /**
+     * 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() {
+        if (mInTransition) {
+            // Defer to after the transition to reset
+            mResetAfterTransition = true;
+        } else {
+            // Reset immediately
+            mAppsCustomizePane.reset();
+        }
+    }
+
+    private void enableAndBuildHardwareLayer() {
+        // isHardwareAccelerated() checks if we're attached to a window and if that
+        // window is HW accelerated-- we were sometimes not attached to a window
+        // and buildLayer was throwing an IllegalStateException
+        if (isHardwareAccelerated()) {
+            // Turn on hardware layers for performance
+            setLayerType(LAYER_TYPE_HARDWARE, null);
+
+            // force building the layer, so you don't get a blip early in an animation
+            // when the layer is created layer
+            buildLayer();
+        }
+    }
+
+    @Override
+    public View getContent() {
+        return mContent;
+    }
+
+    /* LauncherTransitionable overrides */
+    @Override
+    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+        mAppsCustomizePane.onLauncherTransitionPrepare(l, animated, toWorkspace);
+        mInTransition = true;
+        mTransitioningToWorkspace = toWorkspace;
+
+        if (toWorkspace) {
+            // Going from All Apps -> Workspace
+            setVisibilityOfSiblingsWithLowerZOrder(VISIBLE);
+            // Stop the scrolling indicator - we don't want All Apps to be invalidating itself
+            // during the transition, especially since it has a hardware layer set on it
+            mAppsCustomizePane.cancelScrollingIndicatorAnimations();
+        } 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)
+            mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true);
+
+            if (!LauncherApplication.isScreenLarge()) {
+                mAppsCustomizePane.showScrollingIndicator(true);
+            }
+        }
+
+        if (mResetAfterTransition) {
+            mAppsCustomizePane.reset();
+            mResetAfterTransition = false;
+        }
+    }
+
+    @Override
+    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
+        if (animated) {
+            enableAndBuildHardwareLayer();
+        }
+    }
+
+    @Override
+    public void onLauncherTransitionStep(Launcher l, float t) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
+        mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace);
+        mInTransition = false;
+        if (animated) {
+            setLayerType(LAYER_TYPE_NONE, null);
+        }
+
+        if (!toWorkspace) {
+            // Dismiss the workspace cling
+            l.dismissWorkspaceCling(null);
+            // Show the all apps cling (if not already shown)
+            mAppsCustomizePane.showAllAppsCling();
+            // Make sure adjacent pages are loaded (we wait until after the transition to
+            // prevent slowing down the animation)
+            mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage());
+
+            if (!LauncherApplication.isScreenLarge()) {
+                mAppsCustomizePane.hideScrollingIndicator(false);
+            }
+
+            // 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;
+
+        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) {
+                        continue;
+                    }
+                    child.setVisibility(visibility);
+                }
+            }
+        } else {
+            throw new RuntimeException("Failed; can't get z-order of views");
+        }
+    }
+
+    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
+            mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true);
+            mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage());
+        }
+    }
+
+    public void onTrimMemory() {
+        mContent.setVisibility(GONE);
+        // Clear the widget pages of all their subviews - this will trigger the widget previews
+        // to delete their bitmaps
+        mAppsCustomizePane.clearAllWidgetPages();
+    }
+
+    boolean isTransitioning() {
+        return mInTransition;
+    }
+}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
new file mode 100644
index 0000000..7cac8a6
--- /dev/null
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -0,0 +1,342 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Region.Op;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * 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 {
+    static final float CORNER_RADIUS = 4.0f;
+    static final float SHADOW_LARGE_RADIUS = 4.0f;
+    static final float SHADOW_SMALL_RADIUS = 1.75f;
+    static final float SHADOW_Y_OFFSET = 2.0f;
+    static final int SHADOW_LARGE_COLOUR = 0xDD000000;
+    static final int SHADOW_SMALL_COLOUR = 0xCC000000;
+    static final float PADDING_H = 8.0f;
+    static final float PADDING_V = 3.0f;
+
+    private int mPrevAlpha = -1;
+
+    private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper();
+    private final Canvas mTempCanvas = new Canvas();
+    private final Rect mTempRect = new Rect();
+    private boolean mDidInvalidateForPressedState;
+    private Bitmap mPressedOrFocusedBackground;
+    private int mFocusedOutlineColor;
+    private int mFocusedGlowColor;
+    private int mPressedOutlineColor;
+    private int mPressedGlowColor;
+
+    private boolean mBackgroundSizeChanged;
+    private Drawable mBackground;
+
+    private boolean mStayPressed;
+    private CheckLongPressHelper mLongPressHelper;
+
+    public BubbleTextView(Context context) {
+        super(context);
+        init();
+    }
+
+    public BubbleTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    private void init() {
+        mLongPressHelper = new CheckLongPressHelper(this);
+        mBackground = getBackground();
+
+        final Resources res = getContext().getResources();
+        mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor =
+            res.getColor(android.R.color.holo_blue_light);
+
+        setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
+    }
+
+    public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {
+        Bitmap b = info.getIcon(iconCache);
+
+        setCompoundDrawablesWithIntrinsicBounds(null,
+                new FastBitmapDrawable(b),
+                null, null);
+        setText(info.title);
+        setTag(info);
+    }
+
+    @Override
+    protected boolean setFrame(int left, int top, int right, int bottom) {
+        if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) {
+            mBackgroundSizeChanged = true;
+        }
+        return super.setFrame(left, top, right, bottom);
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return who == mBackground || super.verifyDrawable(who);
+    }
+
+    @Override
+    public void setTag(Object tag) {
+        if (tag != null) {
+            LauncherModel.checkItemInfo((ItemInfo) tag);
+        }
+        super.setTag(tag);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        if (isPressed()) {
+            // In this case, we have already created the pressed outline on ACTION_DOWN,
+            // so we just need to do an invalidate to trigger draw
+            if (!mDidInvalidateForPressedState) {
+                setCellLayoutPressedOrFocusedIcon();
+            }
+        } else {
+            // Otherwise, either clear the pressed/focused background, or create a background
+            // for the focused state
+            final boolean backgroundEmptyBefore = mPressedOrFocusedBackground == null;
+            if (!mStayPressed) {
+                mPressedOrFocusedBackground = null;
+            }
+            if (isFocused()) {
+                if (getLayout() == null) {
+                    // In some cases, we get focus before we have been layed out. Set the
+                    // background to null so that it will get created when the view is drawn.
+                    mPressedOrFocusedBackground = null;
+                } else {
+                    mPressedOrFocusedBackground = createGlowingOutline(
+                            mTempCanvas, mFocusedGlowColor, mFocusedOutlineColor);
+                }
+                mStayPressed = false;
+                setCellLayoutPressedOrFocusedIcon();
+            }
+            final boolean backgroundEmptyNow = mPressedOrFocusedBackground == null;
+            if (!backgroundEmptyBefore && backgroundEmptyNow) {
+                setCellLayoutPressedOrFocusedIcon();
+            }
+        }
+
+        Drawable d = mBackground;
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+        super.drawableStateChanged();
+    }
+
+    /**
+     * Draw this BubbleTextView into the given Canvas.
+     *
+     * @param destCanvas the canvas to draw on
+     * @param padding the horizontal and vertical padding to use when drawing
+     */
+    private void drawWithPadding(Canvas destCanvas, int padding) {
+        final Rect clipRect = mTempRect;
+        getDrawingRect(clipRect);
+
+        // adjust the clip rect so that we don't include the text label
+        clipRect.bottom =
+            getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + 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
+        destCanvas.save();
+        destCanvas.scale(getScaleX(), getScaleY(),
+                (getWidth() + padding) / 2, (getHeight() + padding) / 2);
+        destCanvas.translate(-getScrollX() + padding / 2, -getScrollY() + padding / 2);
+        destCanvas.clipRect(clipRect, Op.REPLACE);
+        draw(destCanvas);
+        destCanvas.restore();
+    }
+
+    /**
+     * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location.
+     * Responsibility for the bitmap is transferred to the caller.
+     */
+    private Bitmap createGlowingOutline(Canvas canvas, int outlineColor, int glowColor) {
+        final int padding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS;
+        final Bitmap b = Bitmap.createBitmap(
+                getWidth() + padding, getHeight() + padding, Bitmap.Config.ARGB_8888);
+
+        canvas.setBitmap(b);
+        drawWithPadding(canvas, padding);
+        mOutlineHelper.applyExtraThickExpensiveOutlineWithBlur(b, canvas, glowColor, outlineColor);
+        canvas.setBitmap(null);
+
+        return b;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Call the superclass onTouchEvent first, because sometimes it changes the state to
+        // isPressed() on an ACTION_UP
+        boolean result = super.onTouchEvent(event);
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                // So that the pressed outline is visible immediately when isPressed() is true,
+                // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
+                // to create it)
+                if (mPressedOrFocusedBackground == null) {
+                    mPressedOrFocusedBackground = createGlowingOutline(
+                            mTempCanvas, mPressedGlowColor, mPressedOutlineColor);
+                }
+                // Invalidate so the pressed state is visible, or set a flag so we know that we
+                // have to call invalidate as soon as the state is "pressed"
+                if (isPressed()) {
+                    mDidInvalidateForPressedState = true;
+                    setCellLayoutPressedOrFocusedIcon();
+                } else {
+                    mDidInvalidateForPressedState = false;
+                }
+
+                mLongPressHelper.postCheckForLongPress();
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                // If we've touched down and up on an item, and it's still not "pressed", then
+                // destroy the pressed outline
+                if (!isPressed()) {
+                    mPressedOrFocusedBackground = null;
+                }
+
+                mLongPressHelper.cancelLongPress();
+                break;
+        }
+        return result;
+    }
+
+    void setStayPressed(boolean stayPressed) {
+        mStayPressed = stayPressed;
+        if (!stayPressed) {
+            mPressedOrFocusedBackground = null;
+        }
+        setCellLayoutPressedOrFocusedIcon();
+    }
+
+    void setCellLayoutPressedOrFocusedIcon() {
+        if (getParent() instanceof ShortcutAndWidgetContainer) {
+            ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent();
+            if (parent != null) {
+                CellLayout layout = (CellLayout) parent.getParent();
+                layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null);
+            }
+        }
+    }
+
+    void clearPressedOrFocusedBackground() {
+        mPressedOrFocusedBackground = null;
+        setCellLayoutPressedOrFocusedIcon();
+    }
+
+    Bitmap getPressedOrFocusedBackground() {
+        return mPressedOrFocusedBackground;
+    }
+
+    int getPressedOrFocusedBackgroundPadding() {
+        return HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        final Drawable background = mBackground;
+        if (background != null) {
+            final int scrollX = getScrollX();
+            final int scrollY = getScrollY();
+
+            if (mBackgroundSizeChanged) {
+                background.setBounds(0, 0,  getRight() - getLeft(), getBottom() - getTop());
+                mBackgroundSizeChanged = false;
+            }
+
+            if ((scrollX | scrollY) == 0) {
+                background.draw(canvas);
+            } else {
+                canvas.translate(scrollX, scrollY);
+                background.draw(canvas);
+                canvas.translate(-scrollX, -scrollY);
+            }
+        }
+
+        // If text is transparent, don't draw any shadow
+        if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) {
+            getPaint().clearShadowLayer();
+            super.draw(canvas);
+            return;
+        }
+
+        // We enhance the shadow by drawing the shadow twice
+        getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
+        super.draw(canvas);
+        canvas.save(Canvas.CLIP_SAVE_FLAG);
+        canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(),
+                getScrollX() + getWidth(),
+                getScrollY() + getHeight(), Region.Op.INTERSECT);
+        getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR);
+        super.draw(canvas);
+        canvas.restore();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mBackground != null) mBackground.setCallback(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mBackground != null) mBackground.setCallback(null);
+    }
+
+    @Override
+    protected boolean onSetAlpha(int alpha) {
+        if (mPrevAlpha != alpha) {
+            mPrevAlpha = alpha;
+            super.onSetAlpha(alpha);
+        }
+        return true;
+    }
+
+    @Override
+    public void cancelLongPress() {
+        super.cancelLongPress();
+
+        mLongPressHelper.cancelLongPress();
+    }
+}
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
new file mode 100644
index 0000000..a7486a8
--- /dev/null
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -0,0 +1,166 @@
+/*
+ * 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.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+
+/**
+ * Implements a DropTarget.
+ */
+public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener {
+
+    protected final int mTransitionDuration;
+
+    protected Launcher mLauncher;
+    private int mBottomDragPadding;
+    protected TextView mText;
+    protected SearchDropTargetBar mSearchDropTargetBar;
+
+    /** Whether this drop target is active for the current drag */
+    protected boolean mActive;
+
+    /** The paint applied to the drag view on hover */
+    protected int mHoverColor = 0;
+
+    public ButtonDropTarget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        Resources r = getResources();
+        mTransitionDuration = r.getInteger(R.integer.config_dropTargetBgTransitionDuration);
+        mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding);
+    }
+
+    void setLauncher(Launcher launcher) {
+        mLauncher = launcher;
+    }
+
+    public boolean acceptDrop(DragObject d) {
+        return false;
+    }
+
+    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];
+            }
+        }
+        return null;
+    }
+
+    public void onDrop(DragObject d) {
+    }
+
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
+    public void onDragEnter(DragObject d) {
+        d.dragView.setColor(mHoverColor);
+    }
+
+    public void onDragOver(DragObject d) {
+        // Do nothing
+    }
+
+    public void onDragExit(DragObject d) {
+        d.dragView.setColor(0);
+    }
+
+    public void onDragStart(DragSource source, Object info, int dragAction) {
+        // Do nothing
+    }
+
+    public boolean isDropEnabled() {
+        return mActive;
+    }
+
+    public void onDragEnd() {
+        // Do nothing
+    }
+
+    @Override
+    public void getHitRect(android.graphics.Rect outRect) {
+        super.getHitRect(outRect);
+        outRect.bottom += mBottomDragPadding;
+    }
+
+    private boolean isRtl() {
+        return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+    }
+
+    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)
+        Rect to = new Rect();
+        dragLayer.getViewRectRelativeToSelf(this, to);
+
+        final int width = drawableWidth;
+        final int height = drawableHeight;
+
+        final int left;
+        final int right;
+
+        if (isRtl()) {
+            right = to.right - getPaddingRight();
+            left = right - width;
+        } else {
+            left = to.left + getPaddingLeft();
+            right = left + width;
+        }
+
+        final int top = to.top + (getMeasuredHeight() - height) / 2;
+        final int bottom = top +  height;
+
+        to.set(left, top, right, bottom);
+
+        // Center the destination rect about the trash icon
+        final int xOffset = (int) -(viewWidth - width) / 2;
+        final int yOffset = (int) -(viewHeight - height) / 2;
+        to.offset(xOffset, yOffset);
+
+        return to;
+    }
+
+    @Override
+    public DropTarget getDropTargetDelegate(DragObject d) {
+        return null;
+    }
+
+    public void getLocationInDragLayer(int[] loc) {
+        mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
+    }
+}
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
new file mode 100644
index 0000000..842037c
--- /dev/null
+++ b/src/com/android/launcher3/CellLayout.java
@@ -0,0 +1,3338 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LayoutAnimationController;
+
+import com.android.launcher3.R;
+import com.android.launcher3.FolderIcon.FolderRingAnimator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Stack;
+
+public class CellLayout extends ViewGroup {
+    static final String TAG = "CellLayout";
+
+    private Launcher mLauncher;
+    private int mCellWidth;
+    private int mCellHeight;
+
+    private int mCountX;
+    private int mCountY;
+
+    private int mOriginalWidthGap;
+    private int mOriginalHeightGap;
+    private int mWidthGap;
+    private int mHeightGap;
+    private int mMaxGap;
+    private boolean mScrollingTransformsDirty = false;
+
+    private final Rect mRect = new Rect();
+    private final CellInfo mCellInfo = new CellInfo();
+
+    // 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];
+
+    boolean[][] mOccupied;
+    boolean[][] mTmpOccupied;
+    private boolean mLastDownOnOccupiedCell = false;
+
+    private OnTouchListener mInterceptTouchListener;
+
+    private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>();
+    private int[] mFolderLeaveBehindCell = {-1, -1};
+
+    private int mForegroundAlpha = 0;
+    private float mBackgroundAlpha;
+    private float mBackgroundAlphaMultiplier = 1.0f;
+
+    private Drawable mNormalBackground;
+    private Drawable mActiveGlowBackground;
+    private Drawable mOverScrollForegroundDrawable;
+    private Drawable mOverScrollLeft;
+    private Drawable mOverScrollRight;
+    private Rect mBackgroundRect;
+    private Rect mForegroundRect;
+    private int mForegroundPadding;
+
+    // If we're actively dragging something over this screen, mIsDragOverlapping is true
+    private boolean mIsDragOverlapping = false;
+    private final Point mDragCenter = new Point();
+
+    // 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];
+    private InterruptibleInOutAnimator[] mDragOutlineAnims =
+            new InterruptibleInOutAnimator[mDragOutlines.length];
+
+    // Used as an index into the above 3 arrays; indicates which is the most current value.
+    private int mDragOutlineCurrent = 0;
+    private final Paint mDragOutlinePaint = new Paint();
+
+    private BubbleTextView mPressedOrFocusedIcon;
+
+    private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new
+            HashMap<CellLayout.LayoutParams, Animator>();
+    private HashMap<View, ReorderHintAnimation>
+            mShakeAnimators = new HashMap<View, ReorderHintAnimation>();
+
+    private boolean mItemPlacementDirty = false;
+
+    // When a drag operation is in progress, holds the nearest cell to the touch point
+    private final int[] mDragCell = new int[2];
+
+    private boolean mDragging = false;
+
+    private TimeInterpolator mEaseOutInterpolator;
+    private ShortcutAndWidgetContainer mShortcutsAndWidgets;
+
+    private boolean mIsHotseat = false;
+    private float mHotseatScale = 1f;
+
+    public static final int MODE_DRAG_OVER = 0;
+    public static final int MODE_ON_DROP = 1;
+    public static final int MODE_ON_DROP_EXTERNAL = 2;
+    public static final int MODE_ACCEPT_DROP = 3;
+    private static final boolean DESTRUCTIVE_REORDER = false;
+    private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;
+
+    static final int LANDSCAPE = 0;
+    static final int PORTRAIT = 1;
+
+    private static final float REORDER_HINT_MAGNITUDE = 0.12f;
+    private static final int REORDER_ANIMATION_DURATION = 150;
+    private float mReorderHintAnimationMagnitude;
+
+    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 final static PorterDuffXfermode sAddBlendMode =
+            new PorterDuffXfermode(PorterDuff.Mode.ADD);
+    private final static Paint sPaint = new Paint();
+
+    public CellLayout(Context context) {
+        this(context, null);
+    }
+
+    public CellLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    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.
+        setWillNotDraw(false);
+        setClipToPadding(false);
+        mLauncher = (Launcher) context;
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0);
+
+        mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10);
+        mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10);
+        mWidthGap = mOriginalWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, 0);
+        mHeightGap = mOriginalHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, 0);
+        mMaxGap = a.getDimensionPixelSize(R.styleable.CellLayout_maxGap, 0);
+        mCountX = LauncherModel.getCellCountX();
+        mCountY = LauncherModel.getCellCountY();
+        mOccupied = new boolean[mCountX][mCountY];
+        mTmpOccupied = new boolean[mCountX][mCountY];
+        mPreviousReorderDirection[0] = INVALID_DIRECTION;
+        mPreviousReorderDirection[1] = INVALID_DIRECTION;
+
+        a.recycle();
+
+        setAlwaysDrawnWithCacheEnabled(false);
+
+        final Resources res = getResources();
+        mHotseatScale = (res.getInteger(R.integer.hotseat_item_scale_percentage) / 100f);
+
+        mNormalBackground = res.getDrawable(R.drawable.homescreen_blue_normal_holo);
+        mActiveGlowBackground = res.getDrawable(R.drawable.homescreen_blue_strong_holo);
+
+        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);
+
+        mReorderHintAnimationMagnitude = (REORDER_HINT_MAGNITUDE *
+                res.getDimensionPixelSize(R.dimen.app_icon_size));
+
+        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;
+        for (int i = 0; i < mDragOutlines.length; i++) {
+            mDragOutlines[i] = new Rect(-1, -1, -1, -1);
+        }
+
+        // When dragging things around the home screens, we show a green outline of
+        // where the item will land. The outlines gradually fade out, leaving a trail
+        // behind the drag path.
+        // Set up all the animations that are used to implement this fading.
+        final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime);
+        final float fromAlphaValue = 0;
+        final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha);
+
+        Arrays.fill(mDragOutlineAlphas, fromAlphaValue);
+
+        for (int i = 0; i < mDragOutlineAnims.length; i++) {
+            final InterruptibleInOutAnimator anim =
+                new InterruptibleInOutAnimator(this, duration, fromAlphaValue, toAlphaValue);
+            anim.getAnimator().setInterpolator(mEaseOutInterpolator);
+            final int thisIndex = i;
+            anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final Bitmap outline = (Bitmap)anim.getTag();
+
+                    // If an animation is started and then stopped very quickly, we can still
+                    // get spurious updates we've cleared the tag. Guard against this.
+                    if (outline == null) {
+                        @SuppressWarnings("all") // suppress dead code warning
+                        final boolean debug = false;
+                        if (debug) {
+                            Object val = animation.getAnimatedValue();
+                            Log.d(TAG, "anim " + thisIndex + " update: " + val +
+                                     ", isStopped " + anim.isStopped());
+                        }
+                        // Try to prevent it from continuing to run
+                        animation.cancel();
+                    } else {
+                        mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
+                        CellLayout.this.invalidate(mDragOutlines[thisIndex]);
+                    }
+                }
+            });
+            // The animation holds a reference to the drag outline bitmap as long is it's
+            // running. This way the bitmap can be GCed when the animations are complete.
+            anim.getAnimator().addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) {
+                        anim.setTag(null);
+                    }
+                }
+            });
+            mDragOutlineAnims[i] = anim;
+        }
+
+        mBackgroundRect = new Rect();
+        mForegroundRect = new Rect();
+
+        mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+                mCountX);
+
+        addView(mShortcutsAndWidgets);
+    }
+
+    static int widthInPortrait(Resources r, int numCells) {
+        // We use this method from Workspace to figure out how many rows/columns Launcher should
+        // have. We ignore the left/right padding on CellLayout because it turns out in our design
+        // the padding extends outside the visible screen size, but it looked fine anyway.
+        int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width);
+        int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap),
+                r.getDimensionPixelSize(R.dimen.workspace_height_gap));
+
+        return  minGap * (numCells - 1) + cellWidth * numCells;
+    }
+
+    static int heightInLandscape(Resources r, int numCells) {
+        // We use this method from Workspace to figure out how many rows/columns Launcher should
+        // have. We ignore the left/right padding on CellLayout because it turns out in our design
+        // the padding extends outside the visible screen size, but it looked fine anyway.
+        int cellHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height);
+        int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap),
+                r.getDimensionPixelSize(R.dimen.workspace_height_gap));
+
+        return minGap * (numCells - 1) + cellHeight * numCells;
+    }
+
+    public void enableHardwareLayers() {
+        mShortcutsAndWidgets.setLayerType(LAYER_TYPE_HARDWARE, sPaint);
+    }
+
+    public void disableHardwareLayers() {
+        mShortcutsAndWidgets.setLayerType(LAYER_TYPE_NONE, sPaint);
+    }
+
+    public void buildHardwareLayer() {
+        mShortcutsAndWidgets.buildLayer();
+    }
+
+    public float getChildrenScale() {
+        return mIsHotseat ? mHotseatScale : 1.0f;
+    }
+
+    public void setGridSize(int x, int y) {
+        mCountX = x;
+        mCountY = y;
+        mOccupied = new boolean[mCountX][mCountY];
+        mTmpOccupied = new boolean[mCountX][mCountY];
+        mTempRectStack.clear();
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+                mCountX);
+        requestLayout();
+    }
+
+    // Set whether or not to invert the layout horizontally if the layout is in RTL mode.
+    public void setInvertIfRtl(boolean invert) {
+        mShortcutsAndWidgets.setInvertIfRtl(invert);
+    }
+
+    private void invalidateBubbleTextView(BubbleTextView icon) {
+        final int padding = icon.getPressedOrFocusedBackgroundPadding();
+        invalidate(icon.getLeft() + getPaddingLeft() - padding,
+                icon.getTop() + getPaddingTop() - padding,
+                icon.getRight() + getPaddingLeft() + padding,
+                icon.getBottom() + getPaddingTop() + padding);
+    }
+
+    void setOverScrollAmount(float r, boolean left) {
+        if (left && mOverScrollForegroundDrawable != mOverScrollLeft) {
+            mOverScrollForegroundDrawable = mOverScrollLeft;
+        } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) {
+            mOverScrollForegroundDrawable = mOverScrollRight;
+        }
+
+        mForegroundAlpha = (int) Math.round((r * 255));
+        mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha);
+        invalidate();
+    }
+
+    void setPressedOrFocusedIcon(BubbleTextView icon) {
+        // We draw the pressed or focused BubbleTextView's background in CellLayout because it
+        // requires an expanded clip rect (due to the glow's blur radius)
+        BubbleTextView oldIcon = mPressedOrFocusedIcon;
+        mPressedOrFocusedIcon = icon;
+        if (oldIcon != null) {
+            invalidateBubbleTextView(oldIcon);
+        }
+        if (mPressedOrFocusedIcon != null) {
+            invalidateBubbleTextView(mPressedOrFocusedIcon);
+        }
+    }
+
+    void setIsDragOverlapping(boolean isDragOverlapping) {
+        if (mIsDragOverlapping != isDragOverlapping) {
+            mIsDragOverlapping = isDragOverlapping;
+            invalidate();
+        }
+    }
+
+    boolean getIsDragOverlapping() {
+        return mIsDragOverlapping;
+    }
+
+    protected void setOverscrollTransformsDirty(boolean dirty) {
+        mScrollingTransformsDirty = dirty;
+    }
+
+    protected void resetOverscrollTransforms() {
+        if (mScrollingTransformsDirty) {
+            setOverscrollTransformsDirty(false);
+            setTranslationX(0);
+            setRotationY(0);
+            // It doesn't matter if we pass true or false here, the important thing is that we
+            // pass 0, which results in the overscroll drawable not being drawn any more.
+            setOverScrollAmount(0, false);
+            setPivotX(getMeasuredWidth() / 2);
+            setPivotY(getMeasuredHeight() / 2);
+        }
+    }
+
+    public void scaleRect(Rect r, float scale) {
+        if (scale != 1.0f) {
+            r.left = (int) (r.left * scale + 0.5f);
+            r.top = (int) (r.top * scale + 0.5f);
+            r.right = (int) (r.right * scale + 0.5f);
+            r.bottom = (int) (r.bottom * scale + 0.5f);
+        }
+    }
+
+    Rect temp = new Rect();
+    void scaleRectAboutCenter(Rect in, Rect out, float scale) {
+        int cx = in.centerX();
+        int cy = in.centerY();
+        out.set(in);
+        out.offset(-cx, -cy);
+        scaleRect(out, scale);
+        out.offset(cx, cy);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // 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 (mBackgroundAlpha > 0.0f) {
+            Drawable bg;
+
+            if (mIsDragOverlapping) {
+                // 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);
+        }
+
+        final Paint paint = mDragOutlinePaint;
+        for (int i = 0; i < mDragOutlines.length; i++) {
+            final float alpha = mDragOutlineAlphas[i];
+            if (alpha > 0) {
+                final Rect r = mDragOutlines[i];
+                scaleRectAboutCenter(r, temp, getChildrenScale());
+                final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag();
+                paint.setAlpha((int)(alpha + .5f));
+                canvas.drawBitmap(b, null, temp, paint);
+            }
+        }
+
+        // We draw the pressed or focused BubbleTextView's background in CellLayout because it
+        // requires an expanded clip rect (due to the glow's blur radius)
+        if (mPressedOrFocusedIcon != null) {
+            final int padding = mPressedOrFocusedIcon.getPressedOrFocusedBackgroundPadding();
+            final Bitmap b = mPressedOrFocusedIcon.getPressedOrFocusedBackground();
+            if (b != null) {
+                canvas.drawBitmap(b,
+                        mPressedOrFocusedIcon.getLeft() + getPaddingLeft() - padding,
+                        mPressedOrFocusedIcon.getTop() + getPaddingTop() - padding,
+                        null);
+            }
+        }
+
+        if (DEBUG_VISUALIZE_OCCUPIED) {
+            int[] pt = new int[2];
+            ColorDrawable cd = new ColorDrawable(Color.RED);
+            cd.setBounds(0, 0,  mCellWidth, mCellHeight);
+            for (int i = 0; i < mCountX; i++) {
+                for (int j = 0; j < mCountY; j++) {
+                    if (mOccupied[i][j]) {
+                        cellToPoint(i, j, pt);
+                        canvas.save();
+                        canvas.translate(pt[0], pt[1]);
+                        cd.draw(canvas);
+                        canvas.restore();
+                    }
+                }
+            }
+        }
+
+        int previewOffset = FolderRingAnimator.sPreviewSize;
+
+        // The folder outer / inner ring image(s)
+        for (int i = 0; i < mFolderOuterRings.size(); i++) {
+            FolderRingAnimator fra = mFolderOuterRings.get(i);
+
+            // Draw outer ring
+            Drawable d = FolderRingAnimator.sSharedOuterRingDrawable;
+            int width = (int) fra.getOuterRingSize();
+            int height = width;
+            cellToPoint(fra.mCellX, fra.mCellY, mTempLocation);
+
+            int centerX = mTempLocation[0] + mCellWidth / 2;
+            int centerY = mTempLocation[1] + previewOffset / 2;
+
+            canvas.save();
+            canvas.translate(centerX - width / 2, centerY - height / 2);
+            d.setBounds(0, 0, width, height);
+            d.draw(canvas);
+            canvas.restore();
+
+            // Draw inner ring
+            d = FolderRingAnimator.sSharedInnerRingDrawable;
+            width = (int) fra.getInnerRingSize();
+            height = width;
+            cellToPoint(fra.mCellX, fra.mCellY, mTempLocation);
+
+            centerX = mTempLocation[0] + mCellWidth / 2;
+            centerY = mTempLocation[1] + previewOffset / 2;
+            canvas.save();
+            canvas.translate(centerX - width / 2, centerY - width / 2);
+            d.setBounds(0, 0, width, height);
+            d.draw(canvas);
+            canvas.restore();
+        }
+
+        if (mFolderLeaveBehindCell[0] >= 0 && mFolderLeaveBehindCell[1] >= 0) {
+            Drawable d = FolderIcon.sSharedFolderLeaveBehind;
+            int width = d.getIntrinsicWidth();
+            int height = d.getIntrinsicHeight();
+
+            cellToPoint(mFolderLeaveBehindCell[0], mFolderLeaveBehindCell[1], mTempLocation);
+            int centerX = mTempLocation[0] + mCellWidth / 2;
+            int centerY = mTempLocation[1] + previewOffset / 2;
+
+            canvas.save();
+            canvas.translate(centerX - width / 2, centerY - width / 2);
+            d.setBounds(0, 0, width, height);
+            d.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (mForegroundAlpha > 0) {
+            mOverScrollForegroundDrawable.setBounds(mForegroundRect);
+            Paint p = ((NinePatchDrawable) mOverScrollForegroundDrawable).getPaint();
+            p.setXfermode(sAddBlendMode);
+            mOverScrollForegroundDrawable.draw(canvas);
+            p.setXfermode(null);
+        }
+    }
+
+    public void showFolderAccept(FolderRingAnimator fra) {
+        mFolderOuterRings.add(fra);
+    }
+
+    public void hideFolderAccept(FolderRingAnimator fra) {
+        if (mFolderOuterRings.contains(fra)) {
+            mFolderOuterRings.remove(fra);
+        }
+        invalidate();
+    }
+
+    public void setFolderLeaveBehindCell(int x, int y) {
+        mFolderLeaveBehindCell[0] = x;
+        mFolderLeaveBehindCell[1] = y;
+        invalidate();
+    }
+
+    public void clearFolderLeaveBehind() {
+        mFolderLeaveBehindCell[0] = -1;
+        mFolderLeaveBehindCell[1] = -1;
+        invalidate();
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    public void restoreInstanceState(SparseArray<Parcelable> states) {
+        dispatchRestoreInstanceState(states);
+    }
+
+    @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 setOnInterceptTouchListener(View.OnTouchListener listener) {
+        mInterceptTouchListener = listener;
+    }
+
+    int getCountX() {
+        return mCountX;
+    }
+
+    int getCountY() {
+        return mCountY;
+    }
+
+    public void setIsHotseat(boolean isHotseat) {
+        mIsHotseat = isHotseat;
+    }
+
+    public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params,
+            boolean markCells) {
+        final LayoutParams lp = params;
+
+        // Hotseat icons - remove text
+        if (child instanceof BubbleTextView) {
+            BubbleTextView bubbleChild = (BubbleTextView) child;
+
+            Resources res = getResources();
+            if (mIsHotseat) {
+                bubbleChild.setTextColor(res.getColor(android.R.color.transparent));
+            } else {
+                bubbleChild.setTextColor(res.getColor(R.color.workspace_icon_text_color));
+            }
+        }
+
+        child.setScaleX(getChildrenScale());
+        child.setScaleY(getChildrenScale());
+
+        // Generate an id for each view, this assumes we have at most 256x256 cells
+        // per workspace screen
+        if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 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 = mCountX;
+            if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
+
+            child.setId(childId);
+
+            mShortcutsAndWidgets.addView(child, index, lp);
+
+            if (markCells) markCellsAsOccupiedForView(child);
+
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void removeAllViews() {
+        clearOccupiedCells();
+        mShortcutsAndWidgets.removeAllViews();
+    }
+
+    @Override
+    public void removeAllViewsInLayout() {
+        if (mShortcutsAndWidgets.getChildCount() > 0) {
+            clearOccupiedCells();
+            mShortcutsAndWidgets.removeAllViewsInLayout();
+        }
+    }
+
+    public void removeViewWithoutMarkingCells(View view) {
+        mShortcutsAndWidgets.removeView(view);
+    }
+
+    @Override
+    public void removeView(View view) {
+        markCellsAsUnoccupiedForView(view);
+        mShortcutsAndWidgets.removeView(view);
+    }
+
+    @Override
+    public void removeViewAt(int index) {
+        markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index));
+        mShortcutsAndWidgets.removeViewAt(index);
+    }
+
+    @Override
+    public void removeViewInLayout(View view) {
+        markCellsAsUnoccupiedForView(view);
+        mShortcutsAndWidgets.removeViewInLayout(view);
+    }
+
+    @Override
+    public void removeViews(int start, int count) {
+        for (int i = start; i < start + count; i++) {
+            markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
+        }
+        mShortcutsAndWidgets.removeViews(start, count);
+    }
+
+    @Override
+    public void removeViewsInLayout(int start, int count) {
+        for (int i = start; i < start + count; i++) {
+            markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
+        }
+        mShortcutsAndWidgets.removeViewsInLayout(start, count);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this);
+    }
+
+    public void setTagToCellInfoForPoint(int touchX, int touchY) {
+        final CellInfo cellInfo = mCellInfo;
+        Rect frame = mRect;
+        final int x = touchX + getScrollX();
+        final int y = touchY + getScrollY();
+        final int count = mShortcutsAndWidgets.getChildCount();
+
+        boolean found = false;
+        for (int i = count - 1; i >= 0; i--) {
+            final View child = mShortcutsAndWidgets.getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) &&
+                    lp.isLockedToGrid) {
+                child.getHitRect(frame);
+
+                float scale = child.getScaleX();
+                frame = new Rect(child.getLeft(), child.getTop(), child.getRight(),
+                        child.getBottom());
+                // The child hit rect is relative to the CellLayoutChildren parent, so we need to
+                // offset that by this CellLayout's padding to test an (x,y) point that is relative
+                // to this view.
+                frame.offset(getPaddingLeft(), getPaddingTop());
+                frame.inset((int) (frame.width() * (1f - scale) / 2),
+                        (int) (frame.height() * (1f - scale) / 2));
+
+                if (frame.contains(x, y)) {
+                    cellInfo.cell = child;
+                    cellInfo.cellX = lp.cellX;
+                    cellInfo.cellY = lp.cellY;
+                    cellInfo.spanX = lp.cellHSpan;
+                    cellInfo.spanY = lp.cellVSpan;
+                    found = true;
+                    break;
+                }
+            }
+        }
+
+        mLastDownOnOccupiedCell = found;
+
+        if (!found) {
+            final int cellXY[] = mTmpXY;
+            pointToCellExact(x, y, cellXY);
+
+            cellInfo.cell = null;
+            cellInfo.cellX = cellXY[0];
+            cellInfo.cellY = cellXY[1];
+            cellInfo.spanX = 1;
+            cellInfo.spanY = 1;
+        }
+        setTag(cellInfo);
+    }
+
+    @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.
+        final int action = ev.getAction();
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            clearTagCellInfo();
+        }
+
+        if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
+            return true;
+        }
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
+        }
+
+        return false;
+    }
+
+    private void clearTagCellInfo() {
+        final CellInfo cellInfo = mCellInfo;
+        cellInfo.cell = null;
+        cellInfo.cellX = -1;
+        cellInfo.cellY = -1;
+        cellInfo.spanX = 0;
+        cellInfo.spanY = 0;
+        setTag(cellInfo);
+    }
+
+    public CellInfo getTag() {
+        return (CellInfo) super.getTag();
+    }
+
+    /**
+     * 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) {
+        final int hStartPadding = getPaddingLeft();
+        final int vStartPadding = getPaddingTop();
+
+        result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap);
+        result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap);
+
+        final int xAxis = mCountX;
+        final int yAxis = mCountY;
+
+        if (result[0] < 0) result[0] = 0;
+        if (result[0] >= xAxis) result[0] = xAxis - 1;
+        if (result[1] < 0) result[1] = 0;
+        if (result[1] >= yAxis) result[1] = yAxis - 1;
+    }
+
+    /**
+     * Given a point, return the cell that most closely 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 pointToCellRounded(int x, int y, int[] result) {
+        pointToCellExact(x + (mCellWidth / 2), y + (mCellHeight / 2), result);
+    }
+
+    /**
+     * Given a cell coordinate, return the point that represents the upper left corner of that cell
+     *
+     * @param cellX X coordinate of the cell
+     * @param cellY Y coordinate of the cell
+     *
+     * @param result Array of 2 ints to hold the x and y coordinate of the point
+     */
+    void cellToPoint(int cellX, int cellY, int[] result) {
+        final int hStartPadding = getPaddingLeft();
+        final int vStartPadding = getPaddingTop();
+
+        result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap);
+        result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap);
+    }
+
+    /**
+     * Given a cell coordinate, return the point that represents the center of the cell
+     *
+     * @param cellX X coordinate of the cell
+     * @param cellY Y coordinate of the cell
+     *
+     * @param result Array of 2 ints to hold the x and y coordinate of the point
+     */
+    void cellToCenterPoint(int cellX, int cellY, int[] result) {
+        regionToCenterPoint(cellX, cellY, 1, 1, result);
+    }
+
+    /**
+     * Given a cell coordinate and span return the point that represents the center of the regio
+     *
+     * @param cellX X coordinate of the cell
+     * @param cellY Y coordinate of the cell
+     *
+     * @param result Array of 2 ints to hold the x and y coordinate of the point
+     */
+    void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
+        final int hStartPadding = getPaddingLeft();
+        final int vStartPadding = getPaddingTop();
+        result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) +
+                (spanX * mCellWidth + (spanX - 1) * mWidthGap) / 2;
+        result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) +
+                (spanY * mCellHeight + (spanY - 1) * mHeightGap) / 2;
+    }
+
+     /**
+     * Given a cell coordinate and span fills out a corresponding pixel rect
+     *
+     * @param cellX X coordinate of the cell
+     * @param cellY Y coordinate of the cell
+     * @param result Rect in which to write the result
+     */
+     void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) {
+        final int hStartPadding = getPaddingLeft();
+        final int vStartPadding = getPaddingTop();
+        final int left = hStartPadding + cellX * (mCellWidth + mWidthGap);
+        final int top = vStartPadding + cellY * (mCellHeight + mHeightGap);
+        result.set(left, top, left + (spanX * mCellWidth + (spanX - 1) * mWidthGap),
+                top + (spanY * mCellHeight + (spanY - 1) * mHeightGap));
+    }
+
+    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;
+    }
+
+    int getCellWidth() {
+        return mCellWidth;
+    }
+
+    int getCellHeight() {
+        return mCellHeight;
+    }
+
+    int getWidthGap() {
+        return mWidthGap;
+    }
+
+    int getHeightGap() {
+        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;
+    }
+
+    static void getMetrics(Rect metrics, Resources res, int measureWidth, int measureHeight,
+            int countX, int countY, int orientation) {
+        int numWidthGaps = countX - 1;
+        int numHeightGaps = countY - 1;
+
+        int widthGap;
+        int heightGap;
+        int cellWidth;
+        int cellHeight;
+        int paddingLeft;
+        int paddingRight;
+        int paddingTop;
+        int paddingBottom;
+
+        int maxGap = res.getDimensionPixelSize(R.dimen.workspace_max_gap);
+        if (orientation == LANDSCAPE) {
+            cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_land);
+            cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_land);
+            widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_land);
+            heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_land);
+            paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_land);
+            paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_land);
+            paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_land);
+            paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_land);
+        } else {
+            // PORTRAIT
+            cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_port);
+            cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_port);
+            widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_port);
+            heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_port);
+            paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_port);
+            paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_port);
+            paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_port);
+            paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_port);
+        }
+
+        if (widthGap < 0 || heightGap < 0) {
+            int hSpace = measureWidth - paddingLeft - paddingRight;
+            int vSpace = measureHeight - paddingTop - paddingBottom;
+            int hFreeSpace = hSpace - (countX * cellWidth);
+            int vFreeSpace = vSpace - (countY * cellHeight);
+            widthGap = Math.min(maxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0);
+            heightGap = Math.min(maxGap, numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0);
+        }
+        metrics.set(cellWidth, cellHeight, widthGap, heightGap);
+    }
+
+    @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");
+        }
+
+        int numWidthGaps = mCountX - 1;
+        int numHeightGaps = mCountY - 1;
+
+        if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) {
+            int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight();
+            int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom();
+            int hFreeSpace = hSpace - (mCountX * mCellWidth);
+            int vFreeSpace = vSpace - (mCountY * mCellHeight);
+            mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0);
+            mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0);
+            mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+                    mCountX);
+        } 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() + (mCountX * mCellWidth) +
+                ((mCountX - 1) * mWidthGap);
+            newHeight = getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) +
+                ((mCountY - 1) * mHeightGap);
+            setMeasuredDimension(newWidth, newHeight);
+        }
+
+        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);
+    }
+
+    @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
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mBackgroundRect.set(0, 0, w, h);
+        mForegroundRect.set(mForegroundPadding, mForegroundPadding,
+                w - mForegroundPadding, h - mForegroundPadding);
+    }
+
+    @Override
+    protected void setChildrenDrawingCacheEnabled(boolean enabled) {
+        mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled);
+    }
+
+    @Override
+    protected void setChildrenDrawnWithCacheEnabled(boolean enabled) {
+        mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled);
+    }
+
+    public float getBackgroundAlpha() {
+        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();
+        }
+    }
+
+    public void setShortcutAndWidgetAlpha(float alpha) {
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            getChildAt(i).setAlpha(alpha);
+        }
+    }
+
+    public ShortcutAndWidgetContainer getShortcutsAndWidgets() {
+        if (getChildCount() > 0) {
+            return (ShortcutAndWidgetContainer) getChildAt(0);
+        }
+        return null;
+    }
+
+    public View getChildAt(int x, int y) {
+        return mShortcutsAndWidgets.getChildAt(x, y);
+    }
+
+    public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
+            int delay, boolean permanent, boolean adjustOccupied) {
+        ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
+        boolean[][] occupied = mOccupied;
+        if (!permanent) {
+            occupied = mTmpOccupied;
+        }
+
+        if (clc.indexOfChild(child) != -1) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            final ItemInfo info = (ItemInfo) child.getTag();
+
+            // We cancel any existing animations
+            if (mReorderAnimators.containsKey(lp)) {
+                mReorderAnimators.get(lp).cancel();
+                mReorderAnimators.remove(lp);
+            }
+
+            final int oldX = lp.x;
+            final int oldY = lp.y;
+            if (adjustOccupied) {
+                occupied[lp.cellX][lp.cellY] = false;
+                occupied[cellX][cellY] = true;
+            }
+            lp.isLockedToGrid = true;
+            if (permanent) {
+                lp.cellX = info.cellX = cellX;
+                lp.cellY = info.cellY = cellY;
+            } else {
+                lp.tmpCellX = cellX;
+                lp.tmpCellY = cellY;
+            }
+            clc.setupLp(lp);
+            lp.isLockedToGrid = false;
+            final int newX = lp.x;
+            final int newY = lp.y;
+
+            lp.x = oldX;
+            lp.y = oldY;
+
+            // Exit early if we're not actually moving the view
+            if (oldX == newX && oldY == newY) {
+                lp.isLockedToGrid = true;
+                return true;
+            }
+
+            ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
+            va.setDuration(duration);
+            mReorderAnimators.put(lp, va);
+
+            va.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    float r = ((Float) animation.getAnimatedValue()).floatValue();
+                    lp.x = (int) ((1 - r) * oldX + r * newX);
+                    lp.y = (int) ((1 - r) * oldY + r * newY);
+                    child.requestLayout();
+                }
+            });
+            va.addListener(new AnimatorListenerAdapter() {
+                boolean cancelled = false;
+                public void onAnimationEnd(Animator animation) {
+                    // If the animation was cancelled, it means that another animation
+                    // has interrupted this one, and we don't want to lock the item into
+                    // place just yet.
+                    if (!cancelled) {
+                        lp.isLockedToGrid = true;
+                        child.requestLayout();
+                    }
+                    if (mReorderAnimators.containsKey(lp)) {
+                        mReorderAnimators.remove(lp);
+                    }
+                }
+                public void onAnimationCancel(Animator animation) {
+                    cancelled = true;
+                }
+            });
+            va.setStartDelay(delay);
+            va.start();
+            return true;
+        }
+        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];
+        final int oldDragCellY = mDragCell[1];
+
+        if (v != null && dragOffset == null) {
+            mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2));
+        } else {
+            mDragCenter.set(originX, originY);
+        }
+
+        if (dragOutline == null && v == null) {
+            return;
+        }
+
+        if (cellX != oldDragCellX || cellY != oldDragCellY) {
+            mDragCell[0] = cellX;
+            mDragCell[1] = cellY;
+            // Find the top left corner of the rect the object will occupy
+            final int[] topLeft = mTmpPoint;
+            cellToPoint(cellX, cellY, topLeft);
+
+            int left = topLeft[0];
+            int top = topLeft[1];
+
+            if (v != null && dragOffset == null) {
+                // When drawing the drag outline, it did not account for margin offsets
+                // added by the view's parent.
+                MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
+                left += lp.leftMargin;
+                top += lp.topMargin;
+
+                // Offsets due to the size difference between the View and the dragOutline.
+                // There is a size difference to account for the outer blur, which may lie
+                // outside the bounds of the view.
+                top += (v.getHeight() - dragOutline.getHeight()) / 2;
+                // We center about the x axis
+                left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+                        - dragOutline.getWidth()) / 2;
+            } else {
+                if (dragOffset != null && dragRegion != null) {
+                    // Center the drag region *horizontally* in the cell and apply a drag
+                    // outline offset
+                    left += dragOffset.x + ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+                             - dragRegion.width()) / 2;
+                    top += dragOffset.y;
+                } else {
+                    // Center the drag outline in the cell
+                    left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+                            - dragOutline.getWidth()) / 2;
+                    top += ((mCellHeight * spanY) + ((spanY - 1) * mHeightGap)
+                            - dragOutline.getHeight()) / 2;
+                }
+            }
+            final int oldIndex = mDragOutlineCurrent;
+            mDragOutlineAnims[oldIndex].animateOut();
+            mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
+            Rect r = mDragOutlines[mDragOutlineCurrent];
+            r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight());
+            if (resize) {
+                cellToRect(cellX, cellY, spanX, spanY, r);
+            }
+
+            mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
+            mDragOutlineAnims[mDragOutlineCurrent].animateIn();
+        }
+    }
+
+    public void clearDragOutlines() {
+        final int oldIndex = mDragOutlineCurrent;
+        mDragOutlineAnims[oldIndex].animateOut();
+        mDragCell[0] = mDragCell[1] = -1;
+    }
+
+    /**
+     * 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 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[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY,
+            int[] result) {
+        return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, 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 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[] 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,
+                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()) {
+            for (int i = 0; i < mCountX * mCountY; i++) {
+                mTempRectStack.push(new Rect());
+            }
+        }
+    }
+
+    private void recycleTempRects(Stack<Rect> used) {
+        while (!used.isEmpty()) {
+            mTempRectStack.push(used.pop());
+        }
+    }
+
+    /**
+     * 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 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 minSpanX, int minSpanY, int spanX, int spanY,
+            View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan,
+            boolean[][] occupied) {
+        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
+        // we translate the point over to correspond to the top-left.
+        pixelX -= (mCellWidth + mWidthGap) * (spanX - 1) / 2f;
+        pixelY -= (mCellHeight + mHeightGap) * (spanY - 1) / 2f;
+
+        // Keep track of best-scoring drop area
+        final int[] bestXY = result != null ? result : new int[2];
+        double bestDistance = Double.MAX_VALUE;
+        final Rect bestRect = new Rect(-1, -1, -1, -1);
+        final Stack<Rect> validRegions = new Stack<Rect>();
+
+        final int countX = mCountX;
+        final int countY = mCountY;
+
+        if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 ||
+                spanX < minSpanX || spanY < minSpanY) {
+            return bestXY;
+        }
+
+        for (int y = 0; y < countY - (minSpanY - 1); y++) {
+            inner:
+            for (int x = 0; x < countX - (minSpanX - 1); x++) {
+                int ySize = -1;
+                int xSize = -1;
+                if (ignoreOccupied) {
+                    // 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]) {
+                                continue inner;
+                            }
+                        }
+                    }
+                    xSize = minSpanX;
+                    ySize = minSpanY;
+
+                    // We know that the item will fit at _some_ acceptable size, now let's see
+                    // how big we can make it. We'll alternate between incrementing x and y spans
+                    // until we hit a limit.
+                    boolean incX = true;
+                    boolean hitMaxX = xSize >= spanX;
+                    boolean hitMaxY = ySize >= spanY;
+                    while (!(hitMaxX && hitMaxY)) {
+                        if (incX && !hitMaxX) {
+                            for (int j = 0; j < ySize; j++) {
+                                if (x + xSize > countX -1 || occupied[x + xSize][y + j]) {
+                                    // We can't move out horizontally
+                                    hitMaxX = true;
+                                }
+                            }
+                            if (!hitMaxX) {
+                                xSize++;
+                            }
+                        } else if (!hitMaxY) {
+                            for (int i = 0; i < xSize; i++) {
+                                if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) {
+                                    // We can't move out vertically
+                                    hitMaxY = true;
+                                }
+                            }
+                            if (!hitMaxY) {
+                                ySize++;
+                            }
+                        }
+                        hitMaxX |= xSize >= spanX;
+                        hitMaxY |= ySize >= spanY;
+                        incX = !incX;
+                    }
+                    incX = true;
+                    hitMaxX = xSize >= spanX;
+                    hitMaxY = ySize >= spanY;
+                }
+                final int[] cellXY = mTmpXY;
+                cellToCenterPoint(x, y, cellXY);
+
+                // We verify that the current rect is not a sub-rect of any of our previous
+                // candidates. In this case, the current rect is disqualified in favour of the
+                // containing rect.
+                Rect currentRect = mTempRectStack.pop();
+                currentRect.set(x, y, x + xSize, y + ySize);
+                boolean contained = false;
+                for (Rect r : validRegions) {
+                    if (r.contains(currentRect)) {
+                        contained = true;
+                        break;
+                    }
+                }
+                validRegions.push(currentRect);
+                double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2)
+                        + Math.pow(cellXY[1] - pixelY, 2));
+
+                if ((distance <= bestDistance && !contained) ||
+                        currentRect.contains(bestRect)) {
+                    bestDistance = distance;
+                    bestXY[0] = x;
+                    bestXY[1] = y;
+                    if (resultSpan != null) {
+                        resultSpan[0] = xSize;
+                        resultSpan[1] = ySize;
+                    }
+                    bestRect.set(currentRect);
+                }
+            }
+        }
+        // re-mark space taken by ignoreView as occupied
+        markCellsAsOccupiedForView(ignoreView, occupied);
+
+        // Return -1, -1 if no suitable location found
+        if (bestDistance == Double.MAX_VALUE) {
+            bestXY[0] = -1;
+            bestXY[1] = -1;
+        }
+        recycleTempRects(validRegions);
+        return bestXY;
+    }
+
+     /**
+     * Find a vacant area that will fit the given bounds nearest the requested
+     * cell location, and will also weigh in a suggested direction vector of the
+     * desired location. This method computers distance based on unit grid distances,
+     * not pixel distances.
+     *
+     * @param cellX The X cell nearest to which you want to search for a vacant area.
+     * @param cellY The Y cell nearest which you want to search for a vacant area.
+     * @param spanX Horizontal span of the object.
+     * @param spanY Vertical span of the object.
+     * @param direction The favored direction in which the views should move from x, y
+     * @param exactDirectionOnly If this parameter is true, then only solutions where the direction
+     *        matches exactly. Otherwise we find the best matching direction.
+     * @param occoupied The array which represents which cells in the CellLayout are occupied
+     * @param blockOccupied The array which represents which cells in the specified block (cellX,
+     *        cellY, spanX, spanY) are occupied. This is used when try to move a group of views. 
+     * @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.
+     */
+    private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
+            boolean[][] occupied, boolean blockOccupied[][], int[] result) {
+        // Keep track of best-scoring drop area
+        final int[] bestXY = result != null ? result : new int[2];
+        float bestDistance = Float.MAX_VALUE;
+        int bestDirectionScore = Integer.MIN_VALUE;
+
+        final int countX = mCountX;
+        final int countY = mCountY;
+
+        for (int y = 0; y < countY - (spanY - 1); y++) {
+            inner:
+            for (int x = 0; x < countX - (spanX - 1); x++) {
+                // First, let's see if this thing fits anywhere
+                for (int i = 0; i < spanX; i++) {
+                    for (int j = 0; j < spanY; j++) {
+                        if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
+                            continue inner;
+                        }
+                    }
+                }
+
+                float distance = (float)
+                        Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY));
+                int[] curDirection = mTmpPoint;
+                computeDirectionVector(x - cellX, y - cellY, curDirection);
+                // The direction score is just the dot product of the two candidate direction
+                // and that passed in.
+                int curDirectionScore = direction[0] * curDirection[0] +
+                        direction[1] * curDirection[1];
+                boolean exactDirectionOnly = false;
+                boolean directionMatches = direction[0] == curDirection[0] &&
+                        direction[0] == curDirection[0];
+                if ((directionMatches || !exactDirectionOnly) &&
+                        Float.compare(distance,  bestDistance) < 0 || (Float.compare(distance,
+                        bestDistance) == 0 && curDirectionScore > bestDirectionScore)) {
+                    bestDistance = distance;
+                    bestDirectionScore = curDirectionScore;
+                    bestXY[0] = x;
+                    bestXY[1] = y;
+                }
+            }
+        }
+
+        // Return -1, -1 if no suitable location found
+        if (bestDistance == Float.MAX_VALUE) {
+            bestXY[0] = -1;
+            bestXY[1] = -1;
+        }
+        return bestXY;
+    }
+
+    private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
+            int[] direction, ItemConfiguration currentState) {
+        CellAndSpan c = currentState.map.get(v);
+        boolean success = false;
+        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+        markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+
+        findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation);
+
+        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+            c.x = mTempLocation[0];
+            c.y = mTempLocation[1];
+            success = true;
+        }
+        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+        return success;
+    }
+
+    /**
+     * This helper class defines a cluster of views. It helps with defining complex edges
+     * of the cluster and determining how those edges interact with other views. The edges
+     * essentially define a fine-grained boundary around the cluster of views -- like a more
+     * precise version of a bounding box.
+     */
+    private class ViewCluster {
+        final static int LEFT = 0;
+        final static int TOP = 1;
+        final static int RIGHT = 2;
+        final static int BOTTOM = 3;
+
+        ArrayList<View> views;
+        ItemConfiguration config;
+        Rect boundingRect = new Rect();
+
+        int[] leftEdge = new int[mCountY];
+        int[] rightEdge = new int[mCountY];
+        int[] topEdge = new int[mCountX];
+        int[] bottomEdge = new int[mCountX];
+        boolean leftEdgeDirty, rightEdgeDirty, topEdgeDirty, bottomEdgeDirty, boundingRectDirty;
+
+        @SuppressWarnings("unchecked")
+        public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
+            this.views = (ArrayList<View>) views.clone();
+            this.config = config;
+            resetEdges();
+        }
+
+        void resetEdges() {
+            for (int i = 0; i < mCountX; i++) {
+                topEdge[i] = -1;
+                bottomEdge[i] = -1;
+            }
+            for (int i = 0; i < mCountY; i++) {
+                leftEdge[i] = -1;
+                rightEdge[i] = -1;
+            }
+            leftEdgeDirty = true;
+            rightEdgeDirty = true;
+            bottomEdgeDirty = true;
+            topEdgeDirty = true;
+            boundingRectDirty = true;
+        }
+
+        void computeEdge(int which, int[] edge) {
+            int count = views.size();
+            for (int i = 0; i < count; i++) {
+                CellAndSpan cs = config.map.get(views.get(i));
+                switch (which) {
+                    case LEFT:
+                        int left = cs.x;
+                        for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+                            if (left < edge[j] || edge[j] < 0) {
+                                edge[j] = left;
+                            }
+                        }
+                        break;
+                    case RIGHT:
+                        int right = cs.x + cs.spanX;
+                        for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+                            if (right > edge[j]) {
+                                edge[j] = right;
+                            }
+                        }
+                        break;
+                    case TOP:
+                        int top = cs.y;
+                        for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+                            if (top < edge[j] || edge[j] < 0) {
+                                edge[j] = top;
+                            }
+                        }
+                        break;
+                    case BOTTOM:
+                        int bottom = cs.y + cs.spanY;
+                        for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+                            if (bottom > edge[j]) {
+                                edge[j] = bottom;
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+
+        boolean isViewTouchingEdge(View v, int whichEdge) {
+            CellAndSpan cs = config.map.get(v);
+
+            int[] edge = getEdge(whichEdge);
+
+            switch (whichEdge) {
+                case LEFT:
+                    for (int i = cs.y; i < cs.y + cs.spanY; i++) {
+                        if (edge[i] == cs.x + cs.spanX) {
+                            return true;
+                        }
+                    }
+                    break;
+                case RIGHT:
+                    for (int i = cs.y; i < cs.y + cs.spanY; i++) {
+                        if (edge[i] == cs.x) {
+                            return true;
+                        }
+                    }
+                    break;
+                case TOP:
+                    for (int i = cs.x; i < cs.x + cs.spanX; i++) {
+                        if (edge[i] == cs.y + cs.spanY) {
+                            return true;
+                        }
+                    }
+                    break;
+                case BOTTOM:
+                    for (int i = cs.x; i < cs.x + cs.spanX; i++) {
+                        if (edge[i] == cs.y) {
+                            return true;
+                        }
+                    }
+                    break;
+            }
+            return false;
+        }
+
+        void shift(int whichEdge, int delta) {
+            for (View v: views) {
+                CellAndSpan c = config.map.get(v);
+                switch (whichEdge) {
+                    case LEFT:
+                        c.x -= delta;
+                        break;
+                    case RIGHT:
+                        c.x += delta;
+                        break;
+                    case TOP:
+                        c.y -= delta;
+                        break;
+                    case BOTTOM:
+                    default:
+                        c.y += delta;
+                        break;
+                }
+            }
+            resetEdges();
+        }
+
+        public void addView(View v) {
+            views.add(v);
+            resetEdges();
+        }
+
+        public Rect getBoundingRect() {
+            if (boundingRectDirty) {
+                boolean first = true;
+                for (View v: views) {
+                    CellAndSpan c = config.map.get(v);
+                    if (first) {
+                        boundingRect.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+                        first = false;
+                    } else {
+                        boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+                    }
+                }
+            }
+            return boundingRect;
+        }
+
+        public int[] getEdge(int which) {
+            switch (which) {
+                case LEFT:
+                    return getLeftEdge();
+                case RIGHT:
+                    return getRightEdge();
+                case TOP:
+                    return getTopEdge();
+                case BOTTOM:
+                default:
+                    return getBottomEdge();
+            }
+        }
+
+        public int[] getLeftEdge() {
+            if (leftEdgeDirty) {
+                computeEdge(LEFT, leftEdge);
+            }
+            return leftEdge;
+        }
+
+        public int[] getRightEdge() {
+            if (rightEdgeDirty) {
+                computeEdge(RIGHT, rightEdge);
+            }
+            return rightEdge;
+        }
+
+        public int[] getTopEdge() {
+            if (topEdgeDirty) {
+                computeEdge(TOP, topEdge);
+            }
+            return topEdge;
+        }
+
+        public int[] getBottomEdge() {
+            if (bottomEdgeDirty) {
+                computeEdge(BOTTOM, bottomEdge);
+            }
+            return bottomEdge;
+        }
+
+        PositionComparator comparator = new PositionComparator();
+        class PositionComparator implements Comparator<View> {
+            int whichEdge = 0;
+            public int compare(View left, View right) {
+                CellAndSpan l = config.map.get(left);
+                CellAndSpan r = config.map.get(right);
+                switch (whichEdge) {
+                    case LEFT:
+                        return (r.x + r.spanX) - (l.x + l.spanX);
+                    case RIGHT:
+                        return l.x - r.x;
+                    case TOP:
+                        return (r.y + r.spanY) - (l.y + l.spanY);
+                    case BOTTOM:
+                    default:
+                        return l.y - r.y;
+                }
+            }
+        }
+
+        public void sortConfigurationForEdgePush(int edge) {
+            comparator.whichEdge = edge;
+            Collections.sort(config.sortedViews, comparator);
+        }
+    }
+
+    private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+            int[] direction, View dragView, ItemConfiguration currentState) {
+
+        ViewCluster cluster = new ViewCluster(views, currentState);
+        Rect clusterRect = cluster.getBoundingRect();
+        int whichEdge;
+        int pushDistance;
+        boolean fail = false;
+
+        // Determine the edge of the cluster that will be leading the push and how far
+        // the cluster must be shifted.
+        if (direction[0] < 0) {
+            whichEdge = ViewCluster.LEFT;
+            pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
+        } else if (direction[0] > 0) {
+            whichEdge = ViewCluster.RIGHT;
+            pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
+        } else if (direction[1] < 0) {
+            whichEdge = ViewCluster.TOP;
+            pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
+        } else {
+            whichEdge = ViewCluster.BOTTOM;
+            pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
+        }
+
+        // Break early for invalid push distance.
+        if (pushDistance <= 0) {
+            return false;
+        }
+
+        // Mark the occupied state as false for the group of views we want to move.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+        }
+
+        // We save the current configuration -- if we fail to find a solution we will revert
+        // to the initial state. The process of finding a solution modifies the configuration
+        // in place, hence the need for revert in the failure case.
+        currentState.save();
+
+        // The pushing algorithm is simplified by considering the views in the order in which
+        // they would be pushed by the cluster. For example, if the cluster is leading with its
+        // left edge, we consider sort the views by their right edge, from right to left.
+        cluster.sortConfigurationForEdgePush(whichEdge);
+
+        while (pushDistance > 0 && !fail) {
+            for (View v: currentState.sortedViews) {
+                // For each view that isn't in the cluster, we see if the leading edge of the
+                // cluster is contacting the edge of that view. If so, we add that view to the
+                // cluster.
+                if (!cluster.views.contains(v) && v != dragView) {
+                    if (cluster.isViewTouchingEdge(v, whichEdge)) {
+                        LayoutParams lp = (LayoutParams) v.getLayoutParams();
+                        if (!lp.canReorder) {
+                            // The push solution includes the all apps button, this is not viable.
+                            fail = true;
+                            break;
+                        }
+                        cluster.addView(v);
+                        CellAndSpan c = currentState.map.get(v);
+
+                        // Adding view to cluster, mark it as not occupied.
+                        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+                    }
+                }
+            }
+            pushDistance--;
+
+            // The cluster has been completed, now we move the whole thing over in the appropriate
+            // direction.
+            cluster.shift(whichEdge, 1);
+        }
+
+        boolean foundSolution = false;
+        clusterRect = cluster.getBoundingRect();
+
+        // Due to the nature of the algorithm, the only check required to verify a valid solution
+        // is to ensure that completed shifted cluster lies completely within the cell layout.
+        if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
+                clusterRect.bottom <= mCountY) {
+            foundSolution = true;
+        } else {
+            currentState.restore();
+        }
+
+        // In either case, we set the occupied array as marked for the location of the views
+        for (View v: cluster.views) {
+            CellAndSpan c = currentState.map.get(v);
+            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+        }
+
+        return foundSolution;
+    }
+
+    private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+            int[] direction, View dragView, ItemConfiguration currentState) {
+        if (views.size() == 0) return true;
+
+        boolean success = false;
+        Rect boundingRect = null;
+        // We construct a rect which represents the entire group of views passed in
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            if (boundingRect == null) {
+                boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+            } else {
+                boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+            }
+        }
+
+        // Mark the occupied state as false for the group of views we want to move.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+        }
+
+        boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()];
+        int top = boundingRect.top;
+        int left = boundingRect.left;
+        // We mark more precisely which parts of the bounding rect are truly occupied, allowing
+        // for interlocking.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true);
+        }
+
+        markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+
+        findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
+                boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation);
+
+        // If we successfuly found a location by pushing the block of views, we commit it
+        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+            int deltaX = mTempLocation[0] - boundingRect.left;
+            int deltaY = mTempLocation[1] - boundingRect.top;
+            for (View v: views) {
+                CellAndSpan c = currentState.map.get(v);
+                c.x += deltaX;
+                c.y += deltaY;
+            }
+            success = true;
+        }
+
+        // In either case, we set the occupied array as marked for the location of the views
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+        }
+        return success;
+    }
+
+    private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) {
+        markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value);
+    }
+
+    // This method tries to find a reordering solution which satisfies the push mechanic by trying
+    // to push items in each of the cardinal directions, in an order based on the direction vector
+    // passed.
+    private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
+            int[] direction, View ignoreView, ItemConfiguration solution) {
+        if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
+            // If the direction vector has two non-zero components, we try pushing 
+            // separately in each of the components.
+            int temp = direction[1];
+            direction[1] = 0;
+
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            direction[1] = temp;
+            temp = direction[0];
+            direction[0] = 0;
+
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Revert the direction
+            direction[0] = temp;
+
+            // Now we try pushing in each component of the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            temp = direction[1];
+            direction[1] = 0;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+
+            direction[1] = temp;
+            temp = direction[0];
+            direction[0] = 0;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // revert the direction
+            direction[0] = temp;
+            direction[0] *= -1;
+            direction[1] *= -1;
+            
+        } else {
+            // If the direction vector has a single non-zero component, we push first in the
+            // direction of the vector
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Then we try the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Switch the direction back
+            direction[0] *= -1;
+            direction[1] *= -1;
+            
+            // If we have failed to find a push solution with the above, then we try 
+            // to find a solution by pushing along the perpendicular axis.
+
+            // Swap the components
+            int temp = direction[1];
+            direction[1] = direction[0];
+            direction[0] = temp;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+
+            // Then we try the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Switch the direction back
+            direction[0] *= -1;
+            direction[1] *= -1;
+
+            // Swap the components back
+            temp = direction[1];
+            direction[1] = direction[0];
+            direction[0] = temp;
+        }
+        return false;
+    }
+
+    private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
+            View ignoreView, ItemConfiguration solution) {
+        // Return early if get invalid cell positions
+        if (cellX < 0 || cellY < 0) return false;
+
+        mIntersectingViews.clear();
+        mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+
+        // Mark the desired location of the view currently being dragged.
+        if (ignoreView != null) {
+            CellAndSpan c = solution.map.get(ignoreView);
+            if (c != null) {
+                c.x = cellX;
+                c.y = cellY;
+            }
+        }
+        Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+        Rect r1 = new Rect();
+        for (View child: solution.map.keySet()) {
+            if (child == ignoreView) continue;
+            CellAndSpan c = solution.map.get(child);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+            if (Rect.intersects(r0, r1)) {
+                if (!lp.canReorder) {
+                    return false;
+                }
+                mIntersectingViews.add(child);
+            }
+        }
+
+        // First we try to find a solution which respects the push mechanic. That is, 
+        // we try to find a solution such that no displaced item travels through another item
+        // without also displacing that item.
+        if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+                solution)) {
+            return true;
+        }
+
+        // Next we try moving the views as a block, but without requiring the push mechanic.
+        if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+                solution)) {
+            return true;
+        }
+
+        // Ok, they couldn't move as a block, let's move them individually
+        for (View v : mIntersectingViews) {
+            if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /*
+     * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
+     * the provided point and the provided cell
+     */
+    private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
+        double angle = Math.atan(((float) deltaY) / deltaX);
+
+        result[0] = 0;
+        result[1] = 0;
+        if (Math.abs(Math.cos(angle)) > 0.5f) {
+            result[0] = (int) Math.signum(deltaX);
+        }
+        if (Math.abs(Math.sin(angle)) > 0.5f) {
+            result[1] = (int) Math.signum(deltaY);
+        }
+    }
+
+    private void copyOccupiedArray(boolean[][] occupied) {
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                occupied[i][j] = mOccupied[i][j];
+            }
+        }
+    }
+
+    ItemConfiguration simpleSwap(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.
+        copyCurrentStateToSolution(solution, false);
+        // Copy the current occupied array into the temporary occupied array. This array will be
+        // manipulated as necessary to find a solution.
+        copyOccupiedArray(mTmpOccupied);
+
+        // We find the nearest cell into which we would place the dragged item, assuming there's
+        // nothing in its way.
+        int result[] = new int[2];
+        result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+
+        boolean success = false;
+        // First we try the exact nearest position of the item being dragged,
+        // we will then want to try to move this around to other neighbouring positions
+        success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
+                solution);
+
+        if (!success) {
+            // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
+            // x, then 1 in y etc.
+            if (spanX > minSpanX && (minSpanY == spanY || decX)) {
+                return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, direction,
+                        dragView, false, solution);
+            } else if (spanY > minSpanY) {
+                return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, direction,
+                        dragView, true, solution);
+            }
+            solution.isSolution = false;
+        } else {
+            solution.isSolution = true;
+            solution.dragViewX = result[0];
+            solution.dragViewY = result[1];
+            solution.dragViewSpanX = spanX;
+            solution.dragViewSpanY = spanY;
+        }
+        return solution;
+    }
+
+    private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            CellAndSpan c;
+            if (temp) {
+                c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
+            } else {
+                c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
+            }
+            solution.add(child, c);
+        }
+    }
+
+    private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                mTmpOccupied[i][j] = false;
+            }
+        }
+
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            if (child == dragView) continue;
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            CellAndSpan c = solution.map.get(child);
+            if (c != null) {
+                lp.tmpCellX = c.x;
+                lp.tmpCellY = c.y;
+                lp.cellHSpan = c.spanX;
+                lp.cellVSpan = c.spanY;
+                markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+            }
+        }
+        markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
+                solution.dragViewSpanY, mTmpOccupied, true);
+    }
+
+    private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean
+            commitDragView) {
+
+        boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                occupied[i][j] = false;
+            }
+        }
+
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            if (child == dragView) continue;
+            CellAndSpan c = solution.map.get(child);
+            if (c != null) {
+                animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0,
+                        DESTRUCTIVE_REORDER, false);
+                markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true);
+            }
+        }
+        if (commitDragView) {
+            markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
+                    solution.dragViewSpanY, occupied, true);
+        }
+    }
+
+    // This method starts or changes the reorder hint animations
+    private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) {
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            if (child == dragView) continue;
+            CellAndSpan c = solution.map.get(child);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (c != null) {
+                ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY,
+                        c.x, c.y, c.spanX, c.spanY);
+                rha.animate();
+            }
+        }
+    }
+
+    // Class which represents the reorder hint animations. These animations show that an item is
+    // in a temporary state, and hint at where the item will return to.
+    class ReorderHintAnimation {
+        View child;
+        float finalDeltaX;
+        float finalDeltaY;
+        float initDeltaX;
+        float initDeltaY;
+        float finalScale;
+        float initScale;
+        private static final int DURATION = 300;
+        Animator a;
+
+        public ReorderHintAnimation(View child, int cellX0, int cellY0, int cellX1, int cellY1,
+                int spanX, int spanY) {
+            regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
+            final int x0 = mTmpPoint[0];
+            final int y0 = mTmpPoint[1];
+            regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint);
+            final int x1 = mTmpPoint[0];
+            final int y1 = mTmpPoint[1];
+            final int dX = x1 - x0;
+            final int dY = y1 - y0;
+            finalDeltaX = 0;
+            finalDeltaY = 0;
+            if (dX == dY && dX == 0) {
+            } else {
+                if (dY == 0) {
+                    finalDeltaX = - Math.signum(dX) * mReorderHintAnimationMagnitude;
+                } else if (dX == 0) {
+                    finalDeltaY = - Math.signum(dY) * mReorderHintAnimationMagnitude;
+                } else {
+                    double angle = Math.atan( (float) (dY) / dX);
+                    finalDeltaX = (int) (- Math.signum(dX) *
+                            Math.abs(Math.cos(angle) * mReorderHintAnimationMagnitude));
+                    finalDeltaY = (int) (- Math.signum(dY) *
+                            Math.abs(Math.sin(angle) * mReorderHintAnimationMagnitude));
+                }
+            }
+            initDeltaX = child.getTranslationX();
+            initDeltaY = child.getTranslationY();
+            finalScale = getChildrenScale() - 4.0f / child.getWidth();
+            initScale = child.getScaleX();
+            this.child = child;
+        }
+
+        void animate() {
+            if (mShakeAnimators.containsKey(child)) {
+                ReorderHintAnimation oldAnimation = mShakeAnimators.get(child);
+                oldAnimation.cancel();
+                mShakeAnimators.remove(child);
+                if (finalDeltaX == 0 && finalDeltaY == 0) {
+                    completeAnimationImmediately();
+                    return;
+                }
+            }
+            if (finalDeltaX == 0 && finalDeltaY == 0) {
+                return;
+            }
+            ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
+            a = va;
+            va.setRepeatMode(ValueAnimator.REVERSE);
+            va.setRepeatCount(ValueAnimator.INFINITE);
+            va.setDuration(DURATION);
+            va.setStartDelay((int) (Math.random() * 60));
+            va.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    float r = ((Float) animation.getAnimatedValue()).floatValue();
+                    float x = r * finalDeltaX + (1 - r) * initDeltaX;
+                    float y = r * finalDeltaY + (1 - r) * initDeltaY;
+                    child.setTranslationX(x);
+                    child.setTranslationY(y);
+                    float s = r * finalScale + (1 - r) * initScale;
+                    child.setScaleX(s);
+                    child.setScaleY(s);
+                }
+            });
+            va.addListener(new AnimatorListenerAdapter() {
+                public void onAnimationRepeat(Animator animation) {
+                    // We make sure to end only after a full period
+                    initDeltaX = 0;
+                    initDeltaY = 0;
+                    initScale = getChildrenScale();
+                }
+            });
+            mShakeAnimators.put(child, this);
+            va.start();
+        }
+
+        private void cancel() {
+            if (a != null) {
+                a.cancel();
+            }
+        }
+
+        private void completeAnimationImmediately() {
+            if (a != null) {
+                a.cancel();
+            }
+
+            AnimatorSet s = LauncherAnimUtils.createAnimatorSet();
+            a = s;
+            s.playTogether(
+                LauncherAnimUtils.ofFloat(child, "scaleX", getChildrenScale()),
+                LauncherAnimUtils.ofFloat(child, "scaleY", getChildrenScale()),
+                LauncherAnimUtils.ofFloat(child, "translationX", 0f),
+                LauncherAnimUtils.ofFloat(child, "translationY", 0f)
+            );
+            s.setDuration(REORDER_ANIMATION_DURATION);
+            s.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5f));
+            s.start();
+        }
+    }
+
+    private void completeAndClearReorderHintAnimations() {
+        for (ReorderHintAnimation a: mShakeAnimators.values()) {
+            a.completeAnimationImmediately();
+        }
+        mShakeAnimators.clear();
+    }
+
+    private void commitTempPlacement() {
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                mOccupied[i][j] = mTmpOccupied[i][j];
+            }
+        }
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            ItemInfo info = (ItemInfo) child.getTag();
+            // We do a null check here because the item info can be null in the case of the
+            // AllApps button in the hotseat.
+            if (info != null) {
+                if (info.cellX != lp.tmpCellX || info.cellY != lp.tmpCellY ||
+                        info.spanX != lp.cellHSpan || info.spanY != lp.cellVSpan) {
+                    info.requiresDbUpdate = true;
+                }
+                info.cellX = lp.cellX = lp.tmpCellX;
+                info.cellY = lp.cellY = lp.tmpCellY;
+                info.spanX = lp.cellHSpan;
+                info.spanY = lp.cellVSpan;
+            }
+        }
+        mLauncher.getWorkspace().updateItemLocationsInDatabase(this);
+    }
+
+    public void setUseTempCoords(boolean useTempCoords) {
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams();
+            lp.useTmpCoords = useTempCoords;
+        }
+    }
+
+    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,
+                resultSpan);
+        if (result[0] >= 0 && result[1] >= 0) {
+            copyCurrentStateToSolution(solution, false);
+            solution.dragViewX = result[0];
+            solution.dragViewY = result[1];
+            solution.dragViewSpanX = resultSpan[0];
+            solution.dragViewSpanY = resultSpan[1];
+            solution.isSolution = true;
+        } else {
+            solution.isSolution = false;
+        }
+        return solution;
+    }
+
+    public void prepareChildForDrag(View child) {
+        markCellsAsUnoccupiedForView(child);
+    }
+
+    /* This seems like it should be obvious and straight-forward, but when the direction vector
+    needs to match with the notion of the dragView pushing other views, we have to employ
+    a slightly more subtle notion of the direction vector. The question is what two points is
+    the vector between? The center of the dragView and its desired destination? Not quite, as
+    this doesn't necessarily coincide with the interaction of the dragView and items occupying
+    those cells. Instead we use some heuristics to often lock the vector to up, down, left
+    or right, which helps make pushing feel right.
+    */
+    private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
+            int spanY, View dragView, int[] resultDirection) {
+        int[] targetDestination = new int[2];
+
+        findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
+        Rect dragRect = new Rect();
+        regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+        dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
+
+        Rect dropRegionRect = new Rect();
+        getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
+                dragView, dropRegionRect, mIntersectingViews);
+
+        int dropRegionSpanX = dropRegionRect.width();
+        int dropRegionSpanY = dropRegionRect.height();
+
+        regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+                dropRegionRect.height(), dropRegionRect);
+
+        int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
+        int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
+
+        if (dropRegionSpanX == mCountX || spanX == mCountX) {
+            deltaX = 0;
+        }
+        if (dropRegionSpanY == mCountY || spanY == mCountY) {
+            deltaY = 0;
+        }
+
+        if (deltaX == 0 && deltaY == 0) {
+            // No idea what to do, give a random direction.
+            resultDirection[0] = 1;
+            resultDirection[1] = 0;
+        } else {
+            computeDirectionVector(deltaX, deltaY, resultDirection);
+        }
+    }
+
+    // For a given cell and span, fetch the set of views intersecting the region.
+    private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
+            View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
+        if (boundingRect != null) {
+            boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+        }
+        intersectingViews.clear();
+        Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+        Rect r1 = new Rect();
+        final int count = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            if (child == dragView) continue;
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            r1.set(lp.cellX, lp.cellY, lp.cellX + lp.cellHSpan, lp.cellY + lp.cellVSpan);
+            if (Rect.intersects(r0, r1)) {
+                mIntersectingViews.add(child);
+                if (boundingRect != null) {
+                    boundingRect.union(r1);
+                }
+            }
+        }
+    }
+
+    boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
+            View dragView, int[] result) {
+        result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+        getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null,
+                mIntersectingViews);
+        return !mIntersectingViews.isEmpty();
+    }
+
+    void revertTempState() {
+        if (!isItemPlacementDirty() || DESTRUCTIVE_REORDER) return;
+        final int count = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY) {
+                lp.tmpCellX = lp.cellX;
+                lp.tmpCellY = lp.cellY;
+                animateChildToPosition(child, lp.cellX, lp.cellY, REORDER_ANIMATION_DURATION,
+                        0, false, false);
+            }
+        }
+        completeAndClearReorderHintAnimations();
+        setItemPlacementDirty(false);
+    }
+
+    boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY,
+            View dragView, int[] direction, boolean commit) {
+        int[] pixelXY = new int[2];
+        regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY);
+
+        // First we determine if things have moved enough to cause a different layout
+        ItemConfiguration swapSolution = simpleSwap(pixelXY[0], pixelXY[1], spanX, spanY,
+                 spanX,  spanY, direction, dragView,  true,  new ItemConfiguration());
+
+        setUseTempCoords(true);
+        if (swapSolution != null && swapSolution.isSolution) {
+            // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+            // committing anything or animating anything as we just want to determine if a solution
+            // exists
+            copySolutionToTempState(swapSolution, dragView);
+            setItemPlacementDirty(true);
+            animateItemsToSolution(swapSolution, dragView, commit);
+
+            if (commit) {
+                commitTempPlacement();
+                completeAndClearReorderHintAnimations();
+                setItemPlacementDirty(false);
+            } else {
+                beginOrAdjustHintAnimations(swapSolution, dragView,
+                        REORDER_ANIMATION_DURATION);
+            }
+            mShortcutsAndWidgets.requestLayout();
+        }
+        return swapSolution.isSolution;
+    }
+
+    int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
+            View dragView, int[] result, int resultSpan[], int mode) {
+        // First we determine if things have moved enough to cause a different layout
+        result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+
+        if (resultSpan == null) {
+            resultSpan = new int[2];
+        }
+
+        // When we are checking drop validity or actually dropping, we don't recompute the
+        // direction vector, since we want the solution to match the preview, and it's possible
+        // that the exact position of the item has changed to result in a new reordering outcome.
+        if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP)
+               && mPreviousReorderDirection[0] != INVALID_DIRECTION) {
+            mDirectionVector[0] = mPreviousReorderDirection[0];
+            mDirectionVector[1] = mPreviousReorderDirection[1];
+            // We reset this vector after drop
+            if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+                mPreviousReorderDirection[0] = INVALID_DIRECTION;
+                mPreviousReorderDirection[1] = INVALID_DIRECTION;
+            }
+        } else {
+            getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
+            mPreviousReorderDirection[0] = mDirectionVector[0];
+            mPreviousReorderDirection[1] = mDirectionVector[1];
+        }
+
+        ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY,
+                 spanX,  spanY, mDirectionVector, dragView,  true,  new ItemConfiguration());
+
+        // We attempt the approach which doesn't shuffle views at all
+        ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX,
+                minSpanY, spanX, spanY, dragView, new ItemConfiguration());
+
+        ItemConfiguration finalSolution = null;
+        if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) {
+            finalSolution = swapSolution;
+        } else if (noShuffleSolution.isSolution) {
+            finalSolution = noShuffleSolution;
+        }
+
+        boolean foundSolution = true;
+        if (!DESTRUCTIVE_REORDER) {
+            setUseTempCoords(true);
+        }
+
+        if (finalSolution != null) {
+            result[0] = finalSolution.dragViewX;
+            result[1] = finalSolution.dragViewY;
+            resultSpan[0] = finalSolution.dragViewSpanX;
+            resultSpan[1] = finalSolution.dragViewSpanY;
+
+            // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+            // committing anything or animating anything as we just want to determine if a solution
+            // exists
+            if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+                if (!DESTRUCTIVE_REORDER) {
+                    copySolutionToTempState(finalSolution, dragView);
+                }
+                setItemPlacementDirty(true);
+                animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP);
+
+                if (!DESTRUCTIVE_REORDER &&
+                        (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
+                    commitTempPlacement();
+                    completeAndClearReorderHintAnimations();
+                    setItemPlacementDirty(false);
+                } else {
+                    beginOrAdjustHintAnimations(finalSolution, dragView,
+                            REORDER_ANIMATION_DURATION);
+                }
+            }
+        } else {
+            foundSolution = false;
+            result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+        }
+
+        if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) {
+            setUseTempCoords(false);
+        }
+
+        mShortcutsAndWidgets.requestLayout();
+        return result;
+    }
+
+    void setItemPlacementDirty(boolean dirty) {
+        mItemPlacementDirty = dirty;
+    }
+    boolean isItemPlacementDirty() {
+        return mItemPlacementDirty;
+    }
+
+    private 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>();
+        boolean isSolution = false;
+        int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY;
+
+        void save() {
+            // Copy current state into savedMap
+            for (View v: map.keySet()) {
+                map.get(v).copy(savedMap.get(v));
+            }
+        }
+
+        void restore() {
+            // Restore current state from savedMap
+            for (View v: savedMap.keySet()) {
+                savedMap.get(v).copy(map.get(v));
+            }
+        }
+
+        void add(View v, CellAndSpan cs) {
+            map.put(v, cs);
+            savedMap.put(v, new CellAndSpan());
+            sortedViews.add(v);
+        }
+
+        int area() {
+            return dragViewSpanX * dragViewSpanY;
+        }
+    }
+
+    private class CellAndSpan {
+        int x, y;
+        int spanX, spanY;
+
+        public CellAndSpan() {
+        }
+
+        public void copy(CellAndSpan copy) {
+            copy.x = x;
+            copy.y = y;
+            copy.spanX = spanX;
+            copy.spanY = spanY;
+        }
+
+        public CellAndSpan(int x, int y, int spanX, int spanY) {
+            this.x = x;
+            this.y = y;
+            this.spanX = spanX;
+            this.spanY = spanY;
+        }
+
+        public String toString() {
+            return "(" + x + ", " + y + ": " + spanX + ", " + spanY + ")";
+        }
+
+    }
+
+    /**
+     * 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.
+     *
+     * @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[] findNearestArea(
+            int pixelX, int pixelY, int spanX, int spanY, int[] result) {
+        return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result);
+    }
+
+    boolean existsEmptyCell() {
+        return findCellForSpan(null, 1, 1);
+    }
+
+    /**
+     * Finds the upper-left coordinate of the first rectangle in the grid that can
+     * hold a cell of the specified dimensions. If intersectX and intersectY are not -1,
+     * then this method will only return coordinates for rectangles that contain the cell
+     * (intersectX, intersectY)
+     *
+     * @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.
+     *
+     * @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);
+
+        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;
+                            }
+                        }
+                    }
+                    if (cellXY != null) {
+                        cellXY[0] = x;
+                        cellXY[1] = y;
+                    }
+                    foundCell = true;
+                    break;
+                }
+            }
+            if (intersectX == -1 && intersectY == -1) {
+                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;
+    }
+
+    /**
+     * A drag event has begun over this layout.
+     * It may have begun over this layout (in which case onDragChild is called first),
+     * or it may have begun on another layout.
+     */
+    void onDragEnter() {
+        mDragEnforcer.onDragEnter();
+        mDragging = true;
+    }
+
+    /**
+     * 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.
+        if (mDragging) {
+            mDragging = false;
+        }
+
+        // Invalidate the drag data
+        mDragCell[0] = mDragCell[1] = -1;
+        mDragOutlineAnims[mDragOutlineCurrent].animateOut();
+        mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length;
+        revertTempState();
+        setIsDragOverlapping(false);
+    }
+
+    /**
+     * Mark a child as having been dropped.
+     * At the beginning of the drag operation, the child may have been on another
+     * screen, but it is re-parented before this method is called.
+     *
+     * @param child The child that is being dropped
+     */
+    void onDropChild(View child) {
+        if (child != null) {
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            lp.dropped = true;
+            child.requestLayout();
+        }
+    }
+
+    /**
+     * Computes a bounding rectangle for a range of cells
+     *
+     * @param cellX X coordinate of upper left corner expressed as a cell position
+     * @param cellY Y coordinate of upper left corner expressed as a cell position
+     * @param cellHSpan Width in cells
+     * @param cellVSpan Height in cells
+     * @param resultRect Rect into which to put the results
+     */
+    public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) {
+        final int cellWidth = mCellWidth;
+        final int cellHeight = mCellHeight;
+        final int widthGap = mWidthGap;
+        final int heightGap = mHeightGap;
+
+        final int hStartPadding = getPaddingLeft();
+        final int vStartPadding = getPaddingTop();
+
+        int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap);
+        int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap);
+
+        int x = hStartPadding + cellX * (cellWidth + widthGap);
+        int y = vStartPadding + cellY * (cellHeight + heightGap);
+
+        resultRect.set(x, y, x + width, y + height);
+    }
+
+    /**
+     * Computes the required horizontal and vertical cell spans to always
+     * fit the given rectangle.
+     *
+     * @param width Width in pixels
+     * @param height Height in pixels
+     * @param result An array of length 2 in which to store the result (may be null).
+     */
+    public int[] rectToCell(int width, int height, int[] result) {
+        return rectToCell(getResources(), width, height, result);
+    }
+
+    public static int[] rectToCell(Resources resources, int width, int height, int[] result) {
+        // Always assume we're working with the smallest span to make sure we
+        // reserve enough space in both orientations.
+        int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width);
+        int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height);
+        int smallerSize = Math.min(actualWidth, actualHeight);
+
+        // Always round up to next largest cell
+        int spanX = (int) Math.ceil(width / (float) smallerSize);
+        int spanY = (int) Math.ceil(height / (float) smallerSize);
+
+        if (result == null) {
+            return new int[] { spanX, spanY };
+        }
+        result[0] = spanX;
+        result[1] = spanY;
+        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
+     */
+    public void calculateSpans(ItemInfo info) {
+        final int minWidth;
+        final int minHeight;
+
+        if (info instanceof LauncherAppWidgetInfo) {
+            minWidth = ((LauncherAppWidgetInfo) info).minWidth;
+            minHeight = ((LauncherAppWidgetInfo) info).minHeight;
+        } else if (info instanceof PendingAddWidgetInfo) {
+            minWidth = ((PendingAddWidgetInfo) info).minWidth;
+            minHeight = ((PendingAddWidgetInfo) info).minHeight;
+        } else {
+            // It's not a widget, so it must be 1x1
+            info.spanX = info.spanY = 1;
+            return;
+        }
+        int[] spans = rectToCell(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++) {
+                mOccupied[x][y] = false;
+            }
+        }
+    }
+
+    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);
+    }
+
+    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);
+    }
+
+    private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied,
+            boolean value) {
+        if (cellX < 0 || cellY < 0) return;
+        for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
+            for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
+                occupied[x][y] = value;
+            }
+        }
+    }
+
+    public int getDesiredWidth() {
+        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) +
+                (Math.max((mCountX - 1), 0) * mWidthGap);
+    }
+
+    public int getDesiredHeight()  {
+        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) +
+                (Math.max((mCountY - 1), 0) * mHeightGap);
+    }
+
+    public boolean isOccupied(int x, int y) {
+        if (x < mCountX && y < mCountY) {
+            return mOccupied[x][y];
+        } else {
+            throw new RuntimeException("Position exceeds the bound of this CellLayout");
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new CellLayout.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof CellLayout.LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        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.
+         */
+        @ViewDebug.ExportedProperty
+        public int cellX;
+
+        /**
+         * Vertical location of the item in the grid.
+         */
+        @ViewDebug.ExportedProperty
+        public int cellY;
+
+        /**
+         * Temporary horizontal location of the item in the grid during reorder
+         */
+        public int tmpCellX;
+
+        /**
+         * Temporary vertical location of the item in the grid during reorder
+         */
+        public int tmpCellY;
+
+        /**
+         * Indicates that the temporary coordinates should be used to layout the items
+         */
+        public boolean useTmpCoords;
+
+        /**
+         * 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;
+
+        /**
+         * Indicates whether the item will set its x, y, width and height parameters freely,
+         * or whether these will be computed based on cellX, cellY, cellHSpan and cellVSpan.
+         */
+        public boolean isLockedToGrid = true;
+
+        /**
+         * Indicates whether this item can be reordered. Always true except in the case of the
+         * the AllApps button.
+         */
+        public boolean canReorder = true;
+
+        // X coordinate of the view in the layout.
+        @ViewDebug.ExportedProperty
+        int x;
+        // Y coordinate of the view in the layout.
+        @ViewDebug.ExportedProperty
+        int y;
+
+        boolean dropped;
+
+        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(int cellWidth, int cellHeight, int widthGap, int heightGap,
+                boolean invertHorizontally, int colCount) {
+            if (isLockedToGrid) {
+                final int myCellHSpan = cellHSpan;
+                final int myCellVSpan = cellVSpan;
+                int myCellX = useTmpCoords ? tmpCellX : cellX;
+                int myCellY = useTmpCoords ? tmpCellY : cellY;
+
+                if (invertHorizontally) {
+                    myCellX = colCount - myCellX - cellHSpan;
+                }
+
+                width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) -
+                        leftMargin - rightMargin;
+                height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) -
+                        topMargin - bottomMargin;
+                x = (int) (myCellX * (cellWidth + widthGap) + leftMargin);
+                y = (int) (myCellY * (cellHeight + heightGap) + topMargin);
+            }
+        }
+
+        public String toString() {
+            return "(" + this.cellX + ", " + this.cellY + ")";
+        }
+
+        public void setWidth(int width) {
+            this.width = width;
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public void setHeight(int height) {
+            this.height = height;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setY(int y) {
+            this.y = y;
+        }
+
+        public int getY() {
+            return y;
+        }
+    }
+
+    // This class stores info for two purposes:
+    // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY,
+    //    its spanX, spanY, and the screen it is on
+    // 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 {
+        View cell;
+        int cellX = -1;
+        int cellY = -1;
+        int spanX;
+        int spanY;
+        int screen;
+        long container;
+
+        @Override
+        public String toString() {
+            return "Cell[view=" + (cell == null ? "null" : cell.getClass())
+                    + ", x=" + cellX + ", y=" + cellY + "]";
+        }
+    }
+
+    public boolean lastDownOnOccupiedCell() {
+        return mLastDownOnOccupiedCell;
+    }
+}
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
new file mode 100644
index 0000000..7760f4e
--- /dev/null
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -0,0 +1,62 @@
+/*
+ * 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.view.View;
+
+public class CheckLongPressHelper {
+    private View mView;
+    private boolean mHasPerformedLongPress;
+    private CheckForLongPress mPendingCheckForLongPress;
+
+    class CheckForLongPress implements Runnable {
+        public void run() {
+            if ((mView.getParent() != null) && mView.hasWindowFocus()
+                    && !mHasPerformedLongPress) {
+                if (mView.performLongClick()) {
+                    mView.setPressed(false);
+                    mHasPerformedLongPress = true;
+                }
+            }
+        }
+    }
+
+    public CheckLongPressHelper(View v) {
+        mView = v;
+    }
+
+    public void postCheckForLongPress() {
+        mHasPerformedLongPress = false;
+
+        if (mPendingCheckForLongPress == null) {
+            mPendingCheckForLongPress = new CheckForLongPress();
+        }
+        mView.postDelayed(mPendingCheckForLongPress, LauncherApplication.getLongPressTimeout());
+    }
+
+    public void cancelLongPress() {
+        mHasPerformedLongPress = false;
+        if (mPendingCheckForLongPress != null) {
+            mView.removeCallbacks(mPendingCheckForLongPress);
+            mPendingCheckForLongPress = null;
+        }
+    }
+
+    public boolean hasPerformedLongPress() {
+        return mHasPerformedLongPress;
+    }
+}
diff --git a/src/com/android/launcher3/Cling.java b/src/com/android/launcher3/Cling.java
new file mode 100644
index 0000000..6bb183c
--- /dev/null
+++ b/src/com/android/launcher3/Cling.java
@@ -0,0 +1,271 @@
+/*
+ * 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.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.FocusFinder;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+
+public class Cling extends FrameLayout {
+
+    static final String WORKSPACE_CLING_DISMISSED_KEY = "cling.workspace.dismissed";
+    static final String ALLAPPS_CLING_DISMISSED_KEY = "cling.allapps.dismissed";
+    static final String FOLDER_CLING_DISMISSED_KEY = "cling.folder.dismissed";
+
+    private static String WORKSPACE_PORTRAIT = "workspace_portrait";
+    private static String WORKSPACE_LANDSCAPE = "workspace_landscape";
+    private static String WORKSPACE_LARGE = "workspace_large";
+    private static String WORKSPACE_CUSTOM = "workspace_custom";
+
+    private static String ALLAPPS_PORTRAIT = "all_apps_portrait";
+    private static String ALLAPPS_LANDSCAPE = "all_apps_landscape";
+    private static String ALLAPPS_LARGE = "all_apps_large";
+
+    private static String FOLDER_PORTRAIT = "folder_portrait";
+    private static String FOLDER_LANDSCAPE = "folder_landscape";
+    private static String FOLDER_LARGE = "folder_large";
+
+    private Launcher mLauncher;
+    private boolean mIsInitialized;
+    private String mDrawIdentifier;
+    private Drawable mBackground;
+    private Drawable mPunchThroughGraphic;
+    private Drawable mHandTouchGraphic;
+    private int mPunchThroughGraphicCenterRadius;
+    private int mAppIconSize;
+    private int mButtonBarHeight;
+    private float mRevealRadius;
+    private int[] mPositionData;
+
+    private Paint mErasePaint;
+
+    public Cling(Context context) {
+        this(context, null, 0);
+    }
+
+    public Cling(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public Cling(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Cling, defStyle, 0);
+        mDrawIdentifier = a.getString(R.styleable.Cling_drawIdentifier);
+        a.recycle();
+
+        setClickable(true);
+    }
+
+    void init(Launcher l, int[] positionData) {
+        if (!mIsInitialized) {
+            mLauncher = l;
+            mPositionData = positionData;
+
+            Resources r = getContext().getResources();
+
+            mPunchThroughGraphic = r.getDrawable(R.drawable.cling);
+            mPunchThroughGraphicCenterRadius =
+                r.getDimensionPixelSize(R.dimen.clingPunchThroughGraphicCenterRadius);
+            mAppIconSize = r.getDimensionPixelSize(R.dimen.app_icon_size);
+            mRevealRadius = r.getDimensionPixelSize(R.dimen.reveal_radius) * 1f;
+            mButtonBarHeight = r.getDimensionPixelSize(R.dimen.button_bar_height);
+
+            mErasePaint = new Paint();
+            mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+            mErasePaint.setColor(0xFFFFFF);
+            mErasePaint.setAlpha(0);
+
+            mIsInitialized = true;
+        }
+    }
+
+    void cleanup() {
+        mBackground = null;
+        mPunchThroughGraphic = null;
+        mHandTouchGraphic = null;
+        mIsInitialized = false;
+    }
+
+    public String getDrawIdentifier() {
+        return mDrawIdentifier;
+    }
+
+    private int[] getPunchThroughPositions() {
+        if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT)) {
+            return new int[]{getMeasuredWidth() / 2, getMeasuredHeight() - (mButtonBarHeight / 2)};
+        } else if (mDrawIdentifier.equals(WORKSPACE_LANDSCAPE)) {
+            return new int[]{getMeasuredWidth() - (mButtonBarHeight / 2), getMeasuredHeight() / 2};
+        } else if (mDrawIdentifier.equals(WORKSPACE_LARGE)) {
+            final float scale = LauncherApplication.getScreenDensity();
+            final int cornerXOffset = (int) (scale * 15);
+            final int cornerYOffset = (int) (scale * 10);
+            return new int[]{getMeasuredWidth() - cornerXOffset, cornerYOffset};
+        } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) ||
+                   mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) ||
+                   mDrawIdentifier.equals(ALLAPPS_LARGE)) {
+            return mPositionData;
+        }
+        return new int[]{-1, -1};
+    }
+
+    @Override
+    public View focusSearch(int direction) {
+        return this.focusSearch(this, direction);
+    }
+
+    @Override
+    public View focusSearch(View focused, int direction) {
+        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        return (mDrawIdentifier.equals(WORKSPACE_PORTRAIT)
+                || mDrawIdentifier.equals(WORKSPACE_LANDSCAPE)
+                || mDrawIdentifier.equals(WORKSPACE_LARGE)
+                || mDrawIdentifier.equals(ALLAPPS_PORTRAIT)
+                || mDrawIdentifier.equals(ALLAPPS_LANDSCAPE)
+                || mDrawIdentifier.equals(ALLAPPS_LARGE)
+                || mDrawIdentifier.equals(WORKSPACE_CUSTOM));
+    }
+
+    @Override
+    public boolean onTouchEvent(android.view.MotionEvent event) {
+        if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) ||
+            mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) ||
+            mDrawIdentifier.equals(WORKSPACE_LARGE) ||
+            mDrawIdentifier.equals(ALLAPPS_PORTRAIT) ||
+            mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) ||
+            mDrawIdentifier.equals(ALLAPPS_LARGE)) {
+
+            int[] positions = getPunchThroughPositions();
+            for (int i = 0; i < positions.length; i += 2) {
+                double diff = Math.sqrt(Math.pow(event.getX() - positions[i], 2) +
+                        Math.pow(event.getY() - positions[i + 1], 2));
+                if (diff < mRevealRadius) {
+                    return false;
+                }
+            }
+        } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) ||
+                   mDrawIdentifier.equals(FOLDER_LANDSCAPE) ||
+                   mDrawIdentifier.equals(FOLDER_LARGE)) {
+            Folder f = mLauncher.getWorkspace().getOpenFolder();
+            if (f != null) {
+                Rect r = new Rect();
+                f.getHitRect(r);
+                if (r.contains((int) event.getX(), (int) event.getY())) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    };
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mIsInitialized) {
+            DisplayMetrics metrics = new DisplayMetrics();
+            mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics);
+
+            // Initialize the draw buffer (to allow punching through)
+            Bitmap b = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(),
+                    Bitmap.Config.ARGB_8888);
+            Canvas c = new Canvas(b);
+
+            // Draw the background
+            if (mBackground == null) {
+                if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) ||
+                        mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) ||
+                        mDrawIdentifier.equals(WORKSPACE_LARGE)) {
+                    mBackground = getResources().getDrawable(R.drawable.bg_cling1);
+                } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) ||
+                        mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) ||
+                        mDrawIdentifier.equals(ALLAPPS_LARGE)) {
+                    mBackground = getResources().getDrawable(R.drawable.bg_cling2);
+                } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) ||
+                        mDrawIdentifier.equals(FOLDER_LANDSCAPE)) {
+                    mBackground = getResources().getDrawable(R.drawable.bg_cling3);
+                } else if (mDrawIdentifier.equals(FOLDER_LARGE)) {
+                    mBackground = getResources().getDrawable(R.drawable.bg_cling4);
+                } else if (mDrawIdentifier.equals(WORKSPACE_CUSTOM)) {
+                    mBackground = getResources().getDrawable(R.drawable.bg_cling5);
+                }
+            }
+            if (mBackground != null) {
+                mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
+                mBackground.draw(c);
+            } else {
+                c.drawColor(0x99000000);
+            }
+
+            int cx = -1;
+            int cy = -1;
+            float scale = mRevealRadius / mPunchThroughGraphicCenterRadius;
+            int dw = (int) (scale * mPunchThroughGraphic.getIntrinsicWidth());
+            int dh = (int) (scale * mPunchThroughGraphic.getIntrinsicHeight());
+
+            // Determine where to draw the punch through graphic
+            int[] positions = getPunchThroughPositions();
+            for (int i = 0; i < positions.length; i += 2) {
+                cx = positions[i];
+                cy = positions[i + 1];
+                if (cx > -1 && cy > -1) {
+                    c.drawCircle(cx, cy, mRevealRadius, mErasePaint);
+                    mPunchThroughGraphic.setBounds(cx - dw/2, cy - dh/2, cx + dw/2, cy + dh/2);
+                    mPunchThroughGraphic.draw(c);
+                }
+            }
+
+            // Draw the hand graphic in All Apps
+            if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) ||
+                mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) ||
+                mDrawIdentifier.equals(ALLAPPS_LARGE)) {
+                if (mHandTouchGraphic == null) {
+                    mHandTouchGraphic = getResources().getDrawable(R.drawable.hand);
+                }
+                int offset = mAppIconSize / 4;
+                mHandTouchGraphic.setBounds(cx + offset, cy + offset,
+                        cx + mHandTouchGraphic.getIntrinsicWidth() + offset,
+                        cy + mHandTouchGraphic.getIntrinsicHeight() + offset);
+                mHandTouchGraphic.draw(c);
+            }
+
+            canvas.drawBitmap(b, 0, 0, null);
+            c.setBitmap(null);
+            b = null;
+        }
+
+        // Draw the rest of the cling
+        super.dispatchDraw(canvas);
+    };
+}
diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java
new file mode 100644
index 0000000..92ecf96
--- /dev/null
+++ b/src/com/android/launcher3/DeferredHandler.java
@@ -0,0 +1,146 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.util.Pair;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+/**
+ * Queue of things to run on a looper thread.  Items posted with {@link #post} will not
+ * be actually enqued on the handler until after the last one has run, to keep from
+ * starving the thread.
+ *
+ * This class is fifo.
+ */
+public class DeferredHandler {
+    private LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>();
+    private MessageQueue mMessageQueue = Looper.myQueue();
+    private Impl mHandler = new Impl();
+
+    private 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.run();
+            synchronized (mQueue) {
+                scheduleNextLocked();
+            }
+        }
+
+        public boolean queueIdle() {
+            handleMessage(null);
+            return false;
+        }
+    }
+
+    private class IdleRunnable implements Runnable {
+        Runnable mRunnable;
+
+        IdleRunnable(Runnable r) {
+            mRunnable = r;
+        }
+
+        public void run() {
+            mRunnable.run();
+        }
+    }
+
+    public 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));
+            if (mQueue.size() == 1) {
+                scheduleNextLocked();
+            }
+        }
+    }
+
+    /** 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);
+    }
+
+    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() {
+        synchronized (mQueue) {
+            mQueue.clear();
+        }
+    }
+
+    /** Runs all queued Runnables from the calling thread. */
+    public void flush() {
+        LinkedList<Pair<Runnable, Integer>> queue = new LinkedList<Pair<Runnable, Integer>>();
+        synchronized (mQueue) {
+            queue.addAll(mQueue);
+            mQueue.clear();
+        }
+        for (Pair<Runnable, Integer> p : queue) {
+            p.first.run();
+        }
+    }
+
+    void scheduleNextLocked() {
+        if (mQueue.size() > 0) {
+            Pair<Runnable, Integer> p = mQueue.getFirst();
+            Runnable peek = p.first;
+            if (peek instanceof IdleRunnable) {
+                mMessageQueue.addIdleHandler(mHandler);
+            } else {
+                mHandler.sendEmptyMessage(1);
+            }
+        }
+    }
+}
+
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
new file mode 100644
index 0000000..eba1547
--- /dev/null
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -0,0 +1,437 @@
+/*
+ * 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.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+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.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.R;
+
+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;
+
+    public DeleteDropTarget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @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 (!LauncherApplication.isScreenLarge()) {
+                setText("");
+            }
+        }
+    }
+
+    private boolean isAllAppsApplication(DragSource source, Object info) {
+        return (source instanceof AppsCustomizePagedView) && (info instanceof ApplicationInfo);
+    }
+    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);
+    }
+
+    private void setHoverColor() {
+        mCurrentDrawable.startTransition(mTransitionDuration);
+        setTextColor(mHoverColor);
+    }
+    private void resetHoverColor() {
+        mCurrentDrawable.resetTransition();
+        setTextColor(mOriginalTextColor);
+    }
+
+    @Override
+    public boolean acceptDrop(DragObject d) {
+        // We can remove everything including App shortcuts, folders, widgets, etc.
+        return true;
+    }
+
+    @Override
+    public void onDragStart(DragSource source, Object info, int dragAction) {
+        boolean isVisible = true;
+        boolean isUninstall = false;
+
+        // If we are dragging a widget from AppsCustomize, hide the delete target
+        if (isAllAppsWidget(source, info)) {
+            isVisible = false;
+        }
+
+        // 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
+        if (isAllAppsApplication(source, info)) {
+            ApplicationInfo appInfo = (ApplicationInfo) info;
+            if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) != 0) {
+                isUninstall = true;
+            } else {
+                isVisible = false;
+            }
+        }
+
+        if (isUninstall) {
+            setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
+        } else {
+            setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
+        }
+        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
+
+        mActive = isVisible;
+        resetHoverColor();
+        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
+        if (getText().length() > 0) {
+            setText(isUninstall ? R.string.delete_target_uninstall_label
+                : R.string.delete_target_label);
+        }
+    }
+
+    @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) {
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        Rect from = new Rect();
+        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
+        Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
+                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
+        float scale = (float) to.width() / from.width();
+
+        mSearchDropTargetBar.deferOnDragEnd();
+        Runnable onAnimationEndRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mSearchDropTargetBar.onDragEnd();
+                mLauncher.exitSpringLoadedDragMode();
+                completeDrop(d);
+            }
+        };
+        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 completeDrop(DragObject d) {
+        ItemInfo item = (ItemInfo) d.dragInfo;
+
+        if (isAllAppsApplication(d.dragSource, item)) {
+            // Uninstall the application if it is being dragged from AppsCustomize
+            mLauncher.startApplicationUninstallActivity((ApplicationInfo) item);
+        } 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);
+
+            final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
+            final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
+            if (appWidgetHost != null) {
+                // Deleting an app widget ID is a void call but writes to disk before returning
+                // to the caller...
+                new Thread("deleteAppWidgetId") {
+                    public void run() {
+                        appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
+                    }
+                }.start();
+            }
+        }
+    }
+
+    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) {
+        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
+                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
+        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;
+        }
+    };
+    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);
+    }
+
+    public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
+        final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
+
+        // 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;
+        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 float mOffset = 0f;
+
+            @Override
+            public float getInterpolation(float t) {
+                if (mCount < 0) {
+                    mCount++;
+                } else if (mCount == 0) {
+                    mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
+                            startTime) / duration);
+                    mCount++;
+                }
+                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);
+        }
+        Runnable onAnimationEndRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mSearchDropTargetBar.onDragEnd();
+
+                // 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.getDragController().onDeferredEndFling(d);
+            }
+        };
+        dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
+                DragLayer.ANIMATION_END_DISAPPEAR, null);
+    }
+}
diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java
new file mode 100644
index 0000000..8635589
--- /dev/null
+++ b/src/com/android/launcher3/DragController.java
@@ -0,0 +1,821 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Vibrator;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+
+/**
+ * Class for initiating a drag within a view or across multiple views.
+ */
+public class DragController {
+    private static final String TAG = "Launcher.DragController";
+
+    /** Indicates the drag is a move.  */
+    public static int DRAG_ACTION_MOVE = 0;
+
+    /** 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 = 750;
+    private static final int VIBRATE_DURATION = 15;
+
+    private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
+
+    private static final int SCROLL_OUTSIDE_ZONE = 0;
+    private static final int SCROLL_WAITING_IN_ZONE = 1;
+
+    static final int SCROLL_NONE = -1;
+    static final int SCROLL_LEFT = 0;
+    static final int SCROLL_RIGHT = 1;
+
+    private static final float MAX_FLING_DEGREES = 35f;
+
+    private Launcher mLauncher;
+    private Handler mHandler;
+    private final Vibrator mVibrator;
+
+    // temporaries to avoid gc thrash
+    private Rect mRectTemp = new Rect();
+    private final int[] mCoordinatesTemp = new int[2];
+
+    /** Whether or not we're dragging. */
+    private boolean mDragging;
+
+    /** X coordinate of the down event. */
+    private int mMotionDownX;
+
+    /** Y coordinate of the down event. */
+    private int mMotionDownY;
+
+    /** the area at the edge of the screen that makes the workspace go left
+     *   or right while you're dragging.
+     */
+    private int mScrollZone;
+
+    private DropTarget.DragObject mDragObject;
+
+    /** Who can receive drop events */
+    private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>();
+    private ArrayList<DragListener> mListeners = new ArrayList<DragListener>();
+    private DropTarget mFlingToDeleteDropTarget;
+
+    /** The window token used as the parent for the DragView. */
+    private IBinder mWindowToken;
+
+    /** The view that will be scrolled when dragging to the left and right edges of the screen. */
+    private View mScrollView;
+
+    private View mMoveTarget;
+
+    private DragScroller mDragScroller;
+    private 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;
+
+    private int mTmpPoint[] = new int[2];
+    private Rect mDragLayerRect = new Rect();
+
+    protected int mFlingToDeleteThresholdVelocity;
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * Interface to receive notifications when a drag starts or stops
+     */
+    interface DragListener {
+        
+        /**
+         * A drag has begun
+         * 
+         * @param source An object representing where the drag originated
+         * @param info The data associated with the object that is being dragged
+         * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE}
+         *        or {@link DragController#DRAG_ACTION_COPY}
+         */
+        void onDragStart(DragSource source, Object info, int dragAction);
+        
+        /**
+         * The drag has ended
+         */
+        void onDragEnd();
+    }
+    
+    /**
+     * Used to create a new DragLayer from XML.
+     *
+     * @param context The application's context.
+     */
+    public DragController(Launcher launcher) {
+        Resources r = launcher.getResources();
+        mLauncher = launcher;
+        mHandler = new Handler();
+        mScrollZone = r.getDimensionPixelSize(R.dimen.scroll_zone);
+        mVelocityTracker = VelocityTracker.obtain();
+        mVibrator = (Vibrator) launcher.getSystemService(Context.VIBRATOR_SERVICE);
+
+        float density = r.getDisplayMetrics().density;
+        mFlingToDeleteThresholdVelocity =
+                (int) (r.getInteger(R.integer.config_flingToDeleteMinVelocity) * density);
+    }
+
+    public boolean dragging() {
+        return mDragging;
+    }
+
+    /**
+     * Starts a drag.
+     *
+     * @param v The view that is being dragged
+     * @param bmp The bitmap that represents the view being dragged
+     * @param source An object representing where the drag originated
+     * @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 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) {
+        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);
+
+        startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null,
+                null, initialDragViewScale);
+
+        if (dragAction == DRAG_ACTION_MOVE) {
+            v.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Starts a drag.
+     *
+     * @param b The bitmap to display as the drag image.  It will be re-scaled to the
+     *          enlarged size.
+     * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
+     * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
+     * @param source An object representing where the drag originated
+     * @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 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(Bitmap b, int dragLayerX, int dragLayerY,
+            DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
+            float initialDragViewScale) {
+        if (PROFILE_DRAWING_DURING_DRAG) {
+            android.os.Debug.startMethodTracing("Launcher");
+        }
+
+        // Hide soft keyboard, if visible
+        if (mInputMethodManager == null) {
+            mInputMethodManager = (InputMethodManager)
+                    mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
+        }
+        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
+
+        for (DragListener listener : mListeners) {
+            listener.onDragStart(source, dragInfo, dragAction);
+        }
+
+        final int registrationX = mMotionDownX - dragLayerX;
+        final int registrationY = mMotionDownY - dragLayerY;
+
+        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
+        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
+
+        mDragging = true;
+
+        mDragObject = new DropTarget.DragObject();
+
+        mDragObject.dragComplete = false;
+        mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
+        mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+        mDragObject.dragSource = source;
+        mDragObject.dragInfo = dragInfo;
+
+        mVibrator.vibrate(VIBRATE_DURATION);
+
+        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
+                registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
+
+        if (dragOffset != null) {
+            dragView.setDragVisualizeOffset(new Point(dragOffset));
+        }
+        if (dragRegion != null) {
+            dragView.setDragRegion(new Rect(dragRegion));
+        }
+
+        dragView.show(mMotionDownX, mMotionDownY);
+        handleMoveEvent(mMotionDownX, mMotionDownY);
+    }
+
+    /**
+     * Draw the view into a bitmap.
+     */
+    Bitmap getViewBitmap(View v) {
+        v.clearFocus();
+        v.setPressed(false);
+
+        boolean willNotCache = v.willNotCacheDrawing();
+        v.setWillNotCacheDrawing(false);
+
+        // Reset the drawing cache background color to fully transparent
+        // for the duration of this operation
+        int color = v.getDrawingCacheBackgroundColor();
+        v.setDrawingCacheBackgroundColor(0);
+        float alpha = v.getAlpha();
+        v.setAlpha(1.0f);
+
+        if (color != 0) {
+            v.destroyDrawingCache();
+        }
+        v.buildDrawingCache();
+        Bitmap cacheBitmap = v.getDrawingCache();
+        if (cacheBitmap == null) {
+            Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException());
+            return null;
+        }
+
+        Bitmap bitmap = Bitmap.createBitmap(cacheBitmap);
+
+        // Restore the view
+        v.destroyDrawingCache();
+        v.setAlpha(alpha);
+        v.setWillNotCacheDrawing(willNotCache);
+        v.setDrawingCacheBackgroundColor(color);
+
+        return bitmap;
+    }
+
+    /**
+     * Call this from a drag source view like this:
+     *
+     * <pre>
+     *  @Override
+     *  public boolean dispatchKeyEvent(KeyEvent event) {
+     *      return mDragController.dispatchKeyEvent(this, event)
+     *              || super.dispatchKeyEvent(event);
+     * </pre>
+     */
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mDragging;
+    }
+
+    public boolean isDragging() {
+        return mDragging;
+    }
+
+    /**
+     * Stop dragging without dropping.
+     */
+    public void cancelDrag() {
+        if (mDragging) {
+            if (mLastDropTarget != null) {
+                mLastDropTarget.onDragExit(mDragObject);
+            }
+            mDragObject.deferDragViewCleanupPostAnimation = false;
+            mDragObject.cancelled = true;
+            mDragObject.dragComplete = true;
+            mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false);
+        }
+        endDrag();
+    }
+    public void onAppsRemoved(ArrayList<ApplicationInfo> appInfos, Context context) {
+        // Cancel the current drag if we are removing an app that we are dragging
+        if (mDragObject != null) {
+            Object rawDragInfo = mDragObject.dragInfo;
+            if (rawDragInfo instanceof ShortcutInfo) {
+                ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo;
+                for (ApplicationInfo info : appInfos) {
+                    // Added null checks to prevent NPE we've seen in the wild
+                    if (dragInfo != null &&
+                        dragInfo.intent != null) {
+                        boolean isSameComponent =
+                                dragInfo.intent.getComponent().equals(info.componentName);
+                        if (isSameComponent) {
+                            cancelDrag();
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void endDrag() {
+        if (mDragging) {
+            mDragging = false;
+            clearScrollRunnable();
+            boolean isDeferred = false;
+            if (mDragObject.dragView != null) {
+                isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
+                if (!isDeferred) {
+                    mDragObject.dragView.remove();
+                }
+                mDragObject.dragView = null;
+            }
+
+            // Only end the drag if we are not deferred
+            if (!isDeferred) {
+                for (DragListener listener : mListeners) {
+                    listener.onDragEnd();
+                }
+            }
+        }
+
+        releaseVelocityTracker();
+    }
+
+    /**
+     * This only gets called as a result of drag view cleanup being deferred in endDrag();
+     */
+    void onDeferredEndDrag(DragView dragView) {
+        dragView.remove();
+
+        // If we skipped calling onDragEnd() before, do it now
+        for (DragListener listener : mListeners) {
+            listener.onDragEnd();
+        }
+    }
+
+    void onDeferredEndFling(DropTarget.DragObject d) {
+        d.dragSource.onFlingToDeleteCompleted();
+    }
+
+    /**
+     * Clamps the position to the drag layer bounds.
+     */
+    private int[] getClampedDragLayerPos(float x, float y) {
+        mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
+        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
+        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
+        return mTmpPoint;
+    }
+
+    long getLastGestureUpTime() {
+        if (mDragging) {
+            return System.currentTimeMillis();
+        } else {
+            return mLastTouchUpTime;
+        }
+    }
+
+    void resetLastGestureUpTime() {
+        mLastTouchUpTime = -1;
+    }
+
+    /**
+     * Call this from a drag source view.
+     */
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        @SuppressWarnings("all") // suppress dead code warning
+        final boolean debug = false;
+        if (debug) {
+            Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
+                    + mDragging);
+        }
+
+        // Update the velocity tracker
+        acquireVelocityTrackerAndAddMovement(ev);
+
+        final int action = ev.getAction();
+        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
+        final int dragLayerX = dragLayerPos[0];
+        final int dragLayerY = dragLayerPos[1];
+
+        switch (action) {
+            case MotionEvent.ACTION_MOVE:
+                break;
+            case MotionEvent.ACTION_DOWN:
+                // Remember location of down touch
+                mMotionDownX = dragLayerX;
+                mMotionDownY = dragLayerY;
+                mLastDropTarget = null;
+                break;
+            case MotionEvent.ACTION_UP:
+                mLastTouchUpTime = System.currentTimeMillis();
+                if (mDragging) {
+                    PointF vec = isFlingingToDelete(mDragObject.dragSource);
+                    if (vec != null) {
+                        dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
+                    } else {
+                        drop(dragLayerX, dragLayerY);
+                    }
+                }
+                endDrag();
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancelDrag();
+                break;
+        }
+
+        return mDragging;
+    }
+
+    /**
+     * Sets the view that should handle move events.
+     */
+    void setMoveTarget(View view) {
+        mMoveTarget = view;
+    }    
+
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
+    }
+
+    private void clearScrollRunnable() {
+        mHandler.removeCallbacks(mScrollRunnable);
+        if (mScrollState == SCROLL_WAITING_IN_ZONE) {
+            mScrollState = SCROLL_OUTSIDE_ZONE;
+            mScrollRunnable.setDirection(SCROLL_RIGHT);
+            mDragScroller.onExitScrollArea();
+            mLauncher.getDragLayer().onExitScrollArea();
+        }
+    }
+
+    private void handleMoveEvent(int x, int y) {
+        mDragObject.dragView.move(x, y);
+
+        // Drop on someone?
+        final int[] coordinates = mCoordinatesTemp;
+        DropTarget dropTarget = findDropTarget(x, y, coordinates);
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+        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));
+        mLastTouch[0] = x;
+        mLastTouch[1] = y;
+        checkScrollState(x, y);
+    }
+
+    public void forceTouchMove() {
+        int[] dummyCoordinates = mCoordinatesTemp;
+        DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates);
+        checkTouchMove(dropTarget);
+    }
+
+    private void checkTouchMove(DropTarget dropTarget) {
+        if (dropTarget != null) {
+            DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
+            if (delegate != null) {
+                dropTarget = delegate;
+            }
+
+            if (mLastDropTarget != dropTarget) {
+                if (mLastDropTarget != null) {
+                    mLastDropTarget.onDragExit(mDragObject);
+                }
+                dropTarget.onDragEnter(mDragObject);
+            }
+            dropTarget.onDragOver(mDragObject);
+        } else {
+            if (mLastDropTarget != null) {
+                mLastDropTarget.onDragExit(mDragObject);
+            }
+        }
+        mLastDropTarget = dropTarget;
+    }
+
+    private 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;
+
+        if (x < mScrollZone) {
+            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
+                mScrollState = SCROLL_WAITING_IN_ZONE;
+                if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) {
+                    dragLayer.onEnterScrollArea(forwardDirection);
+                    mScrollRunnable.setDirection(forwardDirection);
+                    mHandler.postDelayed(mScrollRunnable, delay);
+                }
+            }
+        } else if (x > mScrollView.getWidth() - mScrollZone) {
+            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
+                mScrollState = SCROLL_WAITING_IN_ZONE;
+                if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) {
+                    dragLayer.onEnterScrollArea(backwardsDirection);
+                    mScrollRunnable.setDirection(backwardsDirection);
+                    mHandler.postDelayed(mScrollRunnable, delay);
+                }
+            }
+        } else {
+            clearScrollRunnable();
+        }
+    }
+
+    /**
+     * Call this from a drag source view.
+     */
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mDragging) {
+            return false;
+        }
+
+        // Update the velocity tracker
+        acquireVelocityTrackerAndAddMovement(ev);
+
+        final int action = ev.getAction();
+        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
+        final int dragLayerX = dragLayerPos[0];
+        final int dragLayerY = dragLayerPos[1];
+
+        switch (action) {
+        case MotionEvent.ACTION_DOWN:
+            // Remember where the motion event started
+            mMotionDownX = dragLayerX;
+            mMotionDownY = dragLayerY;
+
+            if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
+                mScrollState = SCROLL_WAITING_IN_ZONE;
+                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
+            } else {
+                mScrollState = SCROLL_OUTSIDE_ZONE;
+            }
+            break;
+        case MotionEvent.ACTION_MOVE:
+            handleMoveEvent(dragLayerX, dragLayerY);
+            break;
+        case MotionEvent.ACTION_UP:
+            // Ensure that we've processed a move event at the current pointer location.
+            handleMoveEvent(dragLayerX, dragLayerY);
+            mHandler.removeCallbacks(mScrollRunnable);
+
+            if (mDragging) {
+                PointF vec = isFlingingToDelete(mDragObject.dragSource);
+                if (vec != null) {
+                    dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
+                } else {
+                    drop(dragLayerX, dragLayerY);
+                }
+            }
+            endDrag();
+            break;
+        case MotionEvent.ACTION_CANCEL:
+            mHandler.removeCallbacks(mScrollRunnable);
+            cancelDrag();
+            break;
+        }
+
+        return true;
+    }
+
+    /**
+     * 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.
+     */
+    private PointF isFlingingToDelete(DragSource source) {
+        if (mFlingToDeleteDropTarget == null) return null;
+        if (!source.supportsFlingToDelete()) return null;
+
+        ViewConfiguration config = ViewConfiguration.get(mLauncher);
+        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(MAX_FLING_DEGREES)) {
+                return vel;
+            }
+        }
+        return null;
+    }
+
+    private void dropOnFlingToDeleteTarget(float x, float y, PointF vel) {
+        final int[] coordinates = mCoordinatesTemp;
+
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+
+        // Clean up dragging on the target if it's not the current fling delete target otherwise,
+        // start dragging to it.
+        if (mLastDropTarget != null && mFlingToDeleteDropTarget != mLastDropTarget) {
+            mLastDropTarget.onDragExit(mDragObject);
+        }
+
+        // Drop onto the fling-to-delete target
+        boolean accepted = false;
+        mFlingToDeleteDropTarget.onDragEnter(mDragObject);
+        // We must set dragComplete to true _only_ after we "enter" the fling-to-delete target for
+        // "drop"
+        mDragObject.dragComplete = true;
+        mFlingToDeleteDropTarget.onDragExit(mDragObject);
+        if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) {
+            mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y,
+                    vel);
+            accepted = true;
+        }
+        mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true,
+                accepted);
+    }
+
+    private void drop(float x, float y) {
+        final int[] coordinates = mCoordinatesTemp;
+        final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
+
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+        boolean accepted = false;
+        if (dropTarget != null) {
+            mDragObject.dragComplete = true;
+            dropTarget.onDragExit(mDragObject);
+            if (dropTarget.acceptDrop(mDragObject)) {
+                dropTarget.onDrop(mDragObject);
+                accepted = true;
+            }
+        }
+        mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);
+    }
+
+    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
+        final Rect r = mRectTemp;
+
+        final ArrayList<DropTarget> dropTargets = mDropTargets;
+        final int count = dropTargets.size();
+        for (int i=count-1; i>=0; i--) {
+            DropTarget target = dropTargets.get(i);
+            if (!target.isDropEnabled())
+                continue;
+
+            target.getHitRect(r);
+
+            // Convert the hit rect to DragLayer coordinates
+            target.getLocationInDragLayer(dropCoordinates);
+            r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
+
+            mDragObject.x = x;
+            mDragObject.y = y;
+            if (r.contains(x, y)) {
+                DropTarget delegate = target.getDropTargetDelegate(mDragObject);
+                if (delegate != null) {
+                    target = delegate;
+                    target.getLocationInDragLayer(dropCoordinates);
+                }
+
+                // Make dropCoordinates relative to the DropTarget
+                dropCoordinates[0] = x - dropCoordinates[0];
+                dropCoordinates[1] = y - dropCoordinates[1];
+
+                return target;
+            }
+        }
+        return null;
+    }
+
+    public void setDragScoller(DragScroller scroller) {
+        mDragScroller = scroller;
+    }
+
+    public void setWindowToken(IBinder token) {
+        mWindowToken = token;
+    }
+
+    /**
+     * Sets the drag listner which will be notified when a drag starts or ends.
+     */
+    public void addDragListener(DragListener l) {
+        mListeners.add(l);
+    }
+
+    /**
+     * Remove a previously installed drag listener.
+     */
+    public void removeDragListener(DragListener l) {
+        mListeners.remove(l);
+    }
+
+    /**
+     * Add a DropTarget to the list of potential places to receive drop events.
+     */
+    public void addDropTarget(DropTarget target) {
+        mDropTargets.add(target);
+    }
+
+    /**
+     * Don't send drop events to <em>target</em> any more.
+     */
+    public void removeDropTarget(DropTarget target) {
+        mDropTargets.remove(target);
+    }
+
+    /**
+     * Sets the current fling-to-delete drop target.
+     */
+    public void setFlingToDeleteDropTarget(DropTarget target) {
+        mFlingToDeleteDropTarget = target;
+    }
+
+    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+    }
+
+    private void releaseVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    /**
+     * Set which view scrolls for touch events near the edge of the screen.
+     */
+    public void setScrollView(View v) {
+        mScrollView = v;
+    }
+
+    DragView getDragView() {
+        return mDragObject.dragView;
+    }
+
+    private class ScrollRunnable implements Runnable {
+        private int mDirection;
+
+        ScrollRunnable() {
+        }
+
+        public void run() {
+            if (mDragScroller != null) {
+                if (mDirection == SCROLL_LEFT) {
+                    mDragScroller.scrollLeft();
+                } else {
+                    mDragScroller.scrollRight();
+                }
+                mScrollState = SCROLL_OUTSIDE_ZONE;
+                mDistanceSinceScroll = 0;
+                mDragScroller.onExitScrollArea();
+                mLauncher.getDragLayer().onExitScrollArea();
+
+                if (isDragging()) {
+                    // Check the scroll again so that we can requeue the scroller if necessary
+                    checkScrollState(mLastTouch[0], mLastTouch[1]);
+                }
+            }
+        }
+
+        void setDirection(int direction) {
+            mDirection = direction;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java
new file mode 100644
index 0000000..5a1b4cc
--- /dev/null
+++ b/src/com/android/launcher3/DragLayer.java
@@ -0,0 +1,804 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+
+/**
+ * A ViewGroup that coordinates dragging across its descendants
+ */
+public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChangeListener {
+    private DragController mDragController;
+    private int[] mTmpXY = new int[2];
+
+    private int mXDown, mYDown;
+    private Launcher mLauncher;
+
+    // Variables relating to resizing widgets
+    private final ArrayList<AppWidgetResizeFrame> mResizeFrames =
+            new ArrayList<AppWidgetResizeFrame>();
+    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 boolean mHoverPointClosesFolder = false;
+    private Rect mHitRect = new Rect();
+    private int mWorkspaceIndex = -1;
+    private int mQsbIndex = -1;
+    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;
+
+    /**
+     * Used to create a new DragLayer from XML.
+     *
+     * @param context The application's context.
+     * @param attrs The attributes set containing the Workspace's customization values.
+     */
+    public DragLayer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Disable multitouch across the workspace/all apps/customize tray
+        setMotionEventSplittingEnabled(false);
+        setChildrenDrawingOrderEnabled(true);
+        setOnHierarchyChangeListener(this);
+
+        mLeftHoverDrawable = getResources().getDrawable(R.drawable.page_hover_left_holo);
+        mRightHoverDrawable = getResources().getDrawable(R.drawable.page_hover_right_holo);
+    }
+
+    public void setup(Launcher launcher, DragController controller) {
+        mLauncher = launcher;
+        mDragController = controller;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
+    }
+
+    private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
+        getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
+        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
+        getDescendantRectRelativeToSelf(folder, mHitRect);
+        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
+        Rect hitRect = new Rect();
+        int x = (int) ev.getX();
+        int y = (int) ev.getY();
+
+        for (AppWidgetResizeFrame child: mResizeFrames) {
+            child.getHitRect(hitRect);
+            if (hitRect.contains(x, y)) {
+                if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
+                    mCurrentResizeFrame = child;
+                    mXDown = x;
+                    mYDown = y;
+                    requestDisallowInterceptTouchEvent(true);
+                    return true;
+                }
+            }
+        }
+
+        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
+        if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) {
+            if (currentFolder.isEditingName()) {
+                if (!isEventOverFolderTextRegion(currentFolder, ev)) {
+                    currentFolder.dismissEditingName();
+                    return true;
+                }
+            }
+
+            getDescendantRectRelativeToSelf(currentFolder, hitRect);
+            if (!isEventOverFolder(currentFolder, ev)) {
+                mLauncher.closeFolder();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            if (handleTouchDown(ev, true)) {
+                return true;
+            }
+        }
+        clearAllResizeFrames();
+        return mDragController.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onInterceptHoverEvent(MotionEvent ev) {
+        if (mLauncher == null || mLauncher.getWorkspace() == null) {
+            return false;
+        }
+        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
+        if (currentFolder == null) {
+            return false;
+        } else {
+                AccessibilityManager accessibilityManager = (AccessibilityManager)
+                        getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+            if (accessibilityManager.isTouchExplorationEnabled()) {
+                final int action = ev.getAction();
+                boolean isOverFolder;
+                switch (action) {
+                    case MotionEvent.ACTION_HOVER_ENTER:
+                        isOverFolder = isEventOverFolder(currentFolder, ev);
+                        if (!isOverFolder) {
+                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            mHoverPointClosesFolder = true;
+                            return true;
+                        } else if (isOverFolder) {
+                            mHoverPointClosesFolder = false;
+                        } else {
+                            return true;
+                        }
+                    case MotionEvent.ACTION_HOVER_MOVE:
+                        isOverFolder = isEventOverFolder(currentFolder, ev);
+                        if (!isOverFolder && !mHoverPointClosesFolder) {
+                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            mHoverPointClosesFolder = true;
+                            return true;
+                        } else if (isOverFolder) {
+                            mHoverPointClosesFolder = false;
+                        } else {
+                            return true;
+                        }
+                }
+            }
+        }
+        return false;
+    }
+
+    private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) {
+        AccessibilityManager accessibilityManager = (AccessibilityManager)
+                getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+        if (accessibilityManager.isEnabled()) {
+            int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close;
+            AccessibilityEvent event = AccessibilityEvent.obtain(
+                    AccessibilityEvent.TYPE_VIEW_FOCUSED);
+            onInitializeAccessibilityEvent(event);
+            event.getText().add(getContext().getString(stringId));
+            accessibilityManager.sendAccessibilityEvent(event);
+        }
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
+        if (currentFolder != null) {
+            if (child == currentFolder) {
+                return super.onRequestSendAccessibilityEvent(child, event);
+            }
+            // Skip propagating onRequestSendAccessibilityEvent all for other children
+            // when a folder is open
+            return false;
+        }
+        return super.onRequestSendAccessibilityEvent(child, event);
+    }
+
+    @Override
+    public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
+        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
+        if (currentFolder != null) {
+            // Only add the folder as a child for accessibility when it is open
+            childrenForAccessibility.add(currentFolder);
+        } else {
+            super.addChildrenForAccessibility(childrenForAccessibility);
+        }
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent ev) {
+        // If we've received this, we've already done the necessary handling
+        // in onInterceptHoverEvent. Return true to consume the event.
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean handled = false;
+        int action = ev.getAction();
+
+        int x = (int) ev.getX();
+        int y = (int) ev.getY();
+
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                if (handleTouchDown(ev, false)) {
+                    return true;
+                }
+            }
+        }
+
+        if (mCurrentResizeFrame != null) {
+            handled = true;
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_UP:
+                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
+                    mCurrentResizeFrame.onTouchUp();
+                    mCurrentResizeFrame = null;
+            }
+        }
+        if (handled) return true;
+        return mDragController.onTouchEvent(ev);
+    }
+
+    /**
+     * Determine the rect of the descendant in this DragLayer's coordinates
+     *
+     * @param descendant The descendant whose coordinates we want to find.
+     * @param r The rect into which to place the results.
+     * @return The factor by which this descendant is scaled relative to this DragLayer.
+     */
+    public float getDescendantRectRelativeToSelf(View descendant, Rect r) {
+        mTmpXY[0] = 0;
+        mTmpXY[1] = 0;
+        float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY);
+        r.set(mTmpXY[0], mTmpXY[1],
+                mTmpXY[0] + descendant.getWidth(), mTmpXY[1] + descendant.getHeight());
+        return scale;
+    }
+
+    public float getLocationInDragLayer(View child, int[] loc) {
+        loc[0] = 0;
+        loc[1] = 0;
+        return getDescendantCoordRelativeToSelf(child, loc);
+    }
+
+    /**
+     * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's
+     * coordinates.
+     *
+     * @param descendant The descendant to which the passed coordinate is relative.
+     * @param coord The coordinate that we want mapped.
+     * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
+     *         this scale factor is assumed to be equal in X and Y, and so if at any point this
+     *         assumption fails, we will need to return a pair of scale factors.
+     */
+    public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
+        float scale = 1.0f;
+        float[] pt = {coord[0], coord[1]};
+        descendant.getMatrix().mapPoints(pt);
+        scale *= descendant.getScaleX();
+        pt[0] += descendant.getLeft();
+        pt[1] += descendant.getTop();
+        ViewParent viewParent = descendant.getParent();
+        while (viewParent instanceof View && viewParent != this) {
+            final View view = (View)viewParent;
+            view.getMatrix().mapPoints(pt);
+            scale *= view.getScaleX();
+            pt[0] += view.getLeft() - view.getScrollX();
+            pt[1] += view.getTop() - view.getScrollY();
+            viewParent = view.getParent();
+        }
+        coord[0] = (int) Math.round(pt[0]);
+        coord[1] = (int) Math.round(pt[1]);
+        return scale;
+    }
+
+    public void getViewRectRelativeToSelf(View v, Rect r) {
+        int[] loc = new int[2];
+        getLocationInWindow(loc);
+        int x = loc[0];
+        int y = loc[1];
+
+        v.getLocationInWindow(loc);
+        int vX = loc[0];
+        int vY = loc[1];
+
+        int left = vX - x;
+        int top = vY - y;
+        r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        return mDragController.dispatchUnhandledMove(focused, direction);
+    }
+
+    public static class LayoutParams extends FrameLayout.LayoutParams {
+        public int x, y;
+        public boolean customPosition = false;
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public void setWidth(int width) {
+            this.width = width;
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public void setHeight(int height) {
+            this.height = height;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setY(int y) {
+            this.y = y;
+        }
+
+        public int getY() {
+            return y;
+        }
+    }
+
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams();
+            if (flp instanceof LayoutParams) {
+                final LayoutParams lp = (LayoutParams) flp;
+                if (lp.customPosition) {
+                    child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
+                }
+            }
+        }
+    }
+
+    public void clearAllResizeFrames() {
+        if (mResizeFrames.size() > 0) {
+            for (AppWidgetResizeFrame frame: mResizeFrames) {
+                frame.commitResize();
+                removeView(frame);
+            }
+            mResizeFrames.clear();
+        }
+    }
+
+    public boolean hasResizeFrames() {
+        return mResizeFrames.size() > 0;
+    }
+
+    public boolean isWidgetBeingResized() {
+        return mCurrentResizeFrame != null;
+    }
+
+    public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget,
+            CellLayout cellLayout) {
+        AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(),
+                widget, cellLayout, this);
+
+        LayoutParams lp = new LayoutParams(-1, -1);
+        lp.customPosition = true;
+
+        addView(resizeFrame, lp);
+        mResizeFrames.add(resizeFrame);
+
+        resizeFrame.snapToWidget(false);
+    }
+
+    public void animateViewIntoPosition(DragView dragView, final View child) {
+        animateViewIntoPosition(dragView, child, null);
+    }
+
+    public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
+            float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
+            int duration) {
+        Rect r = new Rect();
+        getViewRectRelativeToSelf(dragView, r);
+        final int fromX = r.left;
+        final int fromY = r.top;
+
+        animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
+                onFinishRunnable, animationEndStyle, duration, null);
+    }
+
+    public void animateViewIntoPosition(DragView dragView, final View child,
+            final Runnable onFinishAnimationRunnable) {
+        animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, null);
+    }
+
+    public void animateViewIntoPosition(DragView dragView, final View child, int duration,
+            final Runnable onFinishAnimationRunnable, View anchorView) {
+        ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
+        CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
+        parentChildren.measureChild(child);
+
+        Rect r = new Rect();
+        getViewRectRelativeToSelf(dragView, r);
+
+        int coord[] = new int[2];
+        float childScale = child.getScaleX();
+        coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2);
+        coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2);
+
+        // Since the child hasn't necessarily been laid out, we force the lp to be updated with
+        // the correct coordinates (above) and use these to determine the final location
+        float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
+        // We need to account for the scale of the child itself, as the above only accounts for
+        // for the scale in parents.
+        scale *= childScale;
+        int toX = coord[0];
+        int toY = coord[1];
+        if (child instanceof TextView) {
+            TextView tv = (TextView) child;
+
+            // The child may be scaled (always about the center of the view) so to account for it,
+            // we have to offset the position by the scaled size.  Once we do that, we can center
+            // the drag view about the scaled child view.
+            toY += Math.round(scale * tv.getPaddingTop());
+            toY -= dragView.getMeasuredHeight() * (1 - scale) / 2;
+            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
+        } else if (child instanceof FolderIcon) {
+            // Account for holographic blur padding on the drag view
+            toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2;
+            toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
+            // Center in the x coordinate about the target's drawable
+            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
+        } else {
+            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
+            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
+                    - child.getMeasuredWidth()))) / 2;
+        }
+
+        final int fromX = r.left;
+        final int fromY = r.top;
+        child.setVisibility(INVISIBLE);
+        Runnable onCompleteRunnable = new Runnable() {
+            public void run() {
+                child.setVisibility(VISIBLE);
+                if (onFinishAnimationRunnable != null) {
+                    onFinishAnimationRunnable.run();
+                }
+            }
+        };
+        animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, scale, scale,
+                onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
+    }
+
+    public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
+            final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
+            float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
+            int animationEndStyle, int duration, View anchorView) {
+        Rect from = new Rect(fromX, fromY, fromX +
+                view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
+        Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
+        animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
+                null, null, onCompleteRunnable, animationEndStyle, anchorView);
+    }
+
+    /**
+     * This method animates a view at the end of a drag and drop animation.
+     *
+     * @param view The view to be animated. This view is drawn directly into DragLayer, and so
+     *        doesn't need to be a child of DragLayer.
+     * @param from The initial location of the view. Only the left and top parameters are used.
+     * @param to The final location of the view. Only the left and top parameters are used. This
+     *        location doesn't account for scaling, and so should be centered about the desired
+     *        final location (including scaling).
+     * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
+     * @param finalScale The final scale of the view. The view is scaled about its center.
+     * @param duration The duration of the animation.
+     * @param motionInterpolator The interpolator to use for the location of the view.
+     * @param alphaInterpolator The interpolator to use for the alpha of the view.
+     * @param onCompleteRunnable Optional runnable to run on animation completion.
+     * @param fadeOut Whether or not to fade out the view once the animation completes. If true,
+     *        the runnable will execute after the view is faded out.
+     * @param anchorView If not null, this represents the view which the animated view stays
+     *        anchored to in case scrolling is currently taking place. Note: currently this is
+     *        only used for the X dimension for the case of the workspace.
+     */
+    public void animateView(final DragView view, final Rect from, final Rect to,
+            final float finalAlpha, final float initScaleX, final float initScaleY,
+            final float finalScaleX, final float finalScaleY, int duration,
+            final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
+            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 Resources res = getResources();
+        final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
+
+        // If duration < 0, this is a cue to compute the duration based on the distance
+        if (duration < 0) {
+            duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
+            if (dist < maxDist) {
+                duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
+            }
+            duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
+        }
+
+        // Fall back to cubic ease out interpolator for the animation if none is specified
+        TimeInterpolator interpolator = null;
+        if (alphaInterpolator == null || motionInterpolator == null) {
+            interpolator = mCubicEaseOutInterpolator;
+        }
+
+        // Animate the view
+        final float initAlpha = view.getAlpha();
+        final float dropViewScale = view.getScaleX();
+        AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final float percent = (Float) animation.getAnimatedValue();
+                final int width = view.getMeasuredWidth();
+                final int height = view.getMeasuredHeight();
+
+                float alphaPercent = alphaInterpolator == null ? percent :
+                        alphaInterpolator.getInterpolation(percent);
+                float motionPercent = motionInterpolator == null ? percent :
+                        motionInterpolator.getInterpolation(percent);
+
+                float initialScaleX = initScaleX * dropViewScale;
+                float initialScaleY = initScaleY * dropViewScale;
+                float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
+                float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
+                float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
+
+                float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
+                float fromTop = from.top + (initialScaleY - 1f) * height / 2;
+
+                int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
+                int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
+
+                int xPos = x - mDropView.getScrollX() + (mAnchorView != null
+                        ? (mAnchorViewInitialScrollX - mAnchorView.getScrollX()) : 0);
+                int yPos = y - mDropView.getScrollY();
+
+                mDropView.setTranslationX(xPos);
+                mDropView.setTranslationY(yPos);
+                mDropView.setScaleX(scaleX);
+                mDropView.setScaleY(scaleY);
+                mDropView.setAlpha(alpha);
+            }
+        };
+        animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
+                anchorView);
+    }
+
+    public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
+            TimeInterpolator interpolator, final Runnable onCompleteRunnable,
+            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;
+        mDropView.cancelAnimation();
+        mDropView.resetLayoutParams();
+
+        // Set the anchor view if the page is scrolling
+        if (anchorView != null) {
+            mAnchorViewInitialScrollX = anchorView.getScrollX();
+        }
+        mAnchorView = anchorView;
+
+        // Create and start the animation
+        mDropAnim = new ValueAnimator();
+        mDropAnim.setInterpolator(interpolator);
+        mDropAnim.setDuration(duration);
+        mDropAnim.setFloatValues(0f, 1f);
+        mDropAnim.addUpdateListener(updateCb);
+        mDropAnim.addListener(new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator animation) {
+                if (onCompleteRunnable != null) {
+                    onCompleteRunnable.run();
+                }
+                switch (animationEndStyle) {
+                case ANIMATION_END_DISAPPEAR:
+                    clearAnimatedView();
+                    break;
+                case ANIMATION_END_FADE_OUT:
+                    fadeOutDragView();
+                    break;
+                case ANIMATION_END_REMAIN_VISIBLE:
+                    break;
+                }
+            }
+        });
+        mDropAnim.start();
+    }
+
+    public void clearAnimatedView() {
+        if (mDropAnim != null) {
+            mDropAnim.cancel();
+        }
+        if (mDropView != null) {
+            mDragController.onDeferredEndDrag(mDropView);
+        }
+        mDropView = null;
+        invalidate();
+    }
+
+    public View getAnimatedView() {
+        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) {
+        updateChildIndices();
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+        updateChildIndices();
+    }
+
+    private void updateChildIndices() {
+        if (mLauncher != null) {
+            mWorkspaceIndex = indexOfChild(mLauncher.getWorkspace());
+            mQsbIndex = indexOfChild(mLauncher.getSearchBar());
+        }
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        // TODO: We have turned off this custom drawing order because it now effects touch
+        // dispatch order. We need to sort that issue out and then decide how to go about this.
+        if (true || LauncherApplication.isScreenLandscape(getContext()) ||
+                mWorkspaceIndex == -1 || mQsbIndex == -1 ||
+                mLauncher.getWorkspace().isDrawingBackgroundGradient()) {
+            return i;
+        }
+
+        // This ensures that the workspace is drawn above the hotseat and qsb,
+        // except when the workspace is drawing a background gradient, in which
+        // case we want the workspace to stay behind these elements.
+        if (i == mQsbIndex) {
+            return mWorkspaceIndex;
+        } else if (i == mWorkspaceIndex) {
+            return mQsbIndex;
+        } else {
+            return i;
+        }
+    }
+
+    private boolean mInScrollArea;
+    private Drawable mLeftHoverDrawable;
+    private Drawable mRightHoverDrawable;
+
+    void onEnterScrollArea(int direction) {
+        mInScrollArea = true;
+        invalidate();
+    }
+
+    void onExitScrollArea() {
+        mInScrollArea = false;
+        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) {
+        super.dispatchDraw(canvas);
+
+        if (mInScrollArea && !LauncherApplication.isScreenLarge()) {
+            Workspace workspace = mLauncher.getWorkspace();
+            int width = workspace.getWidth();
+            Rect childRect = new Rect();
+            getDescendantRectRelativeToSelf(workspace.getChildAt(0), 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);
+
+            if (leftPage != null && leftPage.getIsDragOverlapping()) {
+                mLeftHoverDrawable.setBounds(0, childRect.top,
+                        mLeftHoverDrawable.getIntrinsicWidth(), childRect.bottom);
+                mLeftHoverDrawable.draw(canvas);
+            } else if (rightPage != null && rightPage.getIsDragOverlapping()) {
+                mRightHoverDrawable.setBounds(width - mRightHoverDrawable.getIntrinsicWidth(),
+                        childRect.top, width, childRect.bottom);
+                mRightHoverDrawable.draw(canvas);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/DragScroller.java b/src/com/android/launcher3/DragScroller.java
new file mode 100644
index 0000000..e261f15
--- /dev/null
+++ b/src/com/android/launcher3/DragScroller.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Handles scrolling while dragging
+ *
+ */
+public interface DragScroller {
+    void scrollLeft();
+    void scrollRight();
+
+    /**
+     * The touch point has entered the scroll area; a scroll is imminent.
+     * This event will only occur while a drag is active.
+     *
+     * @param direction The scroll direction
+     */
+    boolean onEnterScrollArea(int x, int y, int direction);
+
+    /**
+     * The touch point has left the scroll area.
+     * NOTE: This may not be called, if a drop occurs inside the scroll area.
+     */
+    boolean onExitScrollArea();
+}
diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java
new file mode 100644
index 0000000..2ef99ae
--- /dev/null
+++ b/src/com/android/launcher3/DragSource.java
@@ -0,0 +1,45 @@
+/*
+ * 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.view.View;
+
+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
+     */
+    boolean supportsFlingToDelete();
+
+    /**
+     * A callback specifically made back to the source after an item from this source has been flung
+     * to be deleted on a DropTarget.  In such a situation, this method will be called after
+     * onDropCompleted, and more importantly, after the fling animation has completed.
+     */
+    void onFlingToDeleteCompleted();
+
+    /**
+     * A callback made back to the source after an item from this source has been dropped on a
+     * DropTarget.
+     */
+    void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success);
+}
diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java
new file mode 100644
index 0000000..686cf62
--- /dev/null
+++ b/src/com/android/launcher3/DragView.java
@@ -0,0 +1,295 @@
+/*
+ * 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.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.launcher3.R;
+
+public class DragView extends View {
+    private static float sDragAlpha = 1f;
+
+    private Bitmap mBitmap;
+    private Bitmap mCrossFadeBitmap;
+    private Paint mPaint;
+    private int mRegistrationX;
+    private int mRegistrationY;
+
+    private Point mDragVisualizeOffset = null;
+    private Rect mDragRegion = null;
+    private DragLayer mDragLayer = null;
+    private boolean mHasDrawn = false;
+    private float mCrossFadeProgress = 0f;
+
+    ValueAnimator mAnim;
+    private float mOffsetX = 0.0f;
+    private float mOffsetY = 0.0f;
+    private float mInitialScale = 1f;
+
+    /**
+     * Construct the drag view.
+     * <p>
+     * The registration point is the point inside our view that the touch events should
+     * be centered upon.
+     *
+     * @param launcher The Launcher instance
+     * @param bitmap The view that we're dragging around.  We scale it up when we draw it.
+     * @param registrationX The x coordinate of the registration point.
+     * @param registrationY The y coordinate of the registration point.
+     */
+    public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
+            int left, int top, int width, int height, final float initialScale) {
+        super(launcher);
+        mDragLayer = launcher.getDragLayer();
+        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;
+
+        // Set the initial scale to avoid any jumps
+        setScaleX(initialScale);
+        setScaleY(initialScale);
+
+        // Animate the view into the correct position
+        mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f);
+        mAnim.setDuration(150);
+        mAnim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            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);
+
+                mOffsetX += deltaX;
+                mOffsetY += deltaY;
+                setScaleX(initialScale + (value * (scale - initialScale)));
+                setScaleY(initialScale + (value * (scale - initialScale)));
+                if (sDragAlpha != 1f) {
+                    setAlpha(sDragAlpha * value + (1f - value));
+                }
+
+                if (getParent() == null) {
+                    animation.cancel();
+                } else {
+                    setTranslationX(getTranslationX() + deltaX);
+                    setTranslationY(getTranslationY() + deltaY);
+                }
+            }
+        });
+
+        mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height);
+        setDragRegion(new Rect(0, 0, width, height));
+
+        // The point in our scaled bitmap that the touch events are located
+        mRegistrationX = registrationX;
+        mRegistrationY = registrationY;
+
+        // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
+        int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        measure(ms, ms);
+        mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+    }
+
+    public float getOffsetY() {
+        return mOffsetY;
+    }
+
+    public int getDragRegionLeft() {
+        return mDragRegion.left;
+    }
+
+    public int getDragRegionTop() {
+        return mDragRegion.top;
+    }
+
+    public int getDragRegionWidth() {
+        return mDragRegion.width();
+    }
+
+    public int getDragRegionHeight() {
+        return mDragRegion.height();
+    }
+
+    public void setDragVisualizeOffset(Point p) {
+        mDragVisualizeOffset = p;
+    }
+
+    public Point getDragVisualizeOffset() {
+        return mDragVisualizeOffset;
+    }
+
+    public void setDragRegion(Rect r) {
+        mDragRegion = r;
+    }
+
+    public Rect getDragRegion() {
+        return mDragRegion;
+    }
+
+    public float getInitialScale() {
+        return mInitialScale;
+    }
+
+    public void updateInitialScaleToCurrentScale() {
+        mInitialScale = getScaleX();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        @SuppressWarnings("all") // suppress dead code warning
+        final boolean debug = false;
+        if (debug) {
+            Paint p = new Paint();
+            p.setStyle(Paint.Style.FILL);
+            p.setColor(0x66ffffff);
+            canvas.drawRect(0, 0, getWidth(), getHeight(), p);
+        }
+
+        mHasDrawn = true;
+        boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
+        if (crossFade) {
+            int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
+            mPaint.setAlpha(alpha);
+        }
+        canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
+        if (crossFade) {
+            mPaint.setAlpha((int) (255 * mCrossFadeProgress));
+            canvas.save();
+            float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
+            float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
+            canvas.scale(sX, sY);
+            canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
+            canvas.restore();
+        }
+    }
+
+    public void setCrossFadeBitmap(Bitmap crossFadeBitmap) {
+        mCrossFadeBitmap = crossFadeBitmap;
+    }
+
+    public void crossFade(int duration) {
+        ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1f);
+        va.setDuration(duration);
+        va.setInterpolator(new DecelerateInterpolator(1.5f));
+        va.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mCrossFadeProgress = animation.getAnimatedFraction();
+            }
+        });
+        va.start();
+    }
+
+    public void setColor(int color) {
+        if (mPaint == null) {
+            mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+        }
+        if (color != 0) {
+            mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
+        } else {
+            mPaint.setColorFilter(null);
+        }
+        invalidate();
+    }
+
+    public boolean hasDrawn() {
+        return mHasDrawn;
+    }
+
+    @Override
+    public void setAlpha(float alpha) {
+        super.setAlpha(alpha);
+        mPaint.setAlpha((int) (255 * alpha));
+        invalidate();
+    }
+
+    /**
+     * Create a window containing this view and show it.
+     *
+     * @param windowToken obtained from v.getWindowToken() from one of your views
+     * @param touchX the x coordinate the user touched in DragLayer coordinates
+     * @param touchY the y coordinate the user touched in DragLayer coordinates
+     */
+    public void show(int touchX, int touchY) {
+        mDragLayer.addView(this);
+
+        // Start the pick-up animation
+        DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
+        lp.width = mBitmap.getWidth();
+        lp.height = mBitmap.getHeight();
+        lp.customPosition = true;
+        setLayoutParams(lp);
+        setTranslationX(touchX - mRegistrationX);
+        setTranslationY(touchY - mRegistrationY);
+        // Post the animation to skip other expensive work happening on the first frame
+        post(new Runnable() {
+                public void run() {
+                    mAnim.start();
+                }
+            });
+    }
+
+    public void cancelAnimation() {
+        if (mAnim != null && mAnim.isRunning()) {
+            mAnim.cancel();
+        }
+    }
+
+    public void resetLayoutParams() {
+        mOffsetX = mOffsetY = 0;
+        requestLayout();
+    }
+
+    /**
+     * Move the window containing this view.
+     *
+     * @param touchX the x coordinate the user touched in DragLayer coordinates
+     * @param touchY the y coordinate the user touched in DragLayer coordinates
+     */
+    void move(int touchX, int touchY) {
+        setTranslationX(touchX - mRegistrationX + (int) mOffsetX);
+        setTranslationY(touchY - mRegistrationY + (int) mOffsetY);
+    }
+
+    void remove() {
+        if (getParent() != null) {
+            mDragLayer.removeView(DragView.this);
+        }
+    }
+}
+
diff --git a/src/com/android/launcher3/DrawableStateProxyView.java b/src/com/android/launcher3/DrawableStateProxyView.java
new file mode 100644
index 0000000..196e2f2
--- /dev/null
+++ b/src/com/android/launcher3/DrawableStateProxyView.java
@@ -0,0 +1,69 @@
+/*
+ * 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.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.R;
+
+public class DrawableStateProxyView extends LinearLayout {
+
+    private View mView;
+    private int mViewId;
+
+    public DrawableStateProxyView(Context context) {
+        this(context, null);
+    }
+
+    public DrawableStateProxyView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+
+    public DrawableStateProxyView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawableStateProxyView,
+                defStyle, 0);
+        mViewId = a.getResourceId(R.styleable.DrawableStateProxyView_sourceViewId, -1);
+        a.recycle();
+
+        setFocusable(false);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        if (mView == null) {
+            View parent = (View) getParent();
+            mView = parent.findViewById(mViewId);
+        }
+        mView.setPressed(isPressed());
+        mView.setHovered(isHovered());
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
new file mode 100644
index 0000000..3ecb8ff
--- /dev/null
+++ b/src/com/android/launcher3/DropTarget.java
@@ -0,0 +1,184 @@
+/*
+ * 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.graphics.PointF;
+import android.graphics.Rect;
+import android.util.Log;
+
+/**
+ * Interface defining an object that can receive a drag.
+ *
+ */
+public interface DropTarget {
+
+    public static final String TAG = "DropTarget";
+
+    class DragObject {
+        public int x = -1;
+        public int y = -1;
+
+        /** X offset from the upper-left corner of the cell to where we touched.  */
+        public int xOffset = -1;
+
+        /** Y offset from the upper-left corner of the cell to where we touched.  */
+        public int yOffset = -1;
+
+        /** This indicates whether a drag is in final stages, either drop or cancel. It
+         * differentiates onDragExit, since this is called when the drag is ending, above
+         * the current drag target, or when the drag moves off the current drag object.
+         */
+        public boolean dragComplete = false;
+
+        /** The view that moves around while you drag.  */
+        public DragView dragView = null;
+
+        /** The data associated with the object being dragged */
+        public Object dragInfo = null;
+
+        /** Where the drag originated */
+        public DragSource dragSource = null;
+
+        /** Post drag animation runnable */
+        public Runnable postAnimationRunnable = null;
+
+        /** Indicates that the drag operation was cancelled */
+        public boolean cancelled = false;
+
+        /** Defers removing the DragView from the DragLayer until after the drop animation. */
+        public boolean deferDragViewCleanupPostAnimation = true;
+
+        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);
+            }
+        }
+
+        void onDragExit() {
+            dragParity--;
+            if (dragParity != 0) {
+                Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity);
+            }
+        }
+
+        @Override
+        public void onDragStart(DragSource source, Object info, int dragAction) {
+            if (dragParity != 0) {
+                Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity);
+            }
+        }
+
+        @Override
+        public void onDragEnd() {
+            if (dragParity != 0) {
+                Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity);
+            }
+        }
+    }
+
+    /**
+     * Used to temporarily disable certain drop targets
+     *
+     * @return boolean specifying whether this drop target is currently enabled
+     */
+    boolean isDropEnabled();
+
+    /**
+     * 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
+     * @param xOffset Horizontal offset with the object being dragged where the original
+     *          touch happened
+     * @param yOffset Vertical offset with the object being dragged where the original
+     *          touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     * 
+     */
+    void onDrop(DragObject dragObject);
+
+    void onDragEnter(DragObject dragObject);
+
+    void onDragOver(DragObject dragObject);
+
+    void onDragExit(DragObject dragObject);
+
+    /**
+     * Handle an object being dropped as a result of flinging to delete and will be called in place
+     * 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);
+
+    /**
+     * Allows a DropTarget to delegate drag and drop events to another object.
+     *
+     * Most subclasses will should just return null from this method.
+     *
+     * @param source DragSource where the drag started
+     * @param x X coordinate of the drop location
+     * @param y Y coordinate of the drop location
+     * @param xOffset Horizontal offset with the object being dragged where the original
+     *          touch happened
+     * @param yOffset Vertical offset with the object being dragged where the original
+     *          touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     *
+     * @return The DropTarget to delegate to, or null to not delegate to another object.
+     */
+    DropTarget getDropTargetDelegate(DragObject dragObject);
+
+    /**
+     * 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
+     * @param xOffset Horizontal offset with the object being dragged where the
+     *            original touch happened
+     * @param yOffset Vertical offset with the object being dragged where the
+     *            original touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     * @return True if the drop will be accepted, false otherwise.
+     */
+    boolean acceptDrop(DragObject dragObject);
+
+    // These methods are implemented in Views
+    void getHitRect(Rect outRect);
+    void getLocationInDragLayer(int[] loc);
+    int getLeft();
+    int getTop();
+}
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
new file mode 100644
index 0000000..14760c7
--- /dev/null
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -0,0 +1,109 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+class FastBitmapDrawable extends Drawable {
+    private Bitmap mBitmap;
+    private int mAlpha;
+    private int mWidth;
+    private int mHeight;
+    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+
+    FastBitmapDrawable(Bitmap b) {
+	mAlpha = 255;
+        mBitmap = b;
+        if (b != null) {
+            mWidth = mBitmap.getWidth();
+            mHeight = mBitmap.getHeight();
+        } else {
+            mWidth = mHeight = 0;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        final Rect r = getBounds();
+        // Draw the bitmap into the bounding rect
+        canvas.drawBitmap(mBitmap, null, r, mPaint);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+        mPaint.setAlpha(alpha);
+    }
+
+    public void setFilterBitmap(boolean filterBitmap) {
+        mPaint.setFilterBitmap(filterBitmap);
+    }
+
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getMinimumWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getMinimumHeight() {
+        return mHeight;
+    }
+
+    public void setBitmap(Bitmap b) {
+        mBitmap = b;
+        if (b != null) {
+            mWidth = mBitmap.getWidth();
+            mHeight = mBitmap.getHeight();
+        } else {
+            mWidth = mHeight = 0;
+        }
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+}
diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
new file mode 100644
index 0000000..78fdadd
--- /dev/null
+++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
@@ -0,0 +1,141 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+
+/*
+ *  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
+ *  prevent jank at the beginning of the animation
+ */
+public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter
+    implements ValueAnimator.AnimatorUpdateListener {
+    private static final boolean DEBUG = false;
+    private static final int MAX_DELAY = 1000;
+    private static final int IDEAL_FRAME_DURATION = 16;
+    private View mTarget;
+    private long mStartFrame;
+    private long mStartTime = -1;
+    private boolean mHandlingOnAnimationUpdate;
+    private boolean mAdjustedSecondFrameTime;
+
+    private static ViewTreeObserver.OnDrawListener sGlobalDrawListener;
+    private static long sGlobalFrameCounter;
+    private static boolean sVisible;
+
+    public FirstFrameAnimatorHelper(ValueAnimator animator, View target) {
+        mTarget = target;
+        animator.addUpdateListener(this);
+    }
+
+    public FirstFrameAnimatorHelper(ViewPropertyAnimator vpa, View target) {
+        mTarget = target;
+        vpa.setListener(this);
+    }
+
+    // only used for ViewPropertyAnimators
+    public void onAnimationStart(Animator animation) {
+        final ValueAnimator va = (ValueAnimator) animation;
+        va.addUpdateListener(FirstFrameAnimatorHelper.this);
+        onAnimationUpdate(va);
+    }
+
+    public static void setIsVisible(boolean visible) {
+        sVisible = visible;
+    }
+
+    public static void initializeDrawListener(View view) {
+        if (sGlobalDrawListener != null) {
+            view.getViewTreeObserver().removeOnDrawListener(sGlobalDrawListener);
+        }
+        sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() {
+                private long mTime = System.currentTimeMillis();
+                public void onDraw() {
+                    sGlobalFrameCounter++;
+                    if (DEBUG) {
+                        long newTime = System.currentTimeMillis();
+                        Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime));
+                        mTime = newTime;
+                    }
+                }
+            };
+        view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener);
+        sVisible = true;
+    }
+
+    public void onAnimationUpdate(final ValueAnimator animation) {
+        final long currentTime = System.currentTimeMillis();
+        if (mStartTime == -1) {
+            mStartFrame = sGlobalFrameCounter;
+            mStartTime = currentTime;
+        }
+
+        if (!mHandlingOnAnimationUpdate &&
+            sVisible &&
+            // If the current play time exceeds the duration, the animation
+            // will get finished, even if we call setCurrentPlayTime -- therefore
+            // don't adjust the animation in that case
+            animation.getCurrentPlayTime() < animation.getDuration()) {
+            mHandlingOnAnimationUpdate = true;
+            long frameNum = sGlobalFrameCounter - mStartFrame;
+            // If we haven't drawn our first frame, reset the time to t = 0
+            // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we
+            // are no longer in the foreground and no frames are being rendered ever)
+            if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY) {
+                // The first frame on animations doesn't always trigger an invalidate...
+                // force an invalidate here to make sure the animation continues to advance
+                mTarget.getRootView().invalidate();
+                animation.setCurrentPlayTime(0);
+
+            // For the second frame, if the first frame took more than 16ms,
+            // adjust the start time and pretend it took only 16ms anyway. This
+            // prevents a large jump in the animation due to an expensive first frame
+            } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY &&
+                       !mAdjustedSecondFrameTime &&
+                       currentTime > mStartTime + IDEAL_FRAME_DURATION) {
+                animation.setCurrentPlayTime(IDEAL_FRAME_DURATION);
+                mAdjustedSecondFrameTime = true;
+            } else {
+                if (frameNum > 1) {
+                    mTarget.post(new Runnable() {
+                            public void run() {
+                                animation.removeUpdateListener(FirstFrameAnimatorHelper.this);
+                            }
+                        });
+                }
+                if (DEBUG) print(animation);
+            }
+            mHandlingOnAnimationUpdate = false;
+        } else {
+            if (DEBUG) print(animation);
+        }
+    }
+
+    public void print(ValueAnimator animation) {
+        float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration();
+        Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter +
+              "(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " +
+              mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation);
+    }
+}
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
new file mode 100644
index 0000000..94c5820
--- /dev/null
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -0,0 +1,898 @@
+/*
+ * 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.res.Configuration;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * A keyboard listener we set on all the workspace icons.
+ */
+class IconKeyEventListener implements View.OnKeyListener {
+    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 {
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        final Configuration configuration = v.getResources().getConfiguration();
+        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation);
+    }
+}
+
+/**
+ * A keyboard listener we set on the last tab button in AppsCustomize to jump to then
+ * market icon and vice versa.
+ */
+class AppsCustomizeTabKeyEventListener implements View.OnKeyListener {
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        return FocusHelper.handleAppsCustomizeTabKeyEvent(v, keyCode, event);
+    }
+}
+
+public class FocusHelper {
+    /**
+     * Private helper to get the parent TabHost in the view hiearchy.
+     */
+    private static TabHost findTabHostParent(View v) {
+        ViewParent p = v.getParent();
+        while (p != null && !(p instanceof TabHost)) {
+            p = p.getParent();
+        }
+        return (TabHost) p;
+    }
+
+    /**
+     * Handles key events in a AppsCustomize tab between the last tab view and the shop button.
+     */
+    static boolean handleAppsCustomizeTabKeyEvent(View v, int keyCode, KeyEvent e) {
+        final TabHost tabHost = findTabHostParent(v);
+        final ViewGroup contents = tabHost.getTabContentView();
+        final View shop = tabHost.findViewById(R.id.market_button);
+
+        final int action = e.getAction();
+        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
+        boolean wasHandled = false;
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (handleKeyEvent) {
+                    // Select the shop button if we aren't on it
+                    if (v != shop) {
+                        shop.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (handleKeyEvent) {
+                    // Select the content view (down is handled by the tab key handler otherwise)
+                    if (v == shop) {
+                        contents.requestFocus();
+                        wasHandled = true;
+                    }
+                }
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * 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 PagedViewCellLayout) {
+            // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
+            page = (ViewGroup) page.getChildAt(0);
+        }
+        return page;
+    }
+
+    /**
+     * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets.
+     */
+    static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode,
+            KeyEvent e) {
+
+        final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent();
+        final PagedView container = (PagedView) parent.getParent();
+        final TabHost tabHost = findTabHostParent(container);
+        final TabWidget tabs = tabHost.getTabWidget();
+        final int widgetIndex = parent.indexOfChild(w);
+        final int widgetCount = parent.getChildCount();
+        final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parent));
+        final int pageCount = container.getChildCount();
+        final int cellCountX = parent.getCellCountX();
+        final int cellCountY = parent.getCellCountY();
+        final int x = widgetIndex % cellCountX;
+        final int y = widgetIndex / cellCountX;
+
+        final int action = e.getAction();
+        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
+        ViewGroup newParent = null;
+        // Now that we load items in the bg asynchronously, we can't just focus
+        // child siblings willy-nilly
+        View child = null;
+        boolean wasHandled = false;
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (handleKeyEvent) {
+                    // Select the previous widget or the last widget on the previous page
+                    if (widgetIndex > 0) {
+                        parent.getChildAt(widgetIndex - 1).requestFocus();
+                    } else {
+                        if (pageIndex > 0) {
+                            newParent = getAppsCustomizePage(container, pageIndex - 1);
+                            if (newParent != null) {
+                                child = newParent.getChildAt(newParent.getChildCount() - 1);
+                                if (child != null) child.requestFocus();
+                            }
+                        }
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (handleKeyEvent) {
+                    // Select the next widget or the first widget on the next page
+                    if (widgetIndex < (widgetCount - 1)) {
+                        parent.getChildAt(widgetIndex + 1).requestFocus();
+                    } else {
+                        if (pageIndex < (pageCount - 1)) {
+                            newParent = getAppsCustomizePage(container, pageIndex + 1);
+                            if (newParent != null) {
+                                child = newParent.getChildAt(0);
+                                if (child != null) child.requestFocus();
+                            }
+                        }
+                    }
+                }
+                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 newWidgetIndex = ((y - 1) * cellCountX) + x;
+                        child = parent.getChildAt(newWidgetIndex);
+                        if (child != null) child.requestFocus();
+                    } else {
+                        tabs.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (handleKeyEvent) {
+                    // Select the closest icon in the previous row, otherwise do nothing
+                    if (y < (cellCountY - 1)) {
+                        int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x);
+                        child = parent.getChildAt(newWidgetIndex);
+                        if (child != null) child.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (handleKeyEvent) {
+                    // Simulate a click on the widget
+                    View.OnClickListener clickListener = (View.OnClickListener) container;
+                    clickListener.onClick(w);
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_PAGE_UP:
+                if (handleKeyEvent) {
+                    // Select the first item on the previous page, or the first item on this page
+                    // if there is no previous page
+                    if (pageIndex > 0) {
+                        newParent = getAppsCustomizePage(container, pageIndex - 1);
+                        if (newParent != null) {
+                            child = newParent.getChildAt(0);
+                        }
+                    } else {
+                        child = parent.getChildAt(0);
+                    }
+                    if (child != null) child.requestFocus();
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_PAGE_DOWN:
+                if (handleKeyEvent) {
+                    // Select the first item on the next page, or the last item on this page
+                    // if there is no next page
+                    if (pageIndex < (pageCount - 1)) {
+                        newParent = getAppsCustomizePage(container, pageIndex + 1);
+                        if (newParent != null) {
+                            child = newParent.getChildAt(0);
+                        }
+                    } else {
+                        child = parent.getChildAt(widgetCount - 1);
+                    }
+                    if (child != null) child.requestFocus();
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_MOVE_HOME:
+                if (handleKeyEvent) {
+                    // Select the first item on this page
+                    child = parent.getChildAt(0);
+                    if (child != null) child.requestFocus();
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_MOVE_END:
+                if (handleKeyEvent) {
+                    // Select the last item on this page
+                    parent.getChildAt(widgetCount - 1).requestFocus();
+                }
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * Handles key events in a PageViewCellLayout containing PagedViewIcons.
+     */
+    static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
+        ViewGroup parentLayout;
+        ViewGroup itemContainer;
+        int countX;
+        int countY;
+        if (v.getParent() instanceof PagedViewCellLayoutChildren) {
+            itemContainer = (ViewGroup) v.getParent();
+            parentLayout = (ViewGroup) itemContainer.getParent();
+            countX = ((PagedViewCellLayout) parentLayout).getCellCountX();
+            countY = ((PagedViewCellLayout) parentLayout).getCellCountY();
+        } else {
+            itemContainer = parentLayout = (ViewGroup) v.getParent();
+            countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
+            countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
+        }
+
+        // Note we have an extra parent because of the
+        // PagedViewCellLayout/PagedViewCellLayoutChildren relationship
+        final PagedView container = (PagedView) parentLayout.getParent();
+        final TabHost tabHost = findTabHostParent(container);
+        final TabWidget tabs = tabHost.getTabWidget();
+        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();
+                    } 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();
+                            }
+                        }
+                    }
+                }
+                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();
+                    } 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();
+                            }
+                        }
+                    }
+                }
+                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();
+                    } else {
+                        tabs.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (handleKeyEvent) {
+                    // Select the closest icon in the previous row, otherwise do nothing
+                    if (y < (countY - 1)) {
+                        int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x);
+                        itemContainer.getChildAt(newiconIndex).requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (handleKeyEvent) {
+                    // Simulate a click on the icon
+                    View.OnClickListener clickListener = (View.OnClickListener) container;
+                    clickListener.onClick(v);
+                }
+                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();
+                        }
+                    } else {
+                        itemContainer.getChildAt(0).requestFocus();
+                    }
+                }
+                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();
+                        }
+                    } else {
+                        itemContainer.getChildAt(itemCount - 1).requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_MOVE_HOME:
+                if (handleKeyEvent) {
+                    // Select the first icon on this page
+                    itemContainer.getChildAt(0).requestFocus();
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_MOVE_END:
+                if (handleKeyEvent) {
+                    // Select the last icon on this page
+                    itemContainer.getChildAt(itemCount - 1).requestFocus();
+                }
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * Handles key events in the tab widget.
+     */
+    static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) {
+        if (!LauncherApplication.isScreenLarge()) return false;
+
+        final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent();
+        final TabHost tabHost = findTabHostParent(parent);
+        final ViewGroup contents = tabHost.getTabContentView();
+        final int tabCount = parent.getTabCount();
+        final int tabIndex = parent.getChildTabIndex(v);
+
+        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 tab
+                    if (tabIndex > 0) {
+                        parent.getChildTabViewAt(tabIndex - 1).requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (handleKeyEvent) {
+                    // Select the next tab, or if the last tab has a focus right id, select that
+                    if (tabIndex < (tabCount - 1)) {
+                        parent.getChildTabViewAt(tabIndex + 1).requestFocus();
+                    } else {
+                        if (v.getNextFocusRightId() != View.NO_ID) {
+                            tabHost.findViewById(v.getNextFocusRightId()).requestFocus();
+                        }
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                // Do nothing
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (handleKeyEvent) {
+                    // Select the content view
+                    contents.requestFocus();
+                }
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * Handles key events in the workspace hotseat (bottom of the screen).
+     */
+    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) {
+        final ViewGroup parent = (ViewGroup) v.getParent();
+        final ViewGroup launcher = (ViewGroup) parent.getParent();
+        final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace);
+        final int buttonIndex = parent.indexOfChild(v);
+        final int buttonCount = parent.getChildCount();
+        final int pageIndex = workspace.getCurrentPage();
+
+        // 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) {
+                    // Select the previous button, otherwise snap to the previous page
+                    if (buttonIndex > 0) {
+                        parent.getChildAt(buttonIndex - 1).requestFocus();
+                    } else {
+                        workspace.snapToPage(pageIndex - 1);
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (handleKeyEvent) {
+                    // Select the next button, otherwise snap to the next page
+                    if (buttonIndex < (buttonCount - 1)) {
+                        parent.getChildAt(buttonIndex + 1).requestFocus();
+                    } else {
+                        workspace.snapToPage(pageIndex + 1);
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                if (handleKeyEvent) {
+                    // Select the first bubble text view in the current page of the workspace
+                    final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex);
+                    final ShortcutAndWidgetContainer children = layout.getShortcutsAndWidgets();
+                    final View newIcon = getIconInDirection(layout, children, -1, 1);
+                    if (newIcon != null) {
+                        newIcon.requestFocus();
+                    } else {
+                        workspace.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                // Do nothing
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * Private helper method to get the CellLayoutChildren given a CellLayout index.
+     */
+    private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
+            ViewGroup container, int i) {
+        ViewGroup parent = (ViewGroup) container.getChildAt(i);
+        return (ShortcutAndWidgetContainer) parent.getChildAt(0);
+    }
+
+    /**
+     * 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));
+        }
+        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;
+        }
+        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;
+                }
+            }
+            if (closestIndex > -1) {
+                return views.get(closestIndex);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Handles key events in a Workspace containing.
+     */
+    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
+        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.qsb_bar);
+        final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat);
+        int pageIndex = workspace.indexOfChild(layout);
+        int pageCount = workspace.getChildCount();
+
+        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();
+                    } 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);
+                            }
+                        }
+                    }
+                }
+                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();
+                    } 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);
+                            }
+                        }
+                    }
+                }
+                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();
+                    }
+                }
+                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();
+                        wasHandled = true;
+                    } else if (hotseat != null) {
+                        hotseat.requestFocus();
+                    }
+                }
+                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);
+                        }
+                    } else {
+                        View newIcon = getIconInDirection(layout, parent, -1, 1);
+                        if (newIcon != null) {
+                            newIcon.requestFocus();
+                        }
+                    }
+                }
+                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 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);
+                        }
+                    } else {
+                        View newIcon = getIconInDirection(layout, parent,
+                                parent.getChildCount(), -1);
+                        if (newIcon != null) {
+                            newIcon.requestFocus();
+                        }
+                    }
+                }
+                wasHandled = true;
+                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();
+                    }
+                }
+                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();
+                    }
+                }
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+
+    /**
+     * Handles key events for items in a Folder.
+     */
+    static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
+        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
+        final CellLayout layout = (CellLayout) parent.getParent();
+        final Folder folder = (Folder) layout.getParent();
+        View title = folder.mFolderName;
+
+        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
+                    View newIcon = getIconInDirection(layout, parent, v, -1);
+                    if (newIcon != null) {
+                        newIcon.requestFocus();
+                    }
+                }
+                wasHandled = true;
+                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();
+                    }
+                }
+                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();
+                    }
+                }
+                wasHandled = true;
+                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();
+                    }
+                }
+                wasHandled = true;
+                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();
+                    }
+                }
+                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();
+                    }
+                }
+                wasHandled = true;
+                break;
+            default: break;
+        }
+        return wasHandled;
+    }
+}
diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java
new file mode 100644
index 0000000..08fc311
--- /dev/null
+++ b/src/com/android/launcher3/FocusOnlyTabWidget.java
@@ -0,0 +1,86 @@
+/*
+ * 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
new file mode 100644
index 0000000..a7b5c5c
--- /dev/null
+++ b/src/com/android/launcher3/Folder.java
@@ -0,0 +1,1114 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.Spannable;
+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.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.FolderInfo.FolderListener;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Represents a set of icons chosen by the user or generated by the system.
+ */
+public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
+        View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
+        View.OnFocusChangeListener {
+    private static final String TAG = "Launcher.Folder";
+
+    protected DragController mDragController;
+    protected Launcher mLauncher;
+    protected FolderInfo mInfo;
+
+    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 int mExpandDuration;
+    protected CellLayout mContent;
+    private final LayoutInflater mInflater;
+    private final IconCache mIconCache;
+    private int mState = STATE_NONE;
+    private static final int REORDER_ANIMATION_DURATION = 230;
+    private static final int ON_EXIT_CLOSE_DELAY = 800;
+    private boolean mRearrangeOnClose = false;
+    private FolderIcon mFolderIcon;
+    private int mMaxCountX;
+    private int mMaxCountY;
+    private int mMaxNumItems;
+    private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
+    private Drawable mIconDrawable;
+    boolean mItemsInvalidated = false;
+    private ShortcutInfo mCurrentDragInfo;
+    private View mCurrentDragView;
+    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;
+
+    private boolean mIsEditingName = false;
+    private InputMethodManager mInputMethodManager;
+
+    private static String sDefaultFolderName;
+    private static String sHintText;
+
+    private boolean mDestroyed;
+
+    /**
+     * Used to inflate the Workspace from XML.
+     *
+     * @param context The application's context.
+     * @param attrs The attribtues set containing the Workspace's customization values.
+     */
+    public Folder(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setAlwaysDrawnWithCacheEnabled(false);
+        mInflater = LayoutInflater.from(context);
+        mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache();
+
+        Resources res = getResources();
+        mMaxCountX = res.getInteger(R.integer.folder_max_count_x);
+        mMaxCountY = res.getInteger(R.integer.folder_max_count_y);
+        mMaxNumItems = res.getInteger(R.integer.folder_max_num_items);
+        if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) {
+            mMaxCountX = LauncherModel.getCellCountX();
+            mMaxCountY = LauncherModel.getCellCountY();
+            mMaxNumItems = mMaxCountX * mMaxCountY;
+        }
+
+        mInputMethodManager = (InputMethodManager)
+                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+
+        mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration);
+
+        if (sDefaultFolderName == null) {
+            sDefaultFolderName = res.getString(R.string.folder_name);
+        }
+        if (sHintText == null) {
+            sHintText = res.getString(R.string.folder_hint_text);
+        }
+        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).
+        setFocusableInTouchMode(true);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mContent = (CellLayout) findViewById(R.id.folder_content);
+        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);
+    }
+
+    private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return false;
+        }
+
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            return false;
+        }
+
+        public void onDestroyActionMode(ActionMode mode) {
+        }
+
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            return false;
+        }
+    };
+
+    public void onClick(View v) {
+        Object tag = v.getTag();
+        if (tag instanceof ShortcutInfo) {
+            // refactor this code from Folder
+            ShortcutInfo item = (ShortcutInfo) tag;
+            int[] pos = new int[2];
+            v.getLocationOnScreen(pos);
+            item.intent.setSourceBounds(new Rect(pos[0], pos[1],
+                    pos[0] + v.getWidth(), pos[1] + v.getHeight()));
+
+            mLauncher.startActivitySafely(v, item.intent, item);
+        }
+    }
+
+    public boolean onLongClick(View v) {
+        // Return if global dragging is not enabled
+        if (!mLauncher.isDraggingEnabled()) return true;
+
+        Object tag = v.getTag();
+        if (tag instanceof ShortcutInfo) {
+            ShortcutInfo item = (ShortcutInfo) tag;
+            if (!v.isInTouchMode()) {
+                return false;
+            }
+
+            mLauncher.dismissFolderCling(null);
+
+            mLauncher.getWorkspace().onDragStartedWithItem(v);
+            mLauncher.getWorkspace().beginDragShared(v, this);
+            mIconDrawable = ((TextView) v).getCompoundDrawables()[1];
+
+            mCurrentDragInfo = item;
+            mEmptyCell[0] = item.cellX;
+            mEmptyCell[1] = item.cellY;
+            mCurrentDragView = v;
+
+            mContent.removeView(mCurrentDragView);
+            mInfo.remove(mCurrentDragInfo);
+            mDragInProgress = true;
+            mItemAddedBackToSelfViaIcon = false;
+        }
+        return true;
+    }
+
+    public boolean isEditingName() {
+        return mIsEditingName;
+    }
+
+    public void startEditingFolderName() {
+        mFolderName.setHint("");
+        mIsEditingName = true;
+    }
+
+    public void dismissEditingName() {
+        mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+        doneEditingFolderName(true);
+    }
+
+    public void doneEditingFolderName(boolean commit) {
+        mFolderName.setHint(sHintText);
+        // Convert to a string here to ensure that no other state associated with the text field
+        // gets saved.
+        String newTitle = mFolderName.getText().toString();
+        mInfo.setTitle(newTitle);
+        LauncherModel.updateItemInDatabase(mLauncher, mInfo);
+
+        if (commit) {
+            sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                    String.format(getContext().getString(R.string.folder_renamed), newTitle));
+        }
+        // In order to clear the focus from the text field, we set the focus on ourself. This
+        // ensures that every time the field is clicked, focus is gained, giving reliable behavior.
+        requestFocus();
+
+        Selection.setSelection((Spannable) mFolderName.getText(), 0, 0);
+        mIsEditingName = false;
+    }
+
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (actionId == EditorInfo.IME_ACTION_DONE) {
+            dismissEditingName();
+            return true;
+        }
+        return false;
+    }
+
+    public View getEditTextRegion() {
+        return mFolderName;
+    }
+
+    public Drawable getDragDrawable() {
+        return mIconDrawable;
+    }
+
+    /**
+     * We need to handle touch events to prevent them from falling through to the workspace below.
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return true;
+    }
+
+    public void setDragController(DragController dragController) {
+        mDragController = dragController;
+    }
+
+    void setFolderIcon(FolderIcon icon) {
+        mFolderIcon = icon;
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        // When the folder gets focus, we don't want to announce the list of items.
+        return true;
+    }
+
+    /**
+     * @return the FolderInfo object associated with this folder
+     */
+    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)) {
+                overflow.add(child);
+            } else {
+                count++;
+            }
+        }
+
+        // We rearrange the items in case there are any empty gaps
+        setupContentForNumItems(count);
+
+        // 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
+        // number of items.
+        for (ShortcutInfo item: overflow) {
+            mInfo.remove(item);
+            LauncherModel.deleteItemFromDatabase(mLauncher, item);
+        }
+
+        mItemsInvalidated = true;
+        updateTextViewFocus();
+        mInfo.addListener(this);
+
+        if (!sDefaultFolderName.contentEquals(mInfo.title)) {
+            mFolderName.setText(mInfo.title);
+        } else {
+            mFolderName.setText("");
+        }
+        updateItemLocationsInDatabase();
+    }
+
+    /**
+     * Creates a new UserFolder, inflated from R.layout.user_folder.
+     *
+     * @param context The application's context.
+     *
+     * @return A new UserFolder.
+     */
+    static Folder fromXml(Context context) {
+        return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null);
+    }
+
+    /**
+     * This method is intended to make the UserFolder to be visually identical in size and position
+     * to its associated FolderIcon. This allows for a seamless transition into the expanded state.
+     */
+    private void positionAndSizeAsIcon() {
+        if (!(getParent() instanceof DragLayer)) return;
+        setScaleX(0.8f);
+        setScaleY(0.8f);
+        setAlpha(0f);
+        mState = STATE_SMALL;
+    }
+
+    public void animateOpen() {
+        positionAndSizeAsIcon();
+
+        if (!(getParent() instanceof DragLayer)) return;
+        centerAboutIcon();
+        PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
+        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
+        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
+        final ObjectAnimator oa =
+            LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
+
+        oa.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                        String.format(getContext().getString(R.string.folder_opened),
+                        mContent.getCountX(), mContent.getCountY()));
+                mState = STATE_ANIMATING;
+            }
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mState = STATE_OPEN;
+                setLayerType(LAYER_TYPE_NONE, null);
+                Cling cling = mLauncher.showFirstRunFoldersCling();
+                if (cling != null) {
+                    cling.bringToFront();
+                }
+                setFocusOnFirstChild();
+            }
+        });
+        oa.setDuration(mExpandDuration);
+        setLayerType(LAYER_TYPE_HARDWARE, null);
+        oa.start();
+    }
+
+    private void sendCustomAccessibilityEvent(int type, String text) {
+        AccessibilityManager accessibilityManager = (AccessibilityManager)
+                getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+        if (accessibilityManager.isEnabled()) {
+            AccessibilityEvent event = AccessibilityEvent.obtain(type);
+            onInitializeAccessibilityEvent(event);
+            event.getText().add(text);
+            accessibilityManager.sendAccessibilityEvent(event);
+        }
+    }
+
+    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);
+        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f);
+        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f);
+        final ObjectAnimator oa =
+                LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
+
+        oa.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onCloseComplete();
+                setLayerType(LAYER_TYPE_NONE, null);
+                mState = STATE_SMALL;
+            }
+            @Override
+            public void onAnimationStart(Animator animation) {
+                sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                        getContext().getString(R.string.folder_closed));
+                mState = STATE_ANIMATING;
+            }
+        });
+        oa.setDuration(mExpandDuration);
+        setLayerType(LAYER_TYPE_HARDWARE, null);
+        oa.start();
+    }
+
+    void notifyDataSetChanged() {
+        // recreate all the children if the data set changes under us. We may want to do this more
+        // intelligently (ie just removing the views that should no longer exist)
+        mContent.removeAllViewsInLayout();
+        bind(mInfo);
+    }
+
+    public boolean acceptDrop(DragObject d) {
+        final ItemInfo item = (ItemInfo) d.dragInfo;
+        final int itemType = item.itemType;
+        return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
+                    itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
+                    !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 boolean createAndAddShortcut(ShortcutInfo item) {
+        final TextView textView =
+            (TextView) mInflater.inflate(R.layout.application, this, false);
+        textView.setCompoundDrawablesWithIntrinsicBounds(null,
+                new FastBitmapDrawable(item.getIcon(mIconCache)), null, null);
+        textView.setText(item.title);
+        textView.setTag(item);
+
+        textView.setOnClickListener(this);
+        textView.setOnLongClickListener(this);
+
+        // 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 false;
+            }
+        }
+
+        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 true;
+    }
+
+    public void onDragEnter(DragObject d) {
+        mPreviousTargetCell[0] = -1;
+        mPreviousTargetCell[1] = -1;
+        mOnExitAlarm.cancelAlarm();
+    }
+
+    OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
+        public void onAlarm(Alarm alarm) {
+            realTimeReorder(mEmptyCell, mTargetCell);
+        }
+    };
+
+    boolean readingOrderGreaterThan(int[] v1, int[] v2) {
+        if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    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;
+                    }
+                }
+            }
+        }
+    }
+
+    public boolean isLayoutRtl() {
+        return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+    }
+
+    public void onDragOver(DragObject d) {
+        float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
+        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 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(150);
+            mPreviousTargetCell[0] = mTargetCell[0];
+            mPreviousTargetCell[1] = mTargetCell[1];
+        }
+    }
+
+    // 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;
+        }
+
+        // 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;
+    }
+
+    OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
+        public void onAlarm(Alarm alarm) {
+            completeDragExit();
+        }
+    };
+
+    public void completeDragExit() {
+        mLauncher.closeFolder();
+        mCurrentDragInfo = null;
+        mCurrentDragView = null;
+        mSuppressOnAdd = false;
+        mRearrangeOnClose = true;
+    }
+
+    public void onDragExit(DragObject d) {
+        // 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) {
+            mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
+            mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
+        }
+        mReorderAlarm.cancelAlarm();
+    }
+
+    public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (success) {
+            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) {
+                replaceFolderWithFinalItem();
+            }
+        } else {
+            setupContentForNumItems(getItemCount());
+            // The drag failed, we need to return the item to the folder
+            mFolderIcon.onDrop(d);
+        }
+
+        if (target != this) {
+            if (mOnExitAlarm.alarmPending()) {
+                mOnExitAlarm.cancelAlarm();
+                if (!success) {
+                    mSuppressFolderDeletion = true;
+                }
+                completeDragExit();
+            }
+        }
+
+        mDeleteFolderOnDropCompleted = false;
+        mDragInProgress = false;
+        mItemAddedBackToSelfViaIcon = false;
+        mCurrentDragInfo = null;
+        mCurrentDragView = null;
+        mSuppressOnAdd = false;
+
+        // 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.
+        updateItemLocationsInDatabase();
+    }
+
+    @Override
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
+    @Override
+    public void onFlingToDeleteCompleted() {
+        // 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);
+        }
+    }
+
+    public void notifyDrop() {
+        if (mDragInProgress) {
+            mItemAddedBackToSelfViaIcon = true;
+        }
+    }
+
+    public boolean isDropEnabled() {
+        return true;
+    }
+
+    public DropTarget getDropTargetDelegate(DragObject d) {
+        return null;
+    }
+
+    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;
+    }
+
+    private void centerAboutIcon() {
+        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+
+        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
+        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
+                + mFolderNameHeight;
+        DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
+
+        float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect);
+
+        int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2);
+        int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2);
+        int centeredLeft = centerX - width / 2;
+        int centeredTop = centerY - height / 2;
+
+        int currentPage = mLauncher.getWorkspace().getCurrentPage();
+        // 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 the folder doesn't fit within the bounds, center it about the desired bounds
+        if (width >= bounds.width()) {
+            left = bounds.left + (bounds.width() - width) / 2;
+        }
+        if (height >= bounds.height()) {
+            top = bounds.top + (bounds.height() - height) / 2;
+        }
+
+        int folderPivotX = width / 2 + (centeredLeft - left);
+        int folderPivotY = height / 2 + (centeredTop - top);
+        setPivotX(folderPivotX);
+        setPivotY(folderPivotY);
+        mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
+                (1.0f * folderPivotX / width));
+        mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
+                (1.0f * folderPivotY / height));
+
+        lp.width = width;
+        lp.height = height;
+        lp.x = left;
+        lp.y = top;
+    }
+
+    float getPivotXForIconAnimation() {
+        return mFolderIconPivotX;
+    }
+    float getPivotYForIconAnimation() {
+        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();
+    }
+
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
+        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
+                + mFolderNameHeight;
+
+        int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(),
+                MeasureSpec.EXACTLY);
+        int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(),
+                MeasureSpec.EXACTLY);
+        mContent.measure(contentWidthSpec, contentHeightSpec);
+
+        mFolderName.measure(contentWidthSpec,
+                MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY));
+        setMeasuredDimension(width, height);
+    }
+
+    private void arrangeChildren(ArrayList<View> list) {
+        int[] vacant = new int[2];
+        if (list == null) {
+            list = getItemsInReadingOrder();
+        }
+        mContent.removeAllViews();
+
+        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);
+        }
+        mItemsInvalidated = true;
+    }
+
+    public int getItemCount() {
+        return mContent.getShortcutsAndWidgets().getChildCount();
+    }
+
+    public View getItemAt(int index) {
+        return mContent.getShortcutsAndWidgets().getChildAt(index);
+    }
+
+    private void onCloseComplete() {
+        DragLayer parent = (DragLayer) getParent();
+        if (parent != null) {
+            parent.removeView(this);
+        }
+        mDragController.removeDropTarget((DropTarget) this);
+        clearFocus();
+        mFolderIcon.requestFocus();
+
+        if (mRearrangeOnClose) {
+            setupContentForNumItems(getItemCount());
+            mRearrangeOnClose = false;
+        }
+        if (getItemCount() <= 1) {
+            if (!mDragInProgress && !mSuppressFolderDeletion) {
+                replaceFolderWithFinalItem();
+            } else if (mDragInProgress) {
+                mDeleteFolderOnDropCompleted = true;
+            }
+        }
+        mSuppressFolderDeletion = false;
+    }
+
+    private void replaceFolderWithFinalItem() {
+        // Add the last remaining child to the workspace in place of the folder
+        Runnable onCompleteRunnable = new Runnable() {
+            @Override
+            public void run() {
+                CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screen);
+
+               View child = null;
+                // 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);
+                    LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
+                            mInfo.screen, mInfo.cellX, mInfo.cellY);
+                }
+                if (getItemCount() <= 1) {
+                    // Remove the folder
+                    LauncherModel.deleteItemFromDatabase(mLauncher, mInfo);
+                    cellLayout.removeView(mFolderIcon);
+                    if (mFolderIcon instanceof DropTarget) {
+                        mDragController.removeDropTarget((DropTarget) mFolderIcon);
+                    }
+                    mLauncher.removeFolder(mInfo);
+                }
+                // We add the child after removing the folder to prevent both from existing at
+                // the same time in the CellLayout.
+                if (child != null) {
+                    mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screen,
+                            mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
+                }
+            }
+        };
+        View finalChild = getItemAt(0);
+        if (finalChild != null) {
+            mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
+        }
+        mDestroyed = true;
+    }
+
+    boolean isDestroyed() {
+        return mDestroyed;
+    }
+
+    // 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);
+        if (lastChild != null) {
+            mFolderName.setNextFocusDownId(lastChild.getId());
+            mFolderName.setNextFocusRightId(lastChild.getId());
+            mFolderName.setNextFocusLeftId(lastChild.getId());
+            mFolderName.setNextFocusUpId(lastChild.getId());
+        }
+    }
+
+    public void onDrop(DragObject d) {
+        ShortcutInfo item;
+        if (d.dragInfo instanceof ApplicationInfo) {
+            // Came from all apps -- make a copy
+            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
+            item.spanX = 1;
+            item.spanY = 1;
+        } else {
+            item = (ShortcutInfo) d.dragInfo;
+        }
+        // Dragged from self onto self, currently this is the only path possible, however
+        // we keep this as a distinct code path.
+        if (item == mCurrentDragInfo) {
+            ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag();
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams();
+            si.cellX = lp.cellX = mEmptyCell[0];
+            si.cellX = lp.cellY = mEmptyCell[1];
+            mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true);
+            if (d.dragView.hasDrawn()) {
+                mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView);
+            } else {
+                d.deferDragViewCleanupPostAnimation = false;
+                mCurrentDragView.setVisibility(VISIBLE);
+            }
+            mItemsInvalidated = true;
+            setupContentDimensions(getItemCount());
+            mSuppressOnAdd = true;
+        }
+        mInfo.add(item);
+    }
+
+    // This is used so the item doesn't immediately appear in the folder when added. In one case
+    // we need to create the illusion that the item isn't added back to the folder yet, to
+    // to correspond to the animation of the icon back into the folder. This is
+    public void hideItem(ShortcutInfo info) {
+        View v = getViewForInfo(info);
+        v.setVisibility(INVISIBLE);
+    }
+    public void showItem(ShortcutInfo info) {
+        View v = getViewForInfo(info);
+        v.setVisibility(VISIBLE);
+    }
+
+    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);
+        LauncherModel.addOrMoveItemInDatabase(
+                mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
+    }
+
+    public void onRemove(ShortcutInfo item) {
+        mItemsInvalidated = true;
+        // If this item is being dragged from this open folder, we have already handled
+        // 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);
+        if (mState == STATE_ANIMATING) {
+            mRearrangeOnClose = true;
+        } else {
+            setupContentForNumItems(getItemCount());
+        }
+        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;
+                }
+            }
+        }
+        return null;
+    }
+
+    public void onItemsChanged() {
+        updateTextViewFocus();
+    }
+
+    public void onTitleChanged(CharSequence title) {
+    }
+
+    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);
+                    }
+                }
+            }
+            mItemsInvalidated = false;
+        }
+        return mItemsInReadingOrder;
+    }
+
+    public void getLocationInDragLayer(int[] loc) {
+        mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
+    }
+
+    public void onFocusChange(View v, boolean hasFocus) {
+        if (v == mFolderName && hasFocus) {
+            startEditingFolderName();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/FolderEditText.java b/src/com/android/launcher3/FolderEditText.java
new file mode 100644
index 0000000..c311008
--- /dev/null
+++ b/src/com/android/launcher3/FolderEditText.java
@@ -0,0 +1,36 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+public class FolderEditText extends EditText {
+
+    private Folder mFolder;
+
+    public FolderEditText(Context context) {
+        super(context);
+    }
+
+    public FolderEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FolderEditText(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setFolder(Folder folder) {
+        mFolder = folder;
+    }
+
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        // Catch the back button on the soft keyboard so that we can just close the activity
+        if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK) {
+            mFolder.doneEditingFolderName(true);
+        }
+        return super.onKeyPreIme(keyCode, event);
+    }
+}
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
new file mode 100644
index 0000000..e11d7d1
--- /dev/null
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -0,0 +1,667 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+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.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.FolderInfo.FolderListener;
+
+import java.util.ArrayList;
+
+/**
+ * An icon that can appear on in the workspace representing an {@link UserFolder}.
+ */
+public class FolderIcon extends LinearLayout implements FolderListener {
+    private Launcher mLauncher;
+    private Folder mFolder;
+    private FolderInfo mInfo;
+    private static boolean sStaticValuesDirty = true;
+
+    private CheckLongPressHelper mLongPressHelper;
+
+    // The number of icons to display in the
+    private 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;
+    private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
+
+    // The degree to which the inner ring grows when accepting drop
+    private static final float INNER_RING_GROWTH_FACTOR = 0.15f;
+
+    // The degree to which the outer ring is scaled in its natural state
+    private static final float OUTER_RING_GROWTH_FACTOR = 0.3f;
+
+    // The amount of vertical spread between items in the stack [0...1]
+    private static final float PERSPECTIVE_SHIFT_FACTOR = 0.24f;
+
+    // The degree to which the item in the back of the stack is scaled [0...1]
+    // (0 means it's not scaled at all, 1 means it's scaled to nothing)
+    private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f;
+
+    public static Drawable sSharedFolderLeaveBehind = null;
+
+    private ImageView mPreviewBackground;
+    private BubbleTextView mFolderName;
+
+    FolderRingAnimator mFolderRingAnimator = null;
+
+    // These variables are all associated with the drawing of the preview; they are stored
+    // as member variables for shared usage and to avoid computation on each frame
+    private int mIntrinsicIconSize;
+    private float mBaselineIconScale;
+    private int mBaselineIconSize;
+    private int mAvailableSpaceInPreview;
+    private int mTotalWidth = -1;
+    private int mPreviewOffsetX;
+    private int mPreviewOffsetY;
+    private float mMaxPerspectiveShift;
+    boolean mAnimating = false;
+
+    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>();
+
+    public FolderIcon(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public FolderIcon(Context context) {
+        super(context);
+        init();
+    }
+
+    private void init() {
+        mLongPressHelper = new CheckLongPressHelper(this);
+    }
+
+    public boolean isDropEnabled() {
+        final ViewGroup cellLayoutChildren = (ViewGroup) getParent();
+        final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent();
+        final Workspace workspace = (Workspace) cellLayout.getParent();
+        return !workspace.isSmall();
+    }
+
+    static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+            FolderInfo folderInfo, IconCache iconCache) {
+        @SuppressWarnings("all") // suppress dead code warning
+        final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
+        if (error) {
+            throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
+                    "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
+                    "is dependent on this");
+        }
+
+        FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
+
+        icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
+        icon.mFolderName.setText(folderInfo.title);
+        icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background);
+
+        icon.setTag(folderInfo);
+        icon.setOnClickListener(launcher);
+        icon.mInfo = folderInfo;
+        icon.mLauncher = launcher;
+        icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format),
+                folderInfo.title));
+        Folder folder = Folder.fromXml(launcher);
+        folder.setDragController(launcher.getDragController());
+        folder.setFolderIcon(icon);
+        folder.bind(folderInfo);
+        icon.mFolder = folder;
+
+        icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon);
+        folderInfo.addListener(icon);
+
+        return icon;
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        sStaticValuesDirty = true;
+        return super.onSaveInstanceState();
+    }
+
+    public static class FolderRingAnimator {
+        public int mCellX;
+        public int mCellY;
+        private CellLayout mCellLayout;
+        public float mOuterRingSize;
+        public float mInnerRingSize;
+        public FolderIcon mFolderIcon = null;
+        public Drawable mOuterRingDrawable = null;
+        public Drawable mInnerRingDrawable = null;
+        public static Drawable sSharedOuterRingDrawable = null;
+        public static Drawable sSharedInnerRingDrawable = null;
+        public static int sPreviewSize = -1;
+        public static int sPreviewPadding = -1;
+
+        private ValueAnimator mAcceptAnimator;
+        private ValueAnimator mNeutralAnimator;
+
+        public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) {
+            mFolderIcon = folderIcon;
+            Resources res = launcher.getResources();
+            mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo);
+            mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo);
+
+            // We need to reload the static values when configuration changes in case they are
+            // different in another configuration
+            if (sStaticValuesDirty) {
+                sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size);
+                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_holo);
+                sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest);
+                sStaticValuesDirty = false;
+            }
+        }
+
+        public void animateToAcceptState() {
+            if (mNeutralAnimator != null) {
+                mNeutralAnimator.cancel();
+            }
+            mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
+            mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+
+            final int previewSize = sPreviewSize;
+            mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float percent = (Float) animation.getAnimatedValue();
+                    mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize;
+                    mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize;
+                    if (mCellLayout != null) {
+                        mCellLayout.invalidate();
+                    }
+                }
+            });
+            mAcceptAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    if (mFolderIcon != null) {
+                        mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE);
+                    }
+                }
+            });
+            mAcceptAnimator.start();
+        }
+
+        public void animateToNaturalState() {
+            if (mAcceptAnimator != null) {
+                mAcceptAnimator.cancel();
+            }
+            mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
+            mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+
+            final int previewSize = sPreviewSize;
+            mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float percent = (Float) animation.getAnimatedValue();
+                    mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize;
+                    mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize;
+                    if (mCellLayout != null) {
+                        mCellLayout.invalidate();
+                    }
+                }
+            });
+            mNeutralAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (mCellLayout != null) {
+                        mCellLayout.hideFolderAccept(FolderRingAnimator.this);
+                    }
+                    if (mFolderIcon != null) {
+                        mFolderIcon.mPreviewBackground.setVisibility(VISIBLE);
+                    }
+                }
+            });
+            mNeutralAnimator.start();
+        }
+
+        // Location is expressed in window coordinates
+        public void getCell(int[] loc) {
+            loc[0] = mCellX;
+            loc[1] = mCellY;
+        }
+
+        // Location is expressed in window coordinates
+        public void setCell(int x, int y) {
+            mCellX = x;
+            mCellY = y;
+        }
+
+        public void setCellLayout(CellLayout layout) {
+            mCellLayout = layout;
+        }
+
+        public float getOuterRingSize() {
+            return mOuterRingSize;
+        }
+
+        public float getInnerRingSize() {
+            return mInnerRingSize;
+        }
+    }
+
+    Folder getFolder() {
+        return mFolder;
+    }
+
+    FolderInfo getFolderInfo() {
+        return mInfo;
+    }
+
+    private boolean willAcceptItem(ItemInfo item) {
+        final int itemType = item.itemType;
+        return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
+                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
+                !mFolder.isFull() && item != mInfo && !mInfo.opened);
+    }
+
+    public boolean acceptDrop(Object dragInfo) {
+        final ItemInfo item = (ItemInfo) dragInfo;
+        return !mFolder.isDestroyed() && willAcceptItem(item);
+    }
+
+    public void addItem(ShortcutInfo item) {
+        mInfo.add(item);
+    }
+
+    public void onDragEnter(Object dragInfo) {
+        if (mFolder.isDestroyed() || !willAcceptItem((ItemInfo) dragInfo)) return;
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
+        CellLayout layout = (CellLayout) getParent().getParent();
+        mFolderRingAnimator.setCell(lp.cellX, lp.cellY);
+        mFolderRingAnimator.setCellLayout(layout);
+        mFolderRingAnimator.animateToAcceptState();
+        layout.showFolderAccept(mFolderRingAnimator);
+    }
+
+    public void onDragOver(Object dragInfo) {
+    }
+
+    public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
+            final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
+            float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
+
+        // These correspond two the drawable and view that the icon was dropped _onto_
+        Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
+        computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
+                destView.getMeasuredWidth());
+
+        // This will animate the first item from it's position as an icon into its
+        // position as the first item in the preview
+        animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
+        addItem(destInfo);
+
+        // This will animate the dragView (srcView) into the new folder
+        onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null);
+    }
+
+    public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
+        Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1];
+        computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), 
+                finalView.getMeasuredWidth());
+
+        // This will animate the first item from it's position as an icon into its
+        // position as the first item in the preview
+        animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
+                onCompleteRunnable);
+    }
+
+    public void onDragExit(Object dragInfo) {
+        onDragExit();
+    }
+
+    public void onDragExit() {
+        mFolderRingAnimator.animateToNaturalState();
+    }
+
+    private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
+            float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable,
+            DragObject d) {
+        item.cellX = -1;
+        item.cellY = -1;
+
+        // Typically, the animateView corresponds to the DragView; however, if this is being done
+        // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
+        // will not have a view to animate
+        if (animateView != null) {
+            DragLayer dragLayer = mLauncher.getDragLayer();
+            Rect from = new Rect();
+            dragLayer.getViewRectRelativeToSelf(animateView, from);
+            Rect to = finalRect;
+            if (to == null) {
+                to = new Rect();
+                Workspace workspace = mLauncher.getWorkspace();
+                // Set cellLayout and this to it's final state to compute final animation locations
+                workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
+                float scaleX = getScaleX();
+                float scaleY = getScaleY();
+                setScaleX(1.0f);
+                setScaleY(1.0f);
+                scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
+                // Finished computing final animation locations, restore current state
+                setScaleX(scaleX);
+                setScaleY(scaleY);
+                workspace.resetTransitionTransform((CellLayout) getParent().getParent());
+            }
+
+            int[] center = new int[2];
+            float scale = getLocalCenterForIndex(index, center);
+            center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
+            center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
+
+            to.offset(center[0] - animateView.getMeasuredWidth() / 2,
+                    center[1] - animateView.getMeasuredHeight() / 2);
+
+            float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f;
+
+            float finalScale = scale * scaleRelativeToDragLayer;
+            dragLayer.animateView(animateView, from, to, finalAlpha,
+                    1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
+                    new DecelerateInterpolator(2), new AccelerateInterpolator(2),
+                    postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
+            addItem(item);
+            mHiddenItems.add(item);
+            mFolder.hideItem(item);
+            postDelayed(new Runnable() {
+                public void run() {
+                    mHiddenItems.remove(item);
+                    mFolder.showItem(item);
+                    invalidate();
+                }
+            }, DROP_IN_ANIMATION_DURATION);
+        } else {
+            addItem(item);
+        }
+    }
+
+    public void onDrop(DragObject d) {
+        ShortcutInfo item;
+        if (d.dragInfo instanceof ApplicationInfo) {
+            // Came from all apps -- make a copy
+            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
+        } else {
+            item = (ShortcutInfo) d.dragInfo;
+        }
+        mFolder.notifyDrop();
+        onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d);
+    }
+
+    public DropTarget getDropTargetDelegate(DragObject d) {
+        return null;
+    }
+
+    private void computePreviewDrawingParams(int drawableSize, int totalSize) {
+        if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) {
+            mIntrinsicIconSize = drawableSize;
+            mTotalWidth = totalSize;
+
+            final int previewSize = FolderRingAnimator.sPreviewSize;
+            final int previewPadding = FolderRingAnimator.sPreviewPadding;
+
+            mAvailableSpaceInPreview = (previewSize - 2 * previewPadding);
+            // cos(45) = 0.707  + ~= 0.1) = 0.8f
+            int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f));
+
+            int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR));
+            mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight);
+
+            mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale);
+            mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR;
+
+            mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2;
+            mPreviewOffsetY = previewPadding;
+        }
+    }
+
+    private void computePreviewDrawingParams(Drawable d) {
+        computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
+    }
+
+    class PreviewItemDrawingParams {
+        PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) {
+            this.transX = transX;
+            this.transY = transY;
+            this.scale = scale;
+            this.overlayAlpha = overlayAlpha;
+        }
+        float transX;
+        float transY;
+        float scale;
+        int overlayAlpha;
+        Drawable drawable;
+    }
+
+    private float getLocalCenterForIndex(int index, int[] center) {
+        mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams);
+
+        mParams.transX += mPreviewOffsetX;
+        mParams.transY += mPreviewOffsetY;
+        float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2;
+        float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2;
+
+        center[0] = (int) Math.round(offsetX);
+        center[1] = (int) Math.round(offsetY);
+        return mParams.scale;
+    }
+
+    private PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
+            PreviewItemDrawingParams params) {
+        index = NUM_ITEMS_IN_PREVIEW - index - 1;
+        float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1);
+        float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r));
+
+        float offset = (1 - r) * mMaxPerspectiveShift;
+        float scaledSize = scale * mBaselineIconSize;
+        float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize;
+
+        // We want to imagine our coordinates from the bottom left, growing up and to the
+        // right. This is natural for the x-axis, but for the y-axis, we have to invert things.
+        float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection);
+        float transX = offset + scaleOffsetCorrection;
+        float totalScale = mBaselineIconScale * scale;
+        final int overlayAlpha = (int) (80 * (1 - r));
+
+        if (params == null) {
+            params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
+        } else {
+            params.transX = transX;
+            params.transY = transY;
+            params.scale = totalScale;
+            params.overlayAlpha = overlayAlpha;
+        }
+        return params;
+    }
+
+    private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
+        canvas.save();
+        canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY);
+        canvas.scale(params.scale, params.scale);
+        Drawable d = params.drawable;
+
+        if (d != null) {
+            d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
+            d.setFilterBitmap(true);
+            d.setColorFilter(Color.argb(params.overlayAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP);
+            d.draw(canvas);
+            d.clearColorFilter();
+            d.setFilterBitmap(false);
+        }
+        canvas.restore();
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mFolder == null) return;
+        if (mFolder.getItemCount() == 0 && !mAnimating) return;
+
+        ArrayList<View> items = mFolder.getItemsInReadingOrder();
+        Drawable d;
+        TextView v;
+
+        // Update our drawing parameters if necessary
+        if (mAnimating) {
+            computePreviewDrawingParams(mAnimParams.drawable);
+        } else {
+            v = (TextView) items.get(0);
+            d = v.getCompoundDrawables()[1];
+            computePreviewDrawingParams(d);
+        }
+
+        int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW);
+        if (!mAnimating) {
+            for (int i = nItemsInPreview - 1; i >= 0; i--) {
+                v = (TextView) items.get(i);
+                if (!mHiddenItems.contains(v.getTag())) {
+                    d = v.getCompoundDrawables()[1];
+                    mParams = computePreviewItemDrawingParams(i, mParams);
+                    mParams.drawable = d;
+                    drawPreviewItem(canvas, mParams);
+                }
+            }
+        } else {
+            drawPreviewItem(canvas, mAnimParams);
+        }
+    }
+
+    private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
+            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;
+        mAnimParams.drawable = d;
+
+        ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f);
+        va.addUpdateListener(new AnimatorUpdateListener(){
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float progress = (Float) animation.getAnimatedValue();
+                if (reverse) {
+                    progress = 1 - progress;
+                    mPreviewBackground.setAlpha(progress);
+                }
+
+                mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0);
+                mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0);
+                mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0);
+                invalidate();
+            }
+        });
+        va.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mAnimating = true;
+            }
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimating = false;
+                if (onCompleteRunnable != null) {
+                    onCompleteRunnable.run();
+                }
+            }
+        });
+        va.setDuration(duration);
+        va.start();
+    }
+
+    public void setTextVisible(boolean visible) {
+        if (visible) {
+            mFolderName.setVisibility(VISIBLE);
+        } else {
+            mFolderName.setVisibility(INVISIBLE);
+        }
+    }
+
+    public boolean getTextVisible() {
+        return mFolderName.getVisibility() == VISIBLE;
+    }
+
+    public void onItemsChanged() {
+        invalidate();
+        requestLayout();
+    }
+
+    public void onAdd(ShortcutInfo item) {
+        invalidate();
+        requestLayout();
+    }
+
+    public void onRemove(ShortcutInfo item) {
+        invalidate();
+        requestLayout();
+    }
+
+    public void onTitleChanged(CharSequence title) {
+        mFolderName.setText(title.toString());
+        setContentDescription(String.format(getContext().getString(R.string.folder_name_format),
+                title));
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Call the superclass onTouchEvent first, because sometimes it changes the state to
+        // isPressed() on an ACTION_UP
+        boolean result = super.onTouchEvent(event);
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mLongPressHelper.postCheckForLongPress();
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                mLongPressHelper.cancelLongPress();
+                break;
+        }
+        return result;
+    }
+
+    @Override
+    public void cancelLongPress() {
+        super.cancelLongPress();
+
+        mLongPressHelper.cancelLongPress();
+    }
+}
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
new file mode 100644
index 0000000..6d45e59
--- /dev/null
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -0,0 +1,111 @@
+/*
+ * 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 java.util.ArrayList;
+
+import android.content.ContentValues;
+
+/**
+ * Represents a folder containing shortcuts or apps.
+ */
+class FolderInfo extends ItemInfo {
+
+    /**
+     * Whether this folder has been opened
+     */
+    boolean opened;
+
+    /**
+     * The apps and shortcuts
+     */
+    ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>();
+
+    ArrayList<FolderListener> listeners = new ArrayList<FolderListener>();
+
+    FolderInfo() {
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
+    }
+
+    /**
+     * Add an app or shortcut
+     *
+     * @param item
+     */
+    public void add(ShortcutInfo item) {
+        contents.add(item);
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).onAdd(item);
+        }
+        itemsChanged();
+    }
+
+    /**
+     * Remove an app or shortcut. Does not change the DB.
+     *
+     * @param item
+     */
+    public void remove(ShortcutInfo item) {
+        contents.remove(item);
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).onRemove(item);
+        }
+        itemsChanged();
+    }
+
+    public void setTitle(CharSequence title) {
+        this.title = title;
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).onTitleChanged(title);
+        }
+    }
+
+    @Override
+    void onAddToDatabase(ContentValues values) {
+        super.onAddToDatabase(values);
+        values.put(LauncherSettings.Favorites.TITLE, title.toString());
+    }
+
+    void addListener(FolderListener listener) {
+        listeners.add(listener);
+    }
+
+    void removeListener(FolderListener listener) {
+        if (listeners.contains(listener)) {
+            listeners.remove(listener);
+        }
+    }
+
+    void itemsChanged() {
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).onItemsChanged();
+        }
+    }
+
+    @Override
+    void unbind() {
+        super.unbind();
+        listeners.clear();
+    }
+
+    interface FolderListener {
+        public void onAdd(ShortcutInfo item);
+        public void onRemove(ShortcutInfo item);
+        public void onTitleChanged(CharSequence title);
+        public void onItemsChanged();
+    }
+}
diff --git a/src/com/android/launcher3/HandleView.java b/src/com/android/launcher3/HandleView.java
new file mode 100644
index 0000000..6cb51da
--- /dev/null
+++ b/src/com/android/launcher3/HandleView.java
@@ -0,0 +1,76 @@
+/*
+ * 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.TypedArray;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.launcher3.R;
+
+public class HandleView extends ImageView {
+    private static final int ORIENTATION_HORIZONTAL = 1;
+
+    private Launcher mLauncher;
+    private int mOrientation = ORIENTATION_HORIZONTAL;
+
+    public HandleView(Context context) {
+        super(context);
+    }
+
+    public HandleView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public HandleView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HandleView, defStyle, 0);
+        mOrientation = a.getInt(R.styleable.HandleView_direction, ORIENTATION_HORIZONTAL);
+        a.recycle();
+
+        setContentDescription(context.getString(R.string.all_apps_button_label));
+    }
+
+    @Override
+    public View focusSearch(int direction) {
+        View newFocus = super.focusSearch(direction);
+        if (newFocus == null && !mLauncher.isAllAppsVisible()) {
+            final Workspace workspace = mLauncher.getWorkspace();
+            workspace.dispatchUnhandledMove(null, direction);
+            return (mOrientation == ORIENTATION_HORIZONTAL && direction == FOCUS_DOWN) ?
+                    this : workspace;
+        }
+        return newFocus;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN && mLauncher.isAllAppsVisible()) {
+            return false;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    void setLauncher(Launcher launcher) {
+        mLauncher = launcher;
+    }
+}
diff --git a/src/com/android/launcher3/HideFromAccessibilityHelper.java b/src/com/android/launcher3/HideFromAccessibilityHelper.java
new file mode 100644
index 0000000..33adf77
--- /dev/null
+++ b/src/com/android/launcher3/HideFromAccessibilityHelper.java
@@ -0,0 +1,113 @@
+/*
+ * 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.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.OnHierarchyChangeListener;
+
+import java.util.HashMap;
+
+public class HideFromAccessibilityHelper implements OnHierarchyChangeListener {
+    private HashMap<View, Integer> mPreviousValues;
+    boolean mHide;
+    boolean mOnlyAllApps;
+
+    public HideFromAccessibilityHelper() {
+        mPreviousValues = new HashMap<View, Integer>();
+        mHide = false;
+    }
+
+    public void setImportantForAccessibilityToNo(View v, boolean onlyAllApps) {
+        mOnlyAllApps = onlyAllApps;
+        setImportantForAccessibilityToNoHelper(v);
+        mHide = true;
+    }
+
+    private void setImportantForAccessibilityToNoHelper(View v) {
+        mPreviousValues.put(v, v.getImportantForAccessibility());
+        v.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+
+        // Call method on children recursively
+        if (v instanceof ViewGroup) {
+            ViewGroup vg = (ViewGroup) v;
+            vg.setOnHierarchyChangeListener(this);
+            for (int i = 0; i < vg.getChildCount(); i++) {
+                View child = vg.getChildAt(i);
+
+                if (includeView(child)) {
+                    setImportantForAccessibilityToNoHelper(child);
+                }
+            }
+        }
+    }
+
+    public void restoreImportantForAccessibility(View v) {
+        if (mHide) {
+            restoreImportantForAccessibilityHelper(v);
+        }
+        mHide = false;
+    }
+
+    private void restoreImportantForAccessibilityHelper(View v) {
+        v.setImportantForAccessibility(mPreviousValues.get(v));
+        mPreviousValues.remove(v);
+
+        // Call method on children recursively
+        if (v instanceof ViewGroup) {
+            ViewGroup vg = (ViewGroup) v;
+
+            // We assume if a class implements OnHierarchyChangeListener, it listens
+            // to changes to any of its children (happens to be the case in Launcher)
+            if (vg instanceof OnHierarchyChangeListener) {
+                vg.setOnHierarchyChangeListener((OnHierarchyChangeListener) vg);
+            } else {
+                vg.setOnHierarchyChangeListener(null);
+            }
+            for (int i = 0; i < vg.getChildCount(); i++) {
+                View child = vg.getChildAt(i);
+                if (includeView(child)) {
+                    restoreImportantForAccessibilityHelper(child);
+                }
+            }
+        }
+    }
+
+    public void onChildViewAdded(View parent, View child) {
+        if (mHide && includeView(child)) {
+            setImportantForAccessibilityToNoHelper(child);
+        }
+    }
+
+    public void onChildViewRemoved(View parent, View child) {
+        if (mHide && includeView(child)) {
+            restoreImportantForAccessibilityHelper(child);
+        }
+    }
+
+    private boolean includeView(View v) {
+        return !hasAncestorOfType(v, Cling.class) &&
+                (!mOnlyAllApps || hasAncestorOfType(v, AppsCustomizeTabHost.class));
+    }
+
+    private boolean hasAncestorOfType(View v, Class c) {
+        return v != null &&
+                (v.getClass().equals(c) ||
+                 (v.getParent() instanceof ViewGroup &&
+                  hasAncestorOfType((ViewGroup) v.getParent(), c)));
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/HolographicImageView.java b/src/com/android/launcher3/HolographicImageView.java
new file mode 100644
index 0000000..0ad82a7
--- /dev/null
+++ b/src/com/android/launcher3/HolographicImageView.java
@@ -0,0 +1,54 @@
+/*
+ * 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 HolographicImageView extends ImageView {
+
+    private final HolographicViewHelper mHolographicHelper;
+
+    public HolographicImageView(Context context) {
+        this(context, null);
+    }
+
+    public HolographicImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public HolographicImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mHolographicHelper = new HolographicViewHelper(context);
+    }
+
+    void invalidatePressedFocusedStates() {
+        mHolographicHelper.invalidatePressedFocusedStates(this);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // One time call to generate the pressed/focused state -- must be called after
+        // measure/layout
+        mHolographicHelper.generatePressedFocusedStates(this);
+    }
+}
diff --git a/src/com/android/launcher3/HolographicLinearLayout.java b/src/com/android/launcher3/HolographicLinearLayout.java
new file mode 100644
index 0000000..73d4c3a
--- /dev/null
+++ b/src/com/android/launcher3/HolographicLinearLayout.java
@@ -0,0 +1,85 @@
+/*
+ * 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.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.R;
+
+public class HolographicLinearLayout extends LinearLayout {
+
+    private final HolographicViewHelper mHolographicHelper;
+    private ImageView mImageView;
+    private int mImageViewId;
+
+    public HolographicLinearLayout(Context context) {
+        this(context, null);
+    }
+
+    public HolographicLinearLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public HolographicLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HolographicLinearLayout,
+                defStyle, 0);
+        mImageViewId = a.getResourceId(R.styleable.HolographicLinearLayout_sourceImageViewId, -1);
+        a.recycle();
+
+        setWillNotDraw(false);
+        mHolographicHelper = new HolographicViewHelper(context);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        if (mImageView != null) {
+            Drawable d = mImageView.getDrawable();
+            if (d instanceof StateListDrawable) {
+                StateListDrawable sld = (StateListDrawable) d;
+                sld.setState(getDrawableState());
+            }
+        }
+    }
+
+    void invalidatePressedFocusedStates() {
+        mHolographicHelper.invalidatePressedFocusedStates(mImageView);
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // One time call to generate the pressed/focused state -- must be called after
+        // measure/layout
+        if (mImageView == null) {
+            mImageView = (ImageView) findViewById(mImageViewId);
+        }
+        mHolographicHelper.generatePressedFocusedStates(mImageView);
+    }
+}
diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java
new file mode 100644
index 0000000..2decc3d
--- /dev/null
+++ b/src/com/android/launcher3/HolographicOutlineHelper.java
@@ -0,0 +1,221 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+
+public class HolographicOutlineHelper {
+    private final Paint mHolographicPaint = new Paint();
+    private final Paint mBlurPaint = new Paint();
+    private final Paint mErasePaint = new Paint();
+
+    public static final int MAX_OUTER_BLUR_RADIUS;
+    public static final int MIN_OUTER_BLUR_RADIUS;
+
+    private static final BlurMaskFilter sExtraThickOuterBlurMaskFilter;
+    private static final BlurMaskFilter sThickOuterBlurMaskFilter;
+    private static final BlurMaskFilter sMediumOuterBlurMaskFilter;
+    private static final BlurMaskFilter sThinOuterBlurMaskFilter;
+    private static final BlurMaskFilter sThickInnerBlurMaskFilter;
+    private static final BlurMaskFilter sExtraThickInnerBlurMaskFilter;
+    private static final BlurMaskFilter sMediumInnerBlurMaskFilter;
+
+    private static final int THICK = 0;
+    private static final int MEDIUM = 1;
+    private static final int EXTRA_THICK = 2;
+
+    static {
+        final float scale = LauncherApplication.getScreenDensity();
+
+        MIN_OUTER_BLUR_RADIUS = (int) (scale * 1.0f);
+        MAX_OUTER_BLUR_RADIUS = (int) (scale * 12.0f);
+
+        sExtraThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 12.0f, BlurMaskFilter.Blur.OUTER);
+        sThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.OUTER);
+        sMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER);
+        sThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER);
+        sExtraThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.NORMAL);
+        sThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL);
+        sMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL);
+    }
+
+    HolographicOutlineHelper() {
+        mHolographicPaint.setFilterBitmap(true);
+        mHolographicPaint.setAntiAlias(true);
+        mBlurPaint.setFilterBitmap(true);
+        mBlurPaint.setAntiAlias(true);
+        mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+        mErasePaint.setFilterBitmap(true);
+        mErasePaint.setAntiAlias(true);
+    }
+
+    /**
+     * Returns the interpolated holographic highlight alpha for the effect we want when scrolling
+     * pages.
+     */
+    public static float highlightAlphaInterpolator(float r) {
+        float maxAlpha = 0.6f;
+        return (float) Math.pow(maxAlpha * (1.0f - r), 1.5f);
+    }
+
+    /**
+     * Returns the interpolated view alpha for the effect we want when scrolling pages.
+     */
+    public static float viewAlphaInterpolator(float r) {
+        final float pivot = 0.95f;
+        if (r < pivot) {
+            return (float) Math.pow(r / pivot, 1.5f);
+        } else {
+            return 1.0f;
+        }
+    }
+
+    /**
+     * Applies a more expensive and accurate outline to whatever is currently drawn in a specified
+     * bitmap.
+     */
+    void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor, int thickness) {
+        applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, true,
+                thickness);
+    }
+    void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor, boolean clipAlpha, int thickness) {
+
+        // We start by removing most of the alpha channel so as to ignore shadows, and
+        // other types of partial transparency when defining the shape of the object
+        if (clipAlpha) {
+            int[] srcBuffer = new int[srcDst.getWidth() * srcDst.getHeight()];
+            srcDst.getPixels(srcBuffer,
+                    0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight());
+            for (int i = 0; i < srcBuffer.length; i++) {
+                final int alpha = srcBuffer[i] >>> 24;
+                if (alpha < 188) {
+                    srcBuffer[i] = 0;
+                }
+            }
+            srcDst.setPixels(srcBuffer,
+                    0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight());
+        }
+        Bitmap glowShape = srcDst.extractAlpha();
+
+        // calculate the outer blur first
+        BlurMaskFilter outerBlurMaskFilter;
+        switch (thickness) {
+            case EXTRA_THICK:
+                outerBlurMaskFilter = sExtraThickOuterBlurMaskFilter;
+                break;
+            case THICK:
+                outerBlurMaskFilter = sThickOuterBlurMaskFilter;
+                break;
+            case MEDIUM:
+                outerBlurMaskFilter = sMediumOuterBlurMaskFilter;
+                break;
+            default:
+                throw new RuntimeException("Invalid blur thickness");
+        }
+        mBlurPaint.setMaskFilter(outerBlurMaskFilter);
+        int[] outerBlurOffset = new int[2];
+        Bitmap thickOuterBlur = glowShape.extractAlpha(mBlurPaint, outerBlurOffset);
+        if (thickness == EXTRA_THICK) {
+            mBlurPaint.setMaskFilter(sMediumOuterBlurMaskFilter);
+        } else {
+            mBlurPaint.setMaskFilter(sThinOuterBlurMaskFilter);
+        }
+
+        int[] brightOutlineOffset = new int[2];
+        Bitmap brightOutline = glowShape.extractAlpha(mBlurPaint, brightOutlineOffset);
+
+        // calculate the inner blur
+        srcDstCanvas.setBitmap(glowShape);
+        srcDstCanvas.drawColor(0xFF000000, PorterDuff.Mode.SRC_OUT);
+        BlurMaskFilter innerBlurMaskFilter;
+        switch (thickness) {
+            case EXTRA_THICK:
+                innerBlurMaskFilter = sExtraThickInnerBlurMaskFilter;
+                break;
+            case THICK:
+                innerBlurMaskFilter = sThickInnerBlurMaskFilter;
+                break;
+            case MEDIUM:
+                innerBlurMaskFilter = sMediumInnerBlurMaskFilter;
+                break;
+            default:
+                throw new RuntimeException("Invalid blur thickness");
+        }
+        mBlurPaint.setMaskFilter(innerBlurMaskFilter);
+        int[] thickInnerBlurOffset = new int[2];
+        Bitmap thickInnerBlur = glowShape.extractAlpha(mBlurPaint, thickInnerBlurOffset);
+
+        // mask out the inner blur
+        srcDstCanvas.setBitmap(thickInnerBlur);
+        srcDstCanvas.drawBitmap(glowShape, -thickInnerBlurOffset[0],
+                -thickInnerBlurOffset[1], mErasePaint);
+        srcDstCanvas.drawRect(0, 0, -thickInnerBlurOffset[0], thickInnerBlur.getHeight(),
+                mErasePaint);
+        srcDstCanvas.drawRect(0, 0, thickInnerBlur.getWidth(), -thickInnerBlurOffset[1],
+                mErasePaint);
+
+        // draw the inner and outer blur
+        srcDstCanvas.setBitmap(srcDst);
+        srcDstCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        mHolographicPaint.setColor(color);
+        srcDstCanvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1],
+                mHolographicPaint);
+        srcDstCanvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1],
+                mHolographicPaint);
+
+        // draw the bright outline
+        mHolographicPaint.setColor(outlineColor);
+        srcDstCanvas.drawBitmap(brightOutline, brightOutlineOffset[0], brightOutlineOffset[1],
+                mHolographicPaint);
+
+        // cleanup
+        srcDstCanvas.setBitmap(null);
+        brightOutline.recycle();
+        thickOuterBlur.recycle();
+        thickInnerBlur.recycle();
+        glowShape.recycle();
+    }
+
+    void applyExtraThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor) {
+        applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, EXTRA_THICK);
+    }
+
+    void applyThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor) {
+        applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, THICK);
+    }
+
+    void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor, boolean clipAlpha) {
+        applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, clipAlpha,
+                MEDIUM);
+    }
+
+    void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color,
+            int outlineColor) {
+        applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, MEDIUM);
+    }
+
+}
diff --git a/src/com/android/launcher3/HolographicViewHelper.java b/src/com/android/launcher3/HolographicViewHelper.java
new file mode 100644
index 0000000..9d3ad70
--- /dev/null
+++ b/src/com/android/launcher3/HolographicViewHelper.java
@@ -0,0 +1,104 @@
+/*
+ * 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.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.widget.ImageView;
+
+public class HolographicViewHelper {
+
+    private final Canvas mTempCanvas = new Canvas();
+
+    private boolean mStatesUpdated;
+    private int mHighlightColor;
+
+    public HolographicViewHelper(Context context) {
+        Resources res = context.getResources();
+        mHighlightColor = res.getColor(android.R.color.holo_blue_light);
+    }
+
+    /**
+     * Generate the pressed/focused states if necessary.
+     */
+    void generatePressedFocusedStates(ImageView v) {
+        if (!mStatesUpdated && v != null) {
+            mStatesUpdated = true;
+            Bitmap original = createOriginalImage(v, mTempCanvas);
+            Bitmap outline = createPressImage(v, mTempCanvas);
+            FastBitmapDrawable originalD = new FastBitmapDrawable(original);
+            FastBitmapDrawable outlineD = new FastBitmapDrawable(outline);
+
+            StateListDrawable states = new StateListDrawable();
+            states.addState(new int[] {android.R.attr.state_pressed}, outlineD);
+            states.addState(new int[] {android.R.attr.state_focused}, outlineD);
+            states.addState(new int[] {}, originalD);
+            v.setImageDrawable(states);
+        }
+    }
+
+    /**
+     * Invalidates the pressed/focused states.
+     */
+    void invalidatePressedFocusedStates(ImageView v) {
+        mStatesUpdated = false;
+        if (v != null) {
+            v.invalidate();
+        }
+    }
+
+    /**
+     * Creates a copy of the original image.
+     */
+    private Bitmap createOriginalImage(ImageView v, Canvas canvas) {
+        final Drawable d = v.getDrawable();
+        final Bitmap b = Bitmap.createBitmap(
+                d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+
+        canvas.setBitmap(b);
+        canvas.save();
+            d.draw(canvas);
+        canvas.restore();
+        canvas.setBitmap(null);
+
+        return b;
+    }
+
+    /**
+     * Creates a new press state image which is the old image with a blue overlay.
+     * Responsibility for the bitmap is transferred to the caller.
+     */
+    private Bitmap createPressImage(ImageView v, Canvas canvas) {
+        final Drawable d = v.getDrawable();
+        final Bitmap b = Bitmap.createBitmap(
+                d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+
+        canvas.setBitmap(b);
+        canvas.save();
+            d.draw(canvas);
+        canvas.restore();
+        canvas.drawColor(mHighlightColor, PorterDuff.Mode.SRC_IN);
+        canvas.setBitmap(null);
+
+        return b;
+    }
+}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
new file mode 100644
index 0000000..2844d24
--- /dev/null
+++ b/src/com/android/launcher3/Hotseat.java
@@ -0,0 +1,147 @@
+/*
+ * 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.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+
+public class Hotseat extends FrameLayout {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Hotseat";
+
+    private Launcher mLauncher;
+    private CellLayout mContent;
+
+    private int mCellCountX;
+    private int mCellCountY;
+    private int mAllAppsButtonRank;
+
+    private boolean mTransposeLayoutWithOrientation;
+    private boolean mIsLandscape;
+
+    public Hotseat(Context context) {
+        this(context, null);
+    }
+
+    public Hotseat(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public Hotseat(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.Hotseat, defStyle, 0);
+        Resources r = context.getResources();
+        mCellCountX = a.getInt(R.styleable.Hotseat_cellCountX, -1);
+        mCellCountY = a.getInt(R.styleable.Hotseat_cellCountY, -1);
+        mAllAppsButtonRank = r.getInteger(R.integer.hotseat_all_apps_index);
+        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;
+        setOnKeyListener(new HotseatIconKeyEventListener());
+    }
+
+    CellLayout getLayout() {
+        return mContent;
+    }
+  
+    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;
+    }
+    /* Get the orientation specific coordinates given an invariant order in the hotseat. */
+    int getCellXFromOrder(int rank) {
+        return hasVerticalHotseat() ? 0 : rank;
+    }
+    int getCellYFromOrder(int rank) {
+        return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0;
+    }
+    public boolean isAllAppsButtonRank(int rank) {
+        return rank == mAllAppsButtonRank;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        if (mCellCountX < 0) mCellCountX = LauncherModel.getCellCountX();
+        if (mCellCountY < 0) mCellCountY = LauncherModel.getCellCountY();
+        mContent = (CellLayout) findViewById(R.id.layout);
+        mContent.setGridSize(mCellCountX, mCellCountY);
+        mContent.setIsHotseat(true);
+
+        resetLayout();
+    }
+
+    void resetLayout() {
+        mContent.removeAllViewsInLayout();
+
+        // Add the Apps button
+        Context context = getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        BubbleTextView allAppsButton = (BubbleTextView)
+                inflater.inflate(R.layout.application, mContent, false);
+        allAppsButton.setCompoundDrawablesWithIntrinsicBounds(null,
+                context.getResources().getDrawable(R.drawable.all_apps_button_icon), null, null);
+        allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label));
+        allAppsButton.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                if (mLauncher != null &&
+                    (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+                    mLauncher.onTouchDownAllAppsButton(v);
+                }
+                return false;
+            }
+        });
+
+        allAppsButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(android.view.View v) {
+                if (mLauncher != null) {
+                    mLauncher.onClickAllAppsButton(v);
+                }
+            }
+        });
+
+        // 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, 0, lp, true);
+    }
+}
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
new file mode 100644
index 0000000..774f27e
--- /dev/null
+++ b/src/com/android/launcher3/IconCache.java
@@ -0,0 +1,229 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+import java.util.HashMap;
+
+/**
+ * Cache of application icons.  Icons can be made from any thread.
+ */
+public class IconCache {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Launcher.IconCache";
+
+    private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
+
+    private static class CacheEntry {
+        public Bitmap icon;
+        public String title;
+    }
+
+    private final Bitmap mDefaultIcon;
+    private final LauncherApplication mContext;
+    private final PackageManager mPackageManager;
+    private final HashMap<ComponentName, CacheEntry> mCache =
+            new HashMap<ComponentName, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
+    private int mIconDpi;
+
+    public IconCache(LauncherApplication context) {
+        ActivityManager activityManager =
+                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+        mIconDpi = activityManager.getLauncherLargeIconDensity();
+
+        // need to set mIconDpi before getting default icon
+        mDefaultIcon = makeDefaultIcon();
+    }
+
+    public Drawable getFullResDefaultActivityIcon() {
+        return getFullResIcon(Resources.getSystem(),
+                android.R.mipmap.sym_def_app_icon);
+    }
+
+    public Drawable getFullResIcon(Resources resources, int iconId) {
+        Drawable d;
+        try {
+            d = resources.getDrawableForDensity(iconId, mIconDpi);
+        } catch (Resources.NotFoundException e) {
+            d = null;
+        }
+
+        return (d != null) ? d : getFullResDefaultActivityIcon();
+    }
+
+    public Drawable getFullResIcon(String packageName, int iconId) {
+        Resources resources;
+        try {
+            resources = mPackageManager.getResourcesForApplication(packageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            resources = null;
+        }
+        if (resources != null) {
+            if (iconId != 0) {
+                return getFullResIcon(resources, iconId);
+            }
+        }
+        return getFullResDefaultActivityIcon();
+    }
+
+    public Drawable getFullResIcon(ResolveInfo info) {
+        return getFullResIcon(info.activityInfo);
+    }
+
+    public Drawable getFullResIcon(ActivityInfo info) {
+
+        Resources resources;
+        try {
+            resources = mPackageManager.getResourcesForApplication(
+                    info.applicationInfo);
+        } catch (PackageManager.NameNotFoundException e) {
+            resources = null;
+        }
+        if (resources != null) {
+            int iconId = info.getIconResource();
+            if (iconId != 0) {
+                return getFullResIcon(resources, iconId);
+            }
+        }
+        return getFullResDefaultActivityIcon();
+    }
+
+    private Bitmap makeDefaultIcon() {
+        Drawable d = getFullResDefaultActivityIcon();
+        Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1),
+                Math.max(d.getIntrinsicHeight(), 1),
+                Bitmap.Config.ARGB_8888);
+        Canvas c = new Canvas(b);
+        d.setBounds(0, 0, b.getWidth(), b.getHeight());
+        d.draw(c);
+        c.setBitmap(null);
+        return b;
+    }
+
+    /**
+     * Remove any records for the supplied ComponentName.
+     */
+    public void remove(ComponentName componentName) {
+        synchronized (mCache) {
+            mCache.remove(componentName);
+        }
+    }
+
+    /**
+     * Empty out the cache.
+     */
+    public void flush() {
+        synchronized (mCache) {
+            mCache.clear();
+        }
+    }
+
+    /**
+     * Fill in "application" with the icon and label for "info."
+     */
+    public void getTitleAndIcon(ApplicationInfo application, ResolveInfo info,
+            HashMap<Object, CharSequence> labelCache) {
+        synchronized (mCache) {
+            CacheEntry entry = cacheLocked(application.componentName, info, labelCache);
+
+            application.title = entry.title;
+            application.iconBitmap = entry.icon;
+        }
+    }
+
+    public Bitmap getIcon(Intent intent) {
+        synchronized (mCache) {
+            final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0);
+            ComponentName component = intent.getComponent();
+
+            if (resolveInfo == null || component == null) {
+                return mDefaultIcon;
+            }
+
+            CacheEntry entry = cacheLocked(component, resolveInfo, null);
+            return entry.icon;
+        }
+    }
+
+    public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo,
+            HashMap<Object, CharSequence> labelCache) {
+        synchronized (mCache) {
+            if (resolveInfo == null || component == null) {
+                return null;
+            }
+
+            CacheEntry entry = cacheLocked(component, resolveInfo, labelCache);
+            return entry.icon;
+        }
+    }
+
+    public boolean isDefaultIcon(Bitmap icon) {
+        return mDefaultIcon == icon;
+    }
+
+    private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info,
+            HashMap<Object, CharSequence> labelCache) {
+        CacheEntry entry = mCache.get(componentName);
+        if (entry == null) {
+            entry = new CacheEntry();
+
+            mCache.put(componentName, entry);
+
+            ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info);
+            if (labelCache != null && labelCache.containsKey(key)) {
+                entry.title = labelCache.get(key).toString();
+            } else {
+                entry.title = info.loadLabel(mPackageManager).toString();
+                if (labelCache != null) {
+                    labelCache.put(key, entry.title);
+                }
+            }
+            if (entry.title == null) {
+                entry.title = info.activityInfo.name;
+            }
+
+            entry.icon = Utilities.createIconBitmap(
+                    getFullResIcon(info), mContext);
+        }
+        return entry;
+    }
+
+    public HashMap<ComponentName,Bitmap> getAllIcons() {
+        synchronized (mCache) {
+            HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
+            for (ComponentName cn : mCache.keySet()) {
+                final CacheEntry e = mCache.get(cn);
+                set.put(cn, e.icon);
+            }
+            return set;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java
new file mode 100644
index 0000000..9f1b016
--- /dev/null
+++ b/src/com/android/launcher3/InfoDropTarget.java
@@ -0,0 +1,129 @@
+/*
+ * 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.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.R;
+
+public class InfoDropTarget extends ButtonDropTarget {
+
+    private ColorStateList mOriginalTextColor;
+    private TransitionDrawable mDrawable;
+
+    public InfoDropTarget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public InfoDropTarget(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @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 (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 (!LauncherApplication.isScreenLarge()) {
+                setText("");
+            }
+        }
+    }
+
+    private boolean isFromAllApps(DragSource source) {
+        return (source instanceof AppsCustomizePagedView);
+    }
+
+    @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.
+        ComponentName componentName = null;
+        if (d.dragInfo instanceof ApplicationInfo) {
+            componentName = ((ApplicationInfo) 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 (componentName != null) {
+            mLauncher.startApplicationDetailsActivity(componentName);
+        }
+
+        // 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 (!isFromAllApps(source)) {
+            isVisible = false;
+        }
+
+        mActive = isVisible;
+        mDrawable.resetTransition();
+        setTextColor(mOriginalTextColor);
+        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public void onDragEnd() {
+        super.onDragEnd();
+        mActive = false;
+    }
+
+    public void onDragEnter(DragObject d) {
+        super.onDragEnter(d);
+
+        mDrawable.startTransition(mTransitionDuration);
+        setTextColor(mHoverColor);
+    }
+
+    public void onDragExit(DragObject d) {
+        super.onDragExit(d);
+
+        if (!d.dragComplete) {
+            mDrawable.resetTransition();
+            setTextColor(mOriginalTextColor);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
new file mode 100644
index 0000000..a0ea93b
--- /dev/null
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -0,0 +1,363 @@
+/*
+ * 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.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Base64;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.json.*;
+
+public class InstallShortcutReceiver extends BroadcastReceiver {
+    public static final String ACTION_INSTALL_SHORTCUT =
+            "com.android.launcher3.action.INSTALL_SHORTCUT";
+    public static final String NEW_APPS_PAGE_KEY = "apps.new.page";
+    public static final String NEW_APPS_LIST_KEY = "apps.new.list";
+
+    public static final String DATA_INTENT_KEY = "intent.data";
+    public static final String LAUNCH_INTENT_KEY = "intent.launch";
+    public static final String NAME_KEY = "name";
+    public static final String ICON_KEY = "icon";
+    public static final String ICON_RESOURCE_NAME_KEY = "iconResource";
+    public static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage";
+    // The set of shortcuts that are pending install
+    public static final String APPS_PENDING_INSTALL = "apps_to_install";
+
+    public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
+    public static final int NEW_SHORTCUT_STAGGER_DELAY = 75;
+
+    private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0;
+    private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1;
+    private static final int INSTALL_SHORTCUT_NO_SPACE = -2;
+
+    // A mime-type representing shortcut data
+    public static final String SHORTCUT_MIMETYPE =
+            "com.android.launcher3/shortcut";
+
+    private static Object sLock = new Object();
+
+    private static void addToStringSet(SharedPreferences sharedPrefs,
+            SharedPreferences.Editor editor, String key, String value) {
+        Set<String> strings = sharedPrefs.getStringSet(key, null);
+        if (strings == null) {
+            strings = new HashSet<String>(0);
+        } else {
+            strings = new HashSet<String>(strings);
+        }
+        strings.add(value);
+        editor.putStringSet(key, strings);
+    }
+
+    private static void addToInstallQueue(
+            SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) {
+        synchronized(sLock) {
+            try {
+                JSONStringer json = new JSONStringer()
+                    .object()
+                    .key(DATA_INTENT_KEY).value(info.data.toUri(0))
+                    .key(LAUNCH_INTENT_KEY).value(info.launchIntent.toUri(0))
+                    .key(NAME_KEY).value(info.name);
+                if (info.icon != null) {
+                    byte[] iconByteArray = ItemInfo.flattenBitmap(info.icon);
+                    json = json.key(ICON_KEY).value(
+                        Base64.encodeToString(
+                            iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
+                }
+                if (info.iconResource != null) {
+                    json = json.key(ICON_RESOURCE_NAME_KEY).value(info.iconResource.resourceName);
+                    json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY)
+                        .value(info.iconResource.packageName);
+                }
+                json = json.endObject();
+                SharedPreferences.Editor editor = sharedPrefs.edit();
+                addToStringSet(sharedPrefs, editor, APPS_PENDING_INSTALL, json.toString());
+                editor.commit();
+            } catch (org.json.JSONException e) {
+                Log.d("InstallShortcutReceiver", "Exception when adding shortcut: " + e);
+            }
+        }
+    }
+
+    private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue(
+            SharedPreferences sharedPrefs) {
+        synchronized(sLock) {
+            Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
+            if (strings == null) {
+                return new ArrayList<PendingInstallShortcutInfo>();
+            }
+            ArrayList<PendingInstallShortcutInfo> infos =
+                new ArrayList<PendingInstallShortcutInfo>();
+            for (String json : strings) {
+                try {
+                    JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
+                    Intent data = Intent.parseUri(object.getString(DATA_INTENT_KEY), 0);
+                    Intent launchIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0);
+                    String name = object.getString(NAME_KEY);
+                    String iconBase64 = object.optString(ICON_KEY);
+                    String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY);
+                    String iconResourcePackageName =
+                        object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY);
+                    if (iconBase64 != null && !iconBase64.isEmpty()) {
+                        byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT);
+                        Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
+                        data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b);
+                    } else if (iconResourceName != null && !iconResourceName.isEmpty()) {
+                        Intent.ShortcutIconResource iconResource =
+                            new Intent.ShortcutIconResource();
+                        iconResource.resourceName = iconResourceName;
+                        iconResource.packageName = iconResourcePackageName;
+                        data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+                    }
+                    data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
+                    PendingInstallShortcutInfo info =
+                        new PendingInstallShortcutInfo(data, name, launchIntent);
+                    infos.add(info);
+                } catch (org.json.JSONException e) {
+                    Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e);
+                } catch (java.net.URISyntaxException e) {
+                    Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e);
+                }
+            }
+            sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).commit();
+            return infos;
+        }
+    }
+
+    // Determines whether to defer installing shortcuts immediately until
+    // processAllPendingInstalls() is called.
+    private static boolean mUseInstallQueue = false;
+
+    private static class PendingInstallShortcutInfo {
+        Intent data;
+        Intent launchIntent;
+        String name;
+        Bitmap icon;
+        Intent.ShortcutIconResource iconResource;
+
+        public PendingInstallShortcutInfo(Intent rawData, String shortcutName,
+                Intent shortcutIntent) {
+            data = rawData;
+            name = shortcutName;
+            launchIntent = shortcutIntent;
+        }
+    }
+
+    public void onReceive(Context context, Intent data) {
+        if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
+            return;
+        }
+
+        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
+        if (intent == null) {
+            return;
+        }
+        // This name is only used for comparisons and notifications, so fall back to activity name
+        // if not supplied
+        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
+        if (name == null) {
+            try {
+                PackageManager pm = context.getPackageManager();
+                ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
+                name = info.loadLabel(pm).toString();
+            } catch (PackageManager.NameNotFoundException nnfe) {
+                return;
+            }
+        }
+        Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+        Intent.ShortcutIconResource iconResource =
+            data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+
+        // Queue the item up for adding if launcher has not loaded properly yet
+        boolean launcherNotLoaded = LauncherModel.getCellCountX() <= 0 ||
+                LauncherModel.getCellCountY() <= 0;
+
+        PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent);
+        info.icon = icon;
+        info.iconResource = iconResource;
+        if (mUseInstallQueue || launcherNotLoaded) {
+            String spKey = LauncherApplication.getSharedPreferencesKey();
+            SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+            addToInstallQueue(sp, info);
+        } else {
+            processInstallShortcut(context, info);
+        }
+    }
+
+    static void enableInstallQueue() {
+        mUseInstallQueue = true;
+    }
+    static void disableAndFlushInstallQueue(Context context) {
+        mUseInstallQueue = false;
+        flushInstallQueue(context);
+    }
+    static void flushInstallQueue(Context context) {
+        String spKey = LauncherApplication.getSharedPreferencesKey();
+        SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+        ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp);
+        Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator();
+        while (iter.hasNext()) {
+            processInstallShortcut(context, iter.next());
+        }
+    }
+
+    private static void processInstallShortcut(Context context,
+            PendingInstallShortcutInfo pendingInfo) {
+        String spKey = LauncherApplication.getSharedPreferencesKey();
+        SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+
+        final Intent data = pendingInfo.data;
+        final Intent intent = pendingInfo.launchIntent;
+        final String name = pendingInfo.name;
+
+        // Lock on the app so that we don't try and get the items while apps are being added
+        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+        final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL};
+        boolean found = false;
+        synchronized (app) {
+            // 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)
+            app.getModel().flushWorkerThread();
+            final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context);
+            final boolean exists = LauncherModel.shortcutExists(context, name, intent);
+
+            // 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))
+            final int screen = Launcher.DEFAULT_SCREEN;
+            for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) {
+                int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1);
+                if (0 <= si && si < Launcher.SCREEN_COUNT) {
+                    found = installShortcut(context, data, items, name, intent, si, exists, sp,
+                            result);
+                }
+            }
+        }
+
+        // We only report error messages (duplicate shortcut or out of space) as the add-animation
+        // will provide feedback otherwise
+        if (!found) {
+            if (result[0] == INSTALL_SHORTCUT_NO_SPACE) {
+                Toast.makeText(context, context.getString(R.string.completely_out_of_space),
+                        Toast.LENGTH_SHORT).show();
+            } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) {
+                Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name),
+                        Toast.LENGTH_SHORT).show();
+            }
+        }
+    }
+
+    private static boolean installShortcut(Context context, Intent data, ArrayList<ItemInfo> items,
+            String name, final Intent intent, final int screen, boolean shortcutExists,
+            final SharedPreferences sharedPrefs, int[] result) {
+        int[] tmpCoordinates = new int[2];
+        if (findEmptyCell(context, items, tmpCoordinates, screen)) {
+            if (intent != null) {
+                if (intent.getAction() == null) {
+                    intent.setAction(Intent.ACTION_VIEW);
+                } else if (intent.getAction().equals(Intent.ACTION_MAIN) &&
+                        intent.getCategories() != null &&
+                        intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
+                    intent.addFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                }
+
+                // By default, we allow for duplicate entries (located in
+                // different places)
+                boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true);
+                if (duplicate || !shortcutExists) {
+                    new Thread("setNewAppsThread") {
+                        public void run() {
+                            synchronized (sLock) {
+                                // If the new app is going to fall into the same page as before,
+                                // then just continue adding to the current page
+                                final int newAppsScreen = sharedPrefs.getInt(
+                                        NEW_APPS_PAGE_KEY, screen);
+                                SharedPreferences.Editor editor = sharedPrefs.edit();
+                                if (newAppsScreen == -1 || newAppsScreen == screen) {
+                                    addToStringSet(sharedPrefs,
+                                        editor, NEW_APPS_LIST_KEY, intent.toUri(0));
+                                }
+                                editor.putInt(NEW_APPS_PAGE_KEY, screen);
+                                editor.commit();
+                            }
+                        }
+                    }.start();
+
+                    // Update the Launcher db
+                    LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+                    ShortcutInfo info = app.getModel().addShortcut(context, data,
+                            LauncherSettings.Favorites.CONTAINER_DESKTOP, screen,
+                            tmpCoordinates[0], tmpCoordinates[1], true);
+                    if (info == null) {
+                        return false;
+                    }
+                } else {
+                    result[0] = INSTALL_SHORTCUT_IS_DUPLICATE;
+                }
+
+                return true;
+            }
+        } else {
+            result[0] = INSTALL_SHORTCUT_NO_SPACE;
+        }
+
+        return false;
+    }
+
+    private static boolean findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy,
+            int screen) {
+        final int xCount = LauncherModel.getCellCountX();
+        final int yCount = LauncherModel.getCellCountY();
+        boolean[][] occupied = new boolean[xCount][yCount];
+
+        ItemInfo item = null;
+        int cellX, cellY, spanX, spanY;
+        for (int i = 0; i < items.size(); ++i) {
+            item = items.get(i);
+            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                if (item.screen == 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;
+                        }
+                    }
+                }
+            }
+        }
+
+        return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied);
+    }
+}
diff --git a/src/com/android/launcher3/InstallWidgetReceiver.java b/src/com/android/launcher3/InstallWidgetReceiver.java
new file mode 100644
index 0000000..d802df2
--- /dev/null
+++ b/src/com/android/launcher3/InstallWidgetReceiver.java
@@ -0,0 +1,195 @@
+/*
+ * 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 java.util.List;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.DataSetObserver;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+
+/**
+ * We will likely flesh this out later, to handle allow external apps to place widgets, but for now,
+ * we just want to expose the action around for checking elsewhere.
+ */
+public class InstallWidgetReceiver {
+    public static final String ACTION_INSTALL_WIDGET =
+            "com.android.launcher3.action.INSTALL_WIDGET";
+    public static final String ACTION_SUPPORTS_CLIPDATA_MIMETYPE =
+            "com.android.launcher3.action.SUPPORTS_CLIPDATA_MIMETYPE";
+
+    // Currently not exposed.  Put into Intent when we want to make it public.
+    // TEMP: Should we call this "EXTRA_APPWIDGET_PROVIDER"?
+    public static final String EXTRA_APPWIDGET_COMPONENT =
+        "com.android.launcher3.extra.widget.COMPONENT";
+    public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE =
+        "com.android.launcher3.extra.widget.CONFIGURATION_DATA_MIME_TYPE";
+    public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA =
+        "com.android.launcher3.extra.widget.CONFIGURATION_DATA";
+
+    /**
+     * A simple data class that contains per-item information that the adapter below can reference.
+     */
+    public static class WidgetMimeTypeHandlerData {
+        public ResolveInfo resolveInfo;
+        public AppWidgetProviderInfo widgetInfo;
+
+        public WidgetMimeTypeHandlerData(ResolveInfo rInfo, AppWidgetProviderInfo wInfo) {
+            resolveInfo = rInfo;
+            widgetInfo = wInfo;
+        }
+    }
+
+    /**
+     * The ListAdapter which presents all the valid widgets that can be created for a given drop.
+     */
+    public static class WidgetListAdapter implements ListAdapter, DialogInterface.OnClickListener {
+        private LayoutInflater mInflater;
+        private Launcher mLauncher;
+        private String mMimeType;
+        private ClipData mClipData;
+        private List<WidgetMimeTypeHandlerData> mActivities;
+        private CellLayout mTargetLayout;
+        private int mTargetLayoutScreen;
+        private int[] mTargetLayoutPos;
+
+        public WidgetListAdapter(Launcher l, String mimeType, ClipData data,
+                List<WidgetMimeTypeHandlerData> list, CellLayout target,
+                int targetScreen, int[] targetPos) {
+            mLauncher = l;
+            mMimeType = mimeType;
+            mClipData = data;
+            mActivities = list;
+            mTargetLayout = target;
+            mTargetLayoutScreen = targetScreen;
+            mTargetLayoutPos = targetPos;
+        }
+
+        @Override
+        public void registerDataSetObserver(DataSetObserver observer) {
+        }
+
+        @Override
+        public void unregisterDataSetObserver(DataSetObserver observer) {
+        }
+
+        @Override
+        public int getCount() {
+            return mActivities.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+            final PackageManager packageManager = context.getPackageManager();
+
+            // Lazy-create inflater
+            if (mInflater == null) {
+                mInflater = LayoutInflater.from(context);
+            }
+
+            // Use the convert-view where possible
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.external_widget_drop_list_item, parent,
+                        false);
+            }
+
+            final WidgetMimeTypeHandlerData data = mActivities.get(position);
+            final ResolveInfo resolveInfo = data.resolveInfo;
+            final AppWidgetProviderInfo widgetInfo = data.widgetInfo;
+
+            // Set the icon
+            Drawable d = resolveInfo.loadIcon(packageManager);
+            ImageView i = (ImageView) convertView.findViewById(R.id.provider_icon);
+            i.setImageDrawable(d);
+
+            // Set the text
+            final CharSequence component = resolveInfo.loadLabel(packageManager);
+            final int[] widgetSpan = new int[2];
+            mTargetLayout.rectToCell(widgetInfo.minWidth, widgetInfo.minHeight, widgetSpan);
+            TextView t = (TextView) convertView.findViewById(R.id.provider);
+            t.setText(context.getString(R.string.external_drop_widget_pick_format,
+                    component, widgetSpan[0], widgetSpan[1]));
+
+            return convertView;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return mActivities.isEmpty();
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return true;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            final AppWidgetProviderInfo widgetInfo = mActivities.get(which).widgetInfo;
+
+            final PendingAddWidgetInfo createInfo = new PendingAddWidgetInfo(widgetInfo, mMimeType,
+                    mClipData);
+            mLauncher.addAppWidgetFromDrop(createInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                    mTargetLayoutScreen, null, null, mTargetLayoutPos);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java
new file mode 100644
index 0000000..2898b34
--- /dev/null
+++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java
@@ -0,0 +1,131 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.View;
+
+/**
+ * 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
+ * a frame-by-frame mirror of the 'in' animation -- i.e., the interpolated values will
+ * be exactly reversed. Using this class, both the 'in' and the 'out' animation use the
+ * interpolator in the same direction.
+ */
+public class InterruptibleInOutAnimator {
+    private long mOriginalDuration;
+    private float mOriginalFromValue;
+    private float mOriginalToValue;
+    private ValueAnimator mAnimator;
+
+    private boolean mFirstRun = true;
+
+    private Object mTag = null;
+
+    private static final int STOPPED = 0;
+    private static final int IN = 1;
+    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;
+
+    public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) {
+        mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration);
+        mOriginalDuration = duration;
+        mOriginalFromValue = fromValue;
+        mOriginalToValue = toValue;
+
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mDirection = STOPPED;
+            }
+        });
+    }
+
+    private void animate(int direction) {
+        final long currentPlayTime = mAnimator.getCurrentPlayTime();
+        final float toValue = (direction == IN) ? mOriginalToValue : mOriginalFromValue;
+        final float startValue = mFirstRun ? mOriginalFromValue :
+                ((Float) mAnimator.getAnimatedValue()).floatValue();
+
+        // Make sure it's stopped before we modify any values
+        cancel();
+
+        // TODO: We don't really need to do the animation if startValue == toValue, but
+        // somehow that doesn't seem to work, possibly a quirk of the animation framework
+        mDirection = direction;
+
+        // Ensure we don't calculate a non-sensical duration
+        long duration = mOriginalDuration - currentPlayTime;
+        mAnimator.setDuration(Math.max(0, Math.min(duration, mOriginalDuration)));
+
+        mAnimator.setFloatValues(startValue, toValue);
+        mAnimator.start();
+        mFirstRun = false;
+    }
+
+    public void cancel() {
+        mAnimator.cancel();
+        mDirection = STOPPED;
+    }
+
+    public void end() {
+        mAnimator.end();
+        mDirection = STOPPED;
+    }
+
+    /**
+     * Return true when the animation is not running and it hasn't even been started.
+     */
+    public boolean isStopped() {
+        return mDirection == STOPPED;
+    }
+
+    /**
+     * This is the equivalent of calling Animator.start(), except that it can be called when
+     * the animation is running in the opposite direction, in which case we reverse
+     * direction and animate for a correspondingly shorter duration.
+     */
+    public void animateIn() {
+        animate(IN);
+    }
+
+    /**
+     * This is the roughly the equivalent of calling Animator.reverse(), except that it uses the
+     * same interpolation curve as animateIn(), rather than mirroring it. Also, like animateIn(),
+     * if the animation is currently running in the opposite direction, we reverse
+     * direction and animate for a correspondingly shorter duration.
+     */
+    public void animateOut() {
+        animate(OUT);
+    }
+
+    public void setTag(Object tag) {
+        mTag = tag;
+    }
+
+    public Object getTag() {
+        return mTag;
+    }
+
+    public ValueAnimator getAnimator() {
+        return mAnimator;
+    }
+}
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
new file mode 100644
index 0000000..fb41834
--- /dev/null
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -0,0 +1,194 @@
+/*
+ * 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.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Represents an item in the launcher.
+ */
+class ItemInfo {
+    
+    static final int NO_ID = -1;
+    
+    /**
+     * The id in the settings database for this item
+     */
+    long id = NO_ID;
+    
+    /**
+     * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_FOLDER}, or
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPWIDGET}.
+     */
+    int itemType;
+    
+    /**
+     * The id of the container that holds this item. For the desktop, this will be 
+     * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. For the all applications folder it
+     * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders
+     * it will be the id of the folder.
+     */
+    long container = NO_ID;
+    
+    /**
+     * Iindicates the screen in which the shortcut appears.
+     */
+    int screen = -1;
+    
+    /**
+     * Indicates the X position of the associated cell.
+     */
+    int cellX = -1;
+
+    /**
+     * Indicates the Y position of the associated cell.
+     */
+    int cellY = -1;
+
+    /**
+     * Indicates the X cell span.
+     */
+    int spanX = 1;
+
+    /**
+     * Indicates the Y cell span.
+     */
+    int spanY = 1;
+
+    /**
+     * Indicates the minimum X cell span.
+     */
+    int minSpanX = 1;
+
+    /**
+     * Indicates the minimum Y cell span.
+     */
+    int minSpanY = 1;
+
+    /**
+     * Indicates that this item needs to be updated in the db
+     */
+    boolean requiresDbUpdate = false;
+
+    /**
+     * Title of the item
+     */
+    CharSequence title;
+
+    /**
+     * The position of the item in a drag-and-drop operation.
+     */
+    int[] dropPos = null;
+
+    ItemInfo() {
+    }
+
+    ItemInfo(ItemInfo info) {
+        id = info.id;
+        cellX = info.cellX;
+        cellY = info.cellY;
+        spanX = info.spanX;
+        spanY = info.spanY;
+        screen = info.screen;
+        itemType = info.itemType;
+        container = info.container;
+        // tempdebug:
+        LauncherModel.checkItemInfo(this);
+    }
+
+    /** Returns the package name that the intent will resolve to, or an empty string if
+     *  none exists. */
+    static String getPackageName(Intent intent) {
+        if (intent != null) {
+            String packageName = intent.getPackage();
+            if (packageName == null && intent.getComponent() != null) {
+                packageName = intent.getComponent().getPackageName();
+            }
+            if (packageName != null) {
+                return packageName;
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Write the fields of this item to the DB
+     * 
+     * @param values
+     */
+    void onAddToDatabase(ContentValues values) { 
+        values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType);
+        values.put(LauncherSettings.Favorites.CONTAINER, container);
+        values.put(LauncherSettings.Favorites.SCREEN, screen);
+        values.put(LauncherSettings.Favorites.CELLX, cellX);
+        values.put(LauncherSettings.Favorites.CELLY, cellY);
+        values.put(LauncherSettings.Favorites.SPANX, spanX);
+        values.put(LauncherSettings.Favorites.SPANY, spanY);
+    }
+
+    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);
+            values.put(LauncherSettings.Favorites.ICON, data);
+        }
+    }
+
+    /**
+     * It is very important that sub-classes implement this if they contain any references
+     * to the activity (anything in the view hierarchy etc.). If not, leaks can result since
+     * ItemInfo objects persist across rotation and can hence leak by holding stale references
+     * to the old view hierarchy / activity.
+     */
+    void unbind() {
+    }
+
+    @Override
+    public String toString() {
+        return "Item(id=" + this.id + " type=" + this.itemType + " container=" + this.container
+            + " screen=" + screen + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX
+            + " spanY=" + spanY + " dropPos=" + dropPos + ")";
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
new file mode 100644
index 0000000..3f487cd
--- /dev/null
+++ b/src/com/android/launcher3/Launcher.java
@@ -0,0 +1,4070 @@
+
+/*
+ * 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.SearchManager;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+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.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+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.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;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.speech.RecognizerIntent;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.method.TextKeyListener;
+import android.util.Log;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Advanceable;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.common.Search;
+import com.android.launcher3.R;
+import com.android.launcher3.DropTarget.DragObject;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Default launcher application.
+ */
+public final class Launcher extends Activity
+        implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
+                   View.OnTouchListener {
+    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_STRICT_MODE = false;
+    static final boolean DEBUG_RESUME_TIME = false;
+
+    private static final int MENU_GROUP_WALLPAPER = 1;
+    private static final int MENU_WALLPAPER_SETTINGS = Menu.FIRST + 1;
+    private static final int MENU_MANAGE_APPS = MENU_WALLPAPER_SETTINGS + 1;
+    private static final int MENU_SYSTEM_SETTINGS = MENU_MANAGE_APPS + 1;
+    private static final int MENU_HELP = MENU_SYSTEM_SETTINGS + 1;
+
+    private static final int REQUEST_CREATE_SHORTCUT = 1;
+    private static final int REQUEST_CREATE_APPWIDGET = 5;
+    private static final int REQUEST_PICK_APPLICATION = 6;
+    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;
+
+    static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate";
+
+    static final int SCREEN_COUNT = 5;
+    static final int DEFAULT_SCREEN = 2;
+
+    private static final String PREFERENCES = "launcher.preferences";
+    // 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";
+    static final String DUMP_STATE_PROPERTY = "launcher_dump_state";
+
+    // The Intent extra that defines whether to ignore the launch animation
+    static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
+            "com.android.launcher3.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
+
+    // Type: int
+    private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen";
+    // Type: int
+    private static final String RUNTIME_STATE = "launcher.state";
+    // Type: int
+    private static final String RUNTIME_STATE_PENDING_ADD_CONTAINER = "launcher.add_container";
+    // Type: int
+    private static final String RUNTIME_STATE_PENDING_ADD_SCREEN = "launcher.add_screen";
+    // Type: int
+    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
+    private static final String RUNTIME_STATE_PENDING_ADD_SPAN_Y = "launcher.add_span_y";
+    // Type: parcelable
+    private static final String RUNTIME_STATE_PENDING_ADD_WIDGET_INFO = "launcher.add_widget_info";
+
+    private static final String TOOLBAR_ICON_METADATA_NAME = "com.android.launcher3.toolbar_icon";
+    private static final String TOOLBAR_SEARCH_ICON_METADATA_NAME =
+            "com.android.launcher3.toolbar_search_icon";
+    private static final String TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME =
+            "com.android.launcher3.toolbar_voice_search_icon";
+
+    /** 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;
+    private AnimatorSet mDividerAnimator;
+
+    static final int APPWIDGET_HOST_ID = 1024;
+    private static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT = 300;
+    private static final int EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT = 600;
+    private static final int SHOW_CLING_DURATION = 550;
+    private static final int DISMISS_CLING_DURATION = 250;
+
+    private static final Object sLock = new Object();
+    private static int sScreen = DEFAULT_SCREEN;
+
+    // How long to wait before the new-shortcut animation automatically pans the workspace
+    private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 10;
+
+    private final BroadcastReceiver mCloseSystemDialogsReceiver
+            = new CloseSystemDialogsIntentReceiver();
+    private final ContentObserver mWidgetObserver = new AppWidgetResetObserver();
+
+    private LayoutInflater mInflater;
+
+    private Workspace mWorkspace;
+    private View mQsbDivider;
+    private View mDockDivider;
+    private View mLauncherView;
+    private DragLayer mDragLayer;
+    private DragController mDragController;
+
+    private AppWidgetManager mAppWidgetManager;
+    private LauncherAppWidgetHost mAppWidgetHost;
+
+    private ItemInfo mPendingAddInfo = new ItemInfo();
+    private AppWidgetProviderInfo mPendingAddWidgetInfo;
+
+    private int[] mTmpAddItemCellCoordinates = new int[2];
+
+    private FolderInfo mFolderInfo;
+
+    private Hotseat mHotseat;
+    private View mAllAppsButton;
+
+    private SearchDropTargetBar mSearchDropTargetBar;
+    private AppsCustomizeTabHost mAppsCustomizeTabHost;
+    private AppsCustomizePagedView mAppsCustomizeContent;
+    private boolean mAutoAdvanceRunning = false;
+
+    private Bundle mSavedState;
+    // We set the state in both onCreate and then onNewIntent in some cases, which causes both
+    // scroll issues (because the workspace may not have been measured yet) and extra work.
+    // Instead, just save the state that we need to restore Launcher to, and commit it in onResume.
+    private State mOnResumeState = State.NONE;
+
+    private SpannableStringBuilder mDefaultKeySsb = null;
+
+    private boolean mWorkspaceLoading = true;
+
+    private boolean mPaused = true;
+    private boolean mRestoring;
+    private boolean mWaitingForResult;
+    private boolean mOnResumeNeedsLoad;
+
+    private ArrayList<Runnable> mOnResumeCallbacks = new ArrayList<Runnable>();
+
+    // Keep track of whether the user has left launcher
+    private static boolean sPausedFromUserAction = false;
+
+    private Bundle mSavedInstanceState;
+
+    private LauncherModel mModel;
+    private IconCache mIconCache;
+    private boolean mUserPresent = true;
+    private boolean mVisible = false;
+    private boolean mAttached = false;
+
+    private static LocaleConfiguration sLocaleConfiguration = null;
+
+    private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>();
+
+    private Intent mAppMarketIntent = null;
+
+    // Related to the auto-advancing of widgets
+    private final int ADVANCE_MSG = 1;
+    private final int mAdvanceInterval = 20000;
+    private final int mAdvanceStagger = 250;
+    private long mAutoAdvanceSentTime;
+    private long mAutoAdvanceTimeLeft = -1;
+    private 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;
+
+    // External icons saved in case of resource changes, orientation, etc.
+    private static Drawable.ConstantState[] sGlobalSearchIcon = new Drawable.ConstantState[2];
+    private static Drawable.ConstantState[] sVoiceSearchIcon = new Drawable.ConstantState[2];
+    private static Drawable.ConstantState[] sAppMarketIcon = new Drawable.ConstantState[2];
+
+    private Drawable mWorkspaceBackgroundDrawable;
+
+    private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>();
+
+    static final ArrayList<String> sDumpLogs = new ArrayList<String>();
+
+    // We only want to get the SharedPreferences once since it does an FS stat each time we get
+    // it from the context.
+    private SharedPreferences mSharedPrefs;
+
+    // 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 int mNewShortcutAnimatePage = -1;
+    private ArrayList<View> mNewShortcutAnimateViews = new ArrayList<View>();
+    private ImageView mFolderIconImageView;
+    private Bitmap mFolderIconBitmap;
+    private Canvas mFolderIconCanvas;
+    private Rect mRectForFolderAnimation = new Rect();
+
+    private BubbleTextView mWaitingForResume;
+
+    private HideFromAccessibilityHelper mHideFromAccessibilityHelper
+        = new HideFromAccessibilityHelper();
+
+    private Runnable mBuildLayersRunnable = new Runnable() {
+        public void run() {
+            if (mWorkspace != null) {
+                mWorkspace.buildPageHardwareLayers();
+            }
+        }
+    };
+
+    private static ArrayList<PendingAddArguments> sPendingAddList
+            = new ArrayList<PendingAddArguments>();
+
+    private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY);
+
+    private static class PendingAddArguments {
+        int requestCode;
+        Intent intent;
+        long container;
+        int screen;
+        int cellX;
+        int cellY;
+    }
+
+    private static boolean isPropertyEnabled(String propertyName) {
+        return Log.isLoggable(propertyName, Log.VERBOSE);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        if (DEBUG_STRICT_MODE) {
+            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .detectNetwork()   // or .detectAll() for all detectable problems
+                    .penaltyLog()
+                    .build());
+            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+                    .detectLeakedSqlLiteObjects()
+                    .detectLeakedClosableObjects()
+                    .penaltyLog()
+                    .penaltyDeath()
+                    .build());
+        }
+
+        super.onCreate(savedInstanceState);
+        LauncherApplication app = ((LauncherApplication)getApplication());
+        mSharedPrefs = getSharedPreferences(LauncherApplication.getSharedPreferencesKey(),
+                Context.MODE_PRIVATE);
+        mModel = app.setLauncher(this);
+        mIconCache = app.getIconCache();
+        mDragController = new DragController(this);
+        mInflater = getLayoutInflater();
+
+        mAppWidgetManager = AppWidgetManager.getInstance(this);
+        mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
+        mAppWidgetHost.startListening();
+
+        // If we are getting an onCreate, we can actually preempt onResume and unset mPaused here,
+        // this also ensures that any synchronous binding below doesn't re-trigger another
+        // LauncherModel load.
+        mPaused = false;
+
+        if (PROFILE_STARTUP) {
+            android.os.Debug.startMethodTracing(
+                    Environment.getExternalStorageDirectory() + "/launcher");
+        }
+
+        checkForLocaleChange();
+        setContentView(R.layout.launcher);
+        setupViews();
+        showFirstRunWorkspaceCling();
+
+        registerContentObservers();
+
+        lockAllApps();
+
+        mSavedState = savedInstanceState;
+        restoreState(mSavedState);
+
+        // Update customization drawer _after_ restoring the states
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.onPackagesUpdated(
+                LauncherModel.getSortedWidgetsAndShortcuts(this));
+        }
+
+        if (PROFILE_STARTUP) {
+            android.os.Debug.stopMethodTracing();
+        }
+
+        if (!mRestoring) {
+            if (sPausedFromUserAction) {
+                // If the user leaves launcher, then we should just load items asynchronously when
+                // they return.
+                mModel.startLoader(true, -1);
+            } 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.getCurrentPage());
+            }
+        }
+
+        if (!mModel.isAllAppsLoaded()) {
+            ViewGroup appsCustomizeContentParent = (ViewGroup) mAppsCustomizeContent.getParent();
+            mInflater.inflate(R.layout.apps_customize_progressbar, appsCustomizeContentParent);
+        }
+
+        // For handling default keys
+        mDefaultKeySsb = new SpannableStringBuilder();
+        Selection.setSelection(mDefaultKeySsb, 0);
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        registerReceiver(mCloseSystemDialogsReceiver, filter);
+
+        updateGlobalIcons();
+
+        // On large interfaces, we want the screen to auto-rotate based on the current orientation
+        unlockScreenOrientation(true);
+    }
+
+    protected void onUserLeaveHint() {
+        super.onUserLeaveHint();
+        sPausedFromUserAction = true;
+    }
+
+    private void updateGlobalIcons() {
+        boolean searchVisible = false;
+        boolean voiceVisible = false;
+        // If we have a saved version of these external icons, we load them up immediately
+        int coi = getCurrentOrientationIndexForGlobalIcons();
+        if (sGlobalSearchIcon[coi] == null || sVoiceSearchIcon[coi] == null ||
+                sAppMarketIcon[coi] == null) {
+            updateAppMarketIcon();
+            searchVisible = updateGlobalSearchIcon();
+            voiceVisible = updateVoiceSearchIcon(searchVisible);
+        }
+        if (sGlobalSearchIcon[coi] != null) {
+             updateGlobalSearchIcon(sGlobalSearchIcon[coi]);
+             searchVisible = true;
+        }
+        if (sVoiceSearchIcon[coi] != null) {
+            updateVoiceSearchIcon(sVoiceSearchIcon[coi]);
+            voiceVisible = true;
+        }
+        if (sAppMarketIcon[coi] != null) {
+            updateAppMarketIcon(sAppMarketIcon[coi]);
+        }
+        if (mSearchDropTargetBar != null) {
+            mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible);
+        }
+    }
+
+    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 Thread("WriteLocaleConfiguration") {
+                @Override
+                public void run() {
+                    writeConfiguration(Launcher.this, localeConfiguration);
+                }
+            }.start();
+        }
+    }
+
+    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(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(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(PREFERENCES).delete();
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    public DragLayer getDragLayer() {
+        return mDragLayer;
+    }
+
+    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;
+        }
+    }
+
+    /**
+     * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
+     * a configuration step, this allows the proper animations to run after other transitions.
+     */
+    private boolean completeAdd(PendingAddArguments args) {
+        boolean result = false;
+        switch (args.requestCode) {
+            case REQUEST_PICK_APPLICATION:
+                completeAddApplication(args.intent, args.container, args.screen, args.cellX,
+                        args.cellY);
+                break;
+            case REQUEST_PICK_SHORTCUT:
+                processShortcut(args.intent);
+                break;
+            case REQUEST_CREATE_SHORTCUT:
+                completeAddShortcut(args.intent, args.container, args.screen, args.cellX,
+                        args.cellY);
+                result = true;
+                break;
+            case REQUEST_CREATE_APPWIDGET:
+                int appWidgetId = args.intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+                completeAddAppWidget(appWidgetId, args.container, args.screen, null, null);
+                result = true;
+                break;
+            case REQUEST_PICK_WALLPAPER:
+                // We just wanted the activity result here so we can clear mWaitingForResult
+                break;
+        }
+        // Before adding this resetAddInfo(), after a shortcut was added to a workspace screen,
+        // if you turned the screen off and then back while in All Apps, Launcher would not
+        // return to the workspace. Clearing mAddInfo.container here fixes this issue
+        resetAddInfo();
+        return result;
+    }
+
+    @Override
+    protected void onActivityResult(
+            final int requestCode, final int resultCode, final Intent data) {
+        if (requestCode == REQUEST_BIND_APPWIDGET) {
+            int appWidgetId = data != null ?
+                    data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1;
+            if (resultCode == RESULT_CANCELED) {
+                completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId);
+            } else if (resultCode == RESULT_OK) {
+                addAppWidgetImpl(appWidgetId, mPendingAddInfo, null, mPendingAddWidgetInfo);
+            }
+            return;
+        }
+        boolean delayExitSpringLoadedMode = false;
+        boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET ||
+                requestCode == REQUEST_CREATE_APPWIDGET);
+        mWaitingForResult = false;
+
+        // We have special handling for widgets
+        if (isWidgetDrop) {
+            int appWidgetId = data != null ?
+                    data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1;
+            if (appWidgetId < 0) {
+                Log.e(TAG, "Error: appWidgetId (EXTRA_APPWIDGET_ID) was not returned from the \\" +
+                        "widget configuration activity.");
+                completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId);
+            } else {
+                completeTwoStageWidgetDrop(resultCode, appWidgetId);
+            }
+            return;
+        }
+
+        // The pattern used here is that a user PICKs a specific application,
+        // which, depending on the target, might need to CREATE the actual target.
+
+        // For example, the user would PICK_SHORTCUT for "Music playlist", and we
+        // launch over to the Music app to actually CREATE_SHORTCUT.
+        if (resultCode == RESULT_OK && mPendingAddInfo.container != ItemInfo.NO_ID) {
+            final PendingAddArguments args = new PendingAddArguments();
+            args.requestCode = requestCode;
+            args.intent = data;
+            args.container = mPendingAddInfo.container;
+            args.screen = mPendingAddInfo.screen;
+            args.cellX = mPendingAddInfo.cellX;
+            args.cellY = mPendingAddInfo.cellY;
+            if (isWorkspaceLocked()) {
+                sPendingAddList.add(args);
+            } else {
+                delayExitSpringLoadedMode = completeAdd(args);
+            }
+        }
+        mDragLayer.clearAnimatedView();
+        // Exit spring loaded mode if necessary after cancelling the configuration of a widget
+        exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), delayExitSpringLoadedMode,
+                null);
+    }
+
+    private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) {
+        CellLayout cellLayout =
+                (CellLayout) mWorkspace.getChildAt(mPendingAddInfo.screen);
+        Runnable onCompleteRunnable = null;
+        int animationType = 0;
+
+        AppWidgetHostView boundWidget = null;
+        if (resultCode == RESULT_OK) {
+            animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
+            final AppWidgetHostView layout = mAppWidgetHost.createView(this, appWidgetId,
+                    mPendingAddWidgetInfo);
+            boundWidget = layout;
+            onCompleteRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    completeAddAppWidget(appWidgetId, mPendingAddInfo.container,
+                            mPendingAddInfo.screen, layout, null);
+                    exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false,
+                            null);
+                }
+            };
+        } else if (resultCode == RESULT_CANCELED) {
+            animationType = Workspace.CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION;
+            onCompleteRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false,
+                            null);
+                }
+            };
+        }
+        if (mDragLayer.getAnimatedView() != null) {
+            mWorkspace.animateWidgetDrop(mPendingAddInfo, cellLayout,
+                    (DragView) mDragLayer.getAnimatedView(), onCompleteRunnable,
+                    animationType, boundWidget, true);
+        } else {
+            // The animated view may be null in the case of a rotation during widget configuration
+            onCompleteRunnable.run();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        FirstFrameAnimatorHelper.setIsVisible(false);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        FirstFrameAnimatorHelper.setIsVisible(true);
+    }
+
+    @Override
+    protected void onResume() {
+        long startTime = 0;
+        if (DEBUG_RESUME_TIME) {
+            startTime = System.currentTimeMillis();
+        }
+        super.onResume();
+
+        // Restore the previous launcher state
+        if (mOnResumeState == State.WORKSPACE) {
+            showWorkspace(false);
+        } else if (mOnResumeState == State.APPS_CUSTOMIZE) {
+            showAllApps(false);
+        }
+        mOnResumeState = State.NONE;
+
+        // Background was set to gradient in onPause(), restore to black if in all apps.
+        setWorkspaceBackground(mState == State.WORKSPACE);
+
+        // Process any items that were added while Launcher was away
+        InstallShortcutReceiver.flushInstallQueue(this);
+
+        mPaused = false;
+        sPausedFromUserAction = false;
+        if (mRestoring || mOnResumeNeedsLoad) {
+            mWorkspaceLoading = true;
+            mModel.startLoader(true, -1);
+            mRestoring = false;
+            mOnResumeNeedsLoad = false;
+        }
+        if (mOnResumeCallbacks.size() > 0) {
+            // We might have postponed some bind calls until onResume (see waitUntilResume) --
+            // execute them here
+            long startTimeCallbacks = 0;
+            if (DEBUG_RESUME_TIME) {
+                startTimeCallbacks = System.currentTimeMillis();
+            }
+
+            if (mAppsCustomizeContent != null) {
+                mAppsCustomizeContent.setBulkBind(true);
+            }
+            for (int i = 0; i < mOnResumeCallbacks.size(); i++) {
+                mOnResumeCallbacks.get(i).run();
+            }
+            if (mAppsCustomizeContent != null) {
+                mAppsCustomizeContent.setBulkBind(false);
+            }
+            mOnResumeCallbacks.clear();
+            if (DEBUG_RESUME_TIME) {
+                Log.d(TAG, "Time spent processing callbacks in onResume: " +
+                    (System.currentTimeMillis() - startTimeCallbacks));
+            }
+        }
+
+        // Reset the pressed state of icons that were locked in the press state while activities
+        // were launching
+        if (mWaitingForResume != null) {
+            // Resets the previous workspace icon press state
+            mWaitingForResume.setStayPressed(false);
+        }
+        if (mAppsCustomizeContent != null) {
+            // Resets the previous all apps icon press state
+            mAppsCustomizeContent.resetDrawableState();
+        }
+        // It is possible that widgets can receive updates while launcher is not in the foreground.
+        // Consequently, the widgets will be inflated in the orientation of the foreground activity
+        // (framework issue). On resuming, we ensure that any widgets are inflated for the current
+        // orientation.
+        getWorkspace().reinflateWidgetsIfNecessary();
+
+        // Again, as with the above scenario, it's possible that one or more of the global icons
+        // were updated in the wrong orientation.
+        updateGlobalIcons();
+        if (DEBUG_RESUME_TIME) {
+            Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime));
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled
+        // to be consistent.  So re-enable the flag here, and we will re-disable it as necessary
+        // when Launcher resumes and we are still in AllApps.
+        updateWallpaperVisibility(true);
+
+        super.onPause();
+        mPaused = true;
+        mDragController.cancelDrag();
+        mDragController.resetLastGestureUpTime();
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        // Flag the loader to stop early before switching
+        mModel.stopLoader();
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.surrender();
+        }
+        return Boolean.TRUE;
+    }
+
+    // We can't hide the IME if it was forced open.  So don't bother
+    /*
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+
+        if (hasFocus) {
+            final InputMethodManager inputManager = (InputMethodManager)
+                    getSystemService(Context.INPUT_METHOD_SERVICE);
+            WindowManager.LayoutParams lp = getWindow().getAttributes();
+            inputManager.hideSoftInputFromWindow(lp.token, 0, new android.os.ResultReceiver(new
+                        android.os.Handler()) {
+                        protected void onReceiveResult(int resultCode, Bundle resultData) {
+                            Log.d(TAG, "ResultReceiver got resultCode=" + resultCode);
+                        }
+                    });
+            Log.d(TAG, "called hideSoftInputFromWindow from onWindowFocusChanged");
+        }
+    }
+    */
+
+    private boolean acceptFilter() {
+        final InputMethodManager inputManager = (InputMethodManager)
+                getSystemService(Context.INPUT_METHOD_SERVICE);
+        return !inputManager.isFullscreenMode();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        final int uniChar = event.getUnicodeChar();
+        final boolean handled = super.onKeyDown(keyCode, event);
+        final boolean isKeyNotWhitespace = uniChar > 0 && !Character.isWhitespace(uniChar);
+        if (!handled && acceptFilter() && isKeyNotWhitespace) {
+            boolean gotKey = TextKeyListener.getInstance().onKeyDown(mWorkspace, mDefaultKeySsb,
+                    keyCode, event);
+            if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) {
+                // something usable has been typed - start a search
+                // the typed text will be retrieved and cleared by
+                // showSearchDialog()
+                // If there are multiple keystrokes before the search dialog takes focus,
+                // onSearchRequested() will be called for every keystroke,
+                // but it is idempotent, so it's fine.
+                return onSearchRequested();
+            }
+        }
+
+        // Eat the long press event so the keyboard doesn't come up.
+        if (keyCode == KeyEvent.KEYCODE_MENU && event.isLongPress()) {
+            return true;
+        }
+
+        return handled;
+    }
+
+    private String getTypedText() {
+        return mDefaultKeySsb.toString();
+    }
+
+    private void clearTypedText() {
+        mDefaultKeySsb.clear();
+        mDefaultKeySsb.clearSpans();
+        Selection.setSelection(mDefaultKeySsb, 0);
+    }
+
+    /**
+     * Given the integer (ordinal) value of a State enum instance, convert it to a variable of type
+     * State
+     */
+    private static State intToState(int stateOrdinal) {
+        State state = State.WORKSPACE;
+        final State[] stateValues = State.values();
+        for (int i = 0; i < stateValues.length; i++) {
+            if (stateValues[i].ordinal() == stateOrdinal) {
+                state = stateValues[i];
+                break;
+            }
+        }
+        return state;
+    }
+
+    /**
+     * Restores the previous state, if it exists.
+     *
+     * @param savedState The previous state.
+     */
+    private void restoreState(Bundle savedState) {
+        if (savedState == null) {
+            return;
+        }
+
+        State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal()));
+        if (state == State.APPS_CUSTOMIZE) {
+            mOnResumeState = State.APPS_CUSTOMIZE;
+        }
+
+        int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1);
+        if (currentScreen > -1) {
+            mWorkspace.setCurrentPage(currentScreen);
+        }
+
+        final long pendingAddContainer = savedState.getLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, -1);
+        final int pendingAddScreen = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SCREEN, -1);
+
+        if (pendingAddContainer != ItemInfo.NO_ID && pendingAddScreen > -1) {
+            mPendingAddInfo.container = pendingAddContainer;
+            mPendingAddInfo.screen = pendingAddScreen;
+            mPendingAddInfo.cellX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_X);
+            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);
+            mWaitingForResult = 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);
+        }
+    }
+
+    /**
+     * Finds all the views we need and configure them properly.
+     */
+    private void setupViews() {
+        final DragController dragController = mDragController;
+
+        mLauncherView = findViewById(R.id.launcher);
+        mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
+        mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
+        mQsbDivider = findViewById(R.id.qsb_divider);
+        mDockDivider = findViewById(R.id.dock_divider);
+
+        mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+        mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg);
+
+        // Setup the drag layer
+        mDragLayer.setup(this, dragController);
+
+        // Setup the hotseat
+        mHotseat = (Hotseat) findViewById(R.id.hotseat);
+        if (mHotseat != null) {
+            mHotseat.setup(this);
+        }
+
+        // Setup the workspace
+        mWorkspace.setHapticFeedbackEnabled(false);
+        mWorkspace.setOnLongClickListener(this);
+        mWorkspace.setup(dragController);
+        dragController.addDragListener(mWorkspace);
+
+        // Get the search/delete bar
+        mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_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 the drag controller (drop targets have to be added in reverse order in priority)
+        dragController.setDragScoller(mWorkspace);
+        dragController.setScrollView(mDragLayer);
+        dragController.setMoveTarget(mWorkspace);
+        dragController.addDropTarget(mWorkspace);
+        if (mSearchDropTargetBar != null) {
+            mSearchDropTargetBar.setup(this, dragController);
+        }
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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);
+        favorite.setOnClickListener(this);
+        return favorite;
+    }
+
+    /**
+     * Add an application shortcut to the workspace.
+     *
+     * @param data The intent describing the application.
+     * @param cellInfo The position on screen where to create the shortcut.
+     */
+    void completeAddApplication(Intent data, long container, int screen, int cellX, int cellY) {
+        final int[] cellXY = mTmpAddItemCellCoordinates;
+        final CellLayout layout = getCellLayout(container, screen);
+
+        // 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;
+            cellXY[1] = cellY;
+        } else if (!layout.findCellForSpan(cellXY, 1, 1)) {
+            showOutOfSpaceMessage(isHotseatLayout(layout));
+            return;
+        }
+
+        final ShortcutInfo info = mModel.getShortcutInfo(getPackageManager(), data, this);
+
+        if (info != null) {
+            info.setActivity(data.getComponent(), Intent.FLAG_ACTIVITY_NEW_TASK |
+                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+            info.container = ItemInfo.NO_ID;
+            mWorkspace.addApplicationShortcut(info, layout, container, screen, cellXY[0], cellXY[1],
+                    isWorkspaceLocked(), cellX, cellY);
+        } else {
+            Log.e(TAG, "Couldn't find ActivityInfo for selected application: " + data);
+        }
+    }
+
+    /**
+     * 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, int screen, int cellX,
+            int cellY) {
+        int[] cellXY = mTmpAddItemCellCoordinates;
+        int[] touchXY = mPendingAddInfo.dropPos;
+        CellLayout layout = getCellLayout(container, screen);
+
+        boolean foundCellSpan = false;
+
+        ShortcutInfo info = mModel.infoFromShortcutIntent(this, data, null);
+        if (info == null) {
+            return;
+        }
+        final View view = createShortcut(info);
+
+        // 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;
+            cellXY[1] = cellY;
+            foundCellSpan = true;
+
+            // If appropriate, either create a folder or add to an existing folder
+            if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, 0,
+                    true, null,null)) {
+                return;
+            }
+            DragObject dragObject = new DragObject();
+            dragObject.dragInfo = info;
+            if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, 0, dragObject,
+                    true)) {
+                return;
+            }
+        } else if (touchXY != null) {
+            // when dragging and dropping, just find the closest free spot
+            int[] result = layout.findNearestVacantArea(touchXY[0], touchXY[1], 1, 1, cellXY);
+            foundCellSpan = (result != null);
+        } else {
+            foundCellSpan = layout.findCellForSpan(cellXY, 1, 1);
+        }
+
+        if (!foundCellSpan) {
+            showOutOfSpaceMessage(isHotseatLayout(layout));
+            return;
+        }
+
+        LauncherModel.addItemToDatabase(this, info, container, screen, cellXY[0], cellXY[1], false);
+
+        if (!mRestoring) {
+            mWorkspace.addInScreen(view, container, screen, cellXY[0], cellXY[1], 1, 1,
+                    isWorkspaceLocked());
+        }
+    }
+
+    static int[] getSpanForWidget(Context context, ComponentName component, int minWidth,
+            int minHeight) {
+        Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context, 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(context.getResources(), 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);
+    }
+
+    static int[] getSpanForWidget(Context context, PendingAddWidgetInfo info) {
+        return getSpanForWidget(context, info.componentName, info.minWidth, info.minHeight);
+    }
+
+    static int[] getMinSpanForWidget(Context context, PendingAddWidgetInfo info) {
+        return getSpanForWidget(context, info.componentName, 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, int screen,
+            AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) {
+        if (appWidgetInfo == null) {
+            appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
+        }
+
+        // Calculate the grid spans needed to fit this widget
+        CellLayout layout = getCellLayout(container, screen);
+
+        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]);
+        }
+
+        if (!foundCellSpan) {
+            if (appWidgetId != -1) {
+                // Deleting an app widget ID is a void call but writes to disk before returning
+                // to the caller...
+                new Thread("deleteAppWidgetId") {
+                    public void run() {
+                        mAppWidgetHost.deleteAppWidgetId(appWidgetId);
+                    }
+                }.start();
+            }
+            showOutOfSpaceMessage(isHotseatLayout(layout));
+            return;
+        }
+
+        // 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;
+
+        LauncherModel.addItemToDatabase(this, launcherInfo,
+                container, screen, cellXY[0], cellXY[1], false);
+
+        if (!mRestoring) {
+            if (hostView == null) {
+                // Perform actual inflation because we're live
+                launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
+                launcherInfo.hostView.setAppWidget(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, screen, cellXY[0], cellXY[1],
+                    launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked());
+
+            addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo);
+        }
+        resetAddInfo();
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                mUserPresent = false;
+                mDragLayer.clearAllResizeFrames();
+                updateRunning();
+
+                // 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) {
+                    mAppsCustomizeTabHost.reset();
+                    showWorkspace(false);
+                }
+            } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+                mUserPresent = true;
+                updateRunning();
+            }
+        }
+    };
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Listen for broadcasts related to user-presence
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(Intent.ACTION_USER_PRESENT);
+        registerReceiver(mReceiver, filter);
+        FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView());
+        mAttached = true;
+        mVisible = true;
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mVisible = false;
+
+        if (mAttached) {
+            unregisterReceiver(mReceiver);
+            mAttached = false;
+        }
+        updateRunning();
+    }
+
+    public void onWindowVisibilityChanged(int visibility) {
+        mVisible = visibility == View.VISIBLE;
+        updateRunning();
+        // 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
+                // layers on all the workspace pages, so that transitioning to Launcher from other
+                // apps is nice and speedy.
+                observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
+                    private boolean mStarted = false;
+                    public void onDraw() {
+                        if (mStarted) return;
+                        mStarted = true;
+                        // We delay the layer building a bit in order to give
+                        // other message processing a time to run.  In particular
+                        // this avoids a delay in hiding the IME if it was
+                        // currently shown, because doing that may involve
+                        // some communication back with the app.
+                        mWorkspace.postDelayed(mBuildLayersRunnable, 500);
+                        final ViewTreeObserver.OnDrawListener listener = this;
+                        mWorkspace.post(new Runnable() {
+                                public void run() {
+                                    if (mWorkspace != null &&
+                                            mWorkspace.getViewTreeObserver() != null) {
+                                        mWorkspace.getViewTreeObserver().
+                                                removeOnDrawListener(listener);
+                                    }
+                                }
+                            });
+                        return;
+                    }
+                });
+            }
+            // When Launcher comes back to foreground, a different Activity might be responsible for
+            // the app market intent, so refresh the icon
+            updateAppMarketIcon();
+            clearTypedText();
+        }
+    }
+
+    private void sendAdvanceMessage(long delay) {
+        mHandler.removeMessages(ADVANCE_MSG);
+        Message msg = mHandler.obtainMessage(ADVANCE_MSG);
+        mHandler.sendMessageDelayed(msg, delay);
+        mAutoAdvanceSentTime = System.currentTimeMillis();
+    }
+
+    private void updateRunning() {
+        boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty();
+        if (autoAdvanceRunning != mAutoAdvanceRunning) {
+            mAutoAdvanceRunning = autoAdvanceRunning;
+            if (autoAdvanceRunning) {
+                long delay = mAutoAdvanceTimeLeft == -1 ? mAdvanceInterval : mAutoAdvanceTimeLeft;
+                sendAdvanceMessage(delay);
+            } else {
+                if (!mWidgetsToAdvance.isEmpty()) {
+                    mAutoAdvanceTimeLeft = Math.max(0, mAdvanceInterval -
+                            (System.currentTimeMillis() - mAutoAdvanceSentTime));
+                }
+                mHandler.removeMessages(ADVANCE_MSG);
+                mHandler.removeMessages(0); // Remove messages sent using postDelayed()
+            }
+        }
+    }
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void 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() {
+                           public void run() {
+                               ((Advanceable) v).advance();
+                           }
+                       }, delay);
+                    }
+                    i++;
+                }
+                sendAdvanceMessage(mAdvanceInterval);
+            }
+        }
+    };
+
+    void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) {
+        if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return;
+        View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId);
+        if (v instanceof Advanceable) {
+            mWidgetsToAdvance.put(hostView, appWidgetInfo);
+            ((Advanceable) v).fyiWillBeAdvancedByHostKThx();
+            updateRunning();
+        }
+    }
+
+    void removeWidgetToAutoAdvance(View hostView) {
+        if (mWidgetsToAdvance.containsKey(hostView)) {
+            mWidgetsToAdvance.remove(hostView);
+            updateRunning();
+        }
+    }
+
+    public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) {
+        removeWidgetToAutoAdvance(launcherInfo.hostView);
+        launcherInfo.hostView = null;
+    }
+
+    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();
+    }
+
+    public LauncherAppWidgetHost getAppWidgetHost() {
+        return mAppWidgetHost;
+    }
+
+    public LauncherModel getModel() {
+        return mModel;
+    }
+
+    void closeSystemDialogs() {
+        getWindow().closeAllPanels();
+
+        // Whatever we were doing is hereby canceled.
+        mWaitingForResult = false;
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        long startTime = 0;
+        if (DEBUG_RESUME_TIME) {
+            startTime = System.currentTimeMillis();
+        }
+        super.onNewIntent(intent);
+
+        // Close the menu
+        if (Intent.ACTION_MAIN.equals(intent.getAction())) {
+            // also will cancel mWaitingForResult.
+            closeSystemDialogs();
+
+            final boolean alreadyOnHome =
+                    ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT)
+                        != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
+
+            Runnable processIntent = new Runnable() {
+                public void run() {
+                    if (mWorkspace == null) {
+                        // Can be cases where mWorkspace is null, this prevents a NPE
+                        return;
+                    }
+                    Folder openFolder = mWorkspace.getOpenFolder();
+                    // In all these cases, only animate if we're already on home
+                    mWorkspace.exitWidgetResizeMode();
+                    if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() &&
+                            openFolder == null) {
+                        mWorkspace.moveToDefaultScreen(true);
+                    }
+
+                    closeFolder();
+                    exitSpringLoadedDragMode();
+
+                    // If we are already on home, then just animate back to the workspace,
+                    // otherwise, just wait until onResume to set the state back to Workspace
+                    if (alreadyOnHome) {
+                        showWorkspace(true);
+                    } else {
+                        mOnResumeState = State.WORKSPACE;
+                    }
+
+                    final View v = getWindow().peekDecorView();
+                    if (v != null && v.getWindowToken() != null) {
+                        InputMethodManager imm = (InputMethodManager)getSystemService(
+                                INPUT_METHOD_SERVICE);
+                        imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+                    }
+
+                    // Reset AllApps to its initial state
+                    if (!alreadyOnHome && mAppsCustomizeTabHost != null) {
+                        mAppsCustomizeTabHost.reset();
+                    }
+                }
+            };
+
+            if (alreadyOnHome && !mWorkspace.hasWindowFocus()) {
+                // Delay processing of the intent to allow the status bar animation to finish
+                // first in order to avoid janky animations.
+                mWorkspace.postDelayed(processIntent, 350);
+            } else {
+                // Process the intent immediately.
+                processIntent.run();
+            }
+
+        }
+        if (DEBUG_RESUME_TIME) {
+            Log.d(TAG, "Time spent in onNewIntent: " + (System.currentTimeMillis() - startTime));
+        }
+    }
+
+    @Override
+    public void onRestoreInstanceState(Bundle state) {
+        super.onRestoreInstanceState(state);
+        for (int page: mSynchronouslyBoundPages) {
+            mWorkspace.restoreInstanceStateForChild(page);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getNextPage());
+        super.onSaveInstanceState(outState);
+
+        outState.putInt(RUNTIME_STATE, mState.ordinal());
+        // We close any open folder since it will not be re-opened, and we need to make sure
+        // this state is reflected.
+        closeFolder();
+
+        if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screen > -1 &&
+                mWaitingForResult) {
+            outState.putLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, mPendingAddInfo.container);
+            outState.putInt(RUNTIME_STATE_PENDING_ADD_SCREEN, mPendingAddInfo.screen);
+            outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_X, mPendingAddInfo.cellX);
+            outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_Y, mPendingAddInfo.cellY);
+            outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_X, mPendingAddInfo.spanX);
+            outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y, mPendingAddInfo.spanY);
+            outState.putParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO, mPendingAddWidgetInfo);
+        }
+
+        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) {
+            String currentTabTag = mAppsCustomizeTabHost.getCurrentTabTag();
+            if (currentTabTag != null) {
+                outState.putString("apps_customize_currentTab", currentTabTag);
+            }
+            int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex();
+            outState.putInt("apps_customize_currentIndex", currentIndex);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        // Remove all pending runnables
+        mHandler.removeMessages(ADVANCE_MSG);
+        mHandler.removeMessages(0);
+        mWorkspace.removeCallbacks(mBuildLayersRunnable);
+
+        // Stop callbacks from LauncherModel
+        LauncherApplication app = ((LauncherApplication) getApplication());
+        mModel.stopLoader();
+        app.setLauncher(null);
+
+        try {
+            mAppWidgetHost.stopListening();
+        } catch (NullPointerException ex) {
+            Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex);
+        }
+        mAppWidgetHost = null;
+
+        mWidgetsToAdvance.clear();
+
+        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();
+        ((ViewGroup) mWorkspace.getParent()).removeAllViews();
+        mWorkspace.removeAllViews();
+        mWorkspace = null;
+        mDragController = null;
+
+        LauncherAnimUtils.onDestroyActivity();
+    }
+
+    public DragController getDragController() {
+        return mDragController;
+    }
+
+    @Override
+    public void startActivityForResult(Intent intent, int requestCode) {
+        if (requestCode >= 0) mWaitingForResult = true;
+        super.startActivityForResult(intent, requestCode);
+    }
+
+    /**
+     * Indicates that we want global search for this activity by setting the globalSearch
+     * argument for {@link #startSearch} to true.
+     */
+    @Override
+    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();
+        }
+        if (appSearchData == null) {
+            appSearchData = new Bundle();
+            appSearchData.putString(Search.SOURCE, "launcher-search");
+        }
+        Rect sourceBounds = new Rect();
+        if (mSearchDropTargetBar != null) {
+            sourceBounds = mSearchDropTargetBar.getSearchBarBounds();
+        }
+
+        startGlobalSearch(initialQuery, selectInitialQuery,
+            appSearchData, sourceBounds);
+    }
+
+    /**
+     * Starts the global search activity. This code is a copied from SearchManager
+     */
+    public void startGlobalSearch(String initialQuery,
+            boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds) {
+        final SearchManager searchManager =
+            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+        ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity();
+        if (globalSearchActivity == null) {
+            Log.w(TAG, "No global search activity found.");
+            return;
+        }
+        Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setComponent(globalSearchActivity);
+        // Make sure that we have a Bundle to put source in
+        if (appSearchData == null) {
+            appSearchData = new Bundle();
+        } else {
+            appSearchData = new Bundle(appSearchData);
+        }
+        // Set source to package name of app that starts global search, if not set already.
+        if (!appSearchData.containsKey("source")) {
+            appSearchData.putString("source", getPackageName());
+        }
+        intent.putExtra(SearchManager.APP_DATA, appSearchData);
+        if (!TextUtils.isEmpty(initialQuery)) {
+            intent.putExtra(SearchManager.QUERY, initialQuery);
+        }
+        if (selectInitialQuery) {
+            intent.putExtra(SearchManager.EXTRA_SELECT_QUERY, selectInitialQuery);
+        }
+        intent.setSourceBounds(sourceBounds);
+        try {
+            startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.e(TAG, "Global search activity not found: " + globalSearchActivity);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (isWorkspaceLocked()) {
+            return false;
+        }
+
+        super.onCreateOptionsMenu(menu);
+
+        Intent manageApps = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
+        manageApps.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        Intent settings = new Intent(android.provider.Settings.ACTION_SETTINGS);
+        settings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+        String helpUrl = getString(R.string.help_url);
+        Intent help = new Intent(Intent.ACTION_VIEW, Uri.parse(helpUrl));
+        help.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+        menu.add(MENU_GROUP_WALLPAPER, MENU_WALLPAPER_SETTINGS, 0, R.string.menu_wallpaper)
+            .setIcon(android.R.drawable.ic_menu_gallery)
+            .setAlphabeticShortcut('W');
+        menu.add(0, MENU_MANAGE_APPS, 0, R.string.menu_manage_apps)
+            .setIcon(android.R.drawable.ic_menu_manage)
+            .setIntent(manageApps)
+            .setAlphabeticShortcut('M');
+        menu.add(0, MENU_SYSTEM_SETTINGS, 0, R.string.menu_settings)
+            .setIcon(android.R.drawable.ic_menu_preferences)
+            .setIntent(settings)
+            .setAlphabeticShortcut('P');
+        if (!helpUrl.isEmpty()) {
+            menu.add(0, MENU_HELP, 0, R.string.menu_help)
+                .setIcon(android.R.drawable.ic_menu_help)
+                .setIntent(help)
+                .setAlphabeticShortcut('H');
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+
+        if (mAppsCustomizeTabHost.isTransitioning()) {
+            return false;
+        }
+        boolean allAppsVisible = (mAppsCustomizeTabHost.getVisibility() == View.VISIBLE);
+        menu.setGroupVisible(MENU_GROUP_WALLPAPER, !allAppsVisible);
+
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+        case MENU_WALLPAPER_SETTINGS:
+            startWallpaper();
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onSearchRequested() {
+        startSearch(null, false, null, true);
+        // Use a custom animation for launching search
+        return true;
+    }
+
+    public boolean isWorkspaceLocked() {
+        return mWorkspaceLoading || mWaitingForResult;
+    }
+
+    private void resetAddInfo() {
+        mPendingAddInfo.container = ItemInfo.NO_ID;
+        mPendingAddInfo.screen = -1;
+        mPendingAddInfo.cellX = mPendingAddInfo.cellY = -1;
+        mPendingAddInfo.spanX = mPendingAddInfo.spanY = -1;
+        mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = -1;
+        mPendingAddInfo.dropPos = null;
+    }
+
+    void addAppWidgetImpl(final int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget,
+            AppWidgetProviderInfo appWidgetInfo) {
+        if (appWidgetInfo.configure != null) {
+            mPendingAddWidgetInfo = appWidgetInfo;
+
+            // Launch over to configure widget, if needed
+            Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
+            intent.setComponent(appWidgetInfo.configure);
+            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+            startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET);
+        } else {
+            // Otherwise just add it
+            completeAddAppWidget(appWidgetId, info.container, info.screen, boundWidget,
+                    appWidgetInfo);
+            // Exit spring loaded mode if necessary after adding the widget
+            exitSpringLoadedDragModeDelayed(true, false, null);
+        }
+    }
+
+    /**
+     * Process a shortcut drop.
+     *
+     * @param componentName The name of the component
+     * @param screen 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, int screen,
+            int[] cell, int[] loc) {
+        resetAddInfo();
+        mPendingAddInfo.container = container;
+        mPendingAddInfo.screen = screen;
+        mPendingAddInfo.dropPos = loc;
+
+        if (cell != null) {
+            mPendingAddInfo.cellX = cell[0];
+            mPendingAddInfo.cellY = cell[1];
+        }
+
+        Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+        createShortcutIntent.setComponent(componentName);
+        processShortcut(createShortcutIntent);
+    }
+
+    /**
+     * Process a widget drop.
+     *
+     * @param info The PendingAppWidgetInfo of the widget being added.
+     * @param screen 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, int screen,
+            int[] cell, int[] span, int[] loc) {
+        resetAddInfo();
+        mPendingAddInfo.container = info.container = container;
+        mPendingAddInfo.screen = info.screen = screen;
+        mPendingAddInfo.dropPos = loc;
+        mPendingAddInfo.minSpanX = info.minSpanX;
+        mPendingAddInfo.minSpanY = info.minSpanY;
+
+        if (cell != null) {
+            mPendingAddInfo.cellX = cell[0];
+            mPendingAddInfo.cellY = cell[1];
+        }
+        if (span != null) {
+            mPendingAddInfo.spanX = span[0];
+            mPendingAddInfo.spanY = span[1];
+        }
+
+        AppWidgetHostView hostView = info.boundWidget;
+        int appWidgetId;
+        if (hostView != null) {
+            appWidgetId = hostView.getAppWidgetId();
+            addAppWidgetImpl(appWidgetId, info, hostView, info.info);
+        } 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.
+            appWidgetId = getAppWidgetHost().allocateAppWidgetId();
+            Bundle options = info.bindOptions;
+
+            boolean success = false;
+            if (options != null) {
+                success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
+                        info.componentName, options);
+            } else {
+                success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
+                        info.componentName);
+            }
+            if (success) {
+                addAppWidgetImpl(appWidgetId, info, null, info.info);
+            } else {
+                mPendingAddWidgetInfo = info.info;
+                Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
+                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName);
+                // TODO: we need to make sure that this accounts for the options bundle.
+                // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
+                startActivityForResult(intent, REQUEST_BIND_APPWIDGET);
+            }
+        }
+    }
+
+    void processShortcut(Intent intent) {
+        // Handle case where user selected "Applications"
+        String applicationName = getResources().getString(R.string.group_applications);
+        String shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
+
+        if (applicationName != null && applicationName.equals(shortcutName)) {
+            Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+
+            Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY);
+            pickIntent.putExtra(Intent.EXTRA_INTENT, mainIntent);
+            pickIntent.putExtra(Intent.EXTRA_TITLE, getText(R.string.title_select_application));
+            startActivityForResultSafely(pickIntent, REQUEST_PICK_APPLICATION);
+        } else {
+            startActivityForResultSafely(intent, REQUEST_CREATE_SHORTCUT);
+        }
+    }
+
+    void processWallpaper(Intent intent) {
+        startActivityForResult(intent, REQUEST_PICK_WALLPAPER);
+    }
+
+    FolderIcon addFolder(CellLayout layout, long container, final int screen, 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, screen, cellX, cellY,
+                false);
+        sFolders.put(folderInfo.id, folderInfo);
+
+        // Create the view
+        FolderIcon newFolder =
+            FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo, mIconCache);
+        mWorkspace.addInScreen(newFolder, container, screen, cellX, cellY, 1, 1,
+                isWorkspaceLocked());
+        return newFolder;
+    }
+
+    void removeFolder(FolderInfo folder) {
+        sFolders.remove(folder.id);
+    }
+
+    private void startWallpaper() {
+        showWorkspace(true);
+        final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER);
+        Intent chooser = Intent.createChooser(pickWallpaper,
+                getText(R.string.chooser_wallpaper));
+        // NOTE: Adds a configure option to the chooser if the wallpaper supports it
+        //       Removed in Eclair MR1
+//        WallpaperManager wm = (WallpaperManager)
+//                getSystemService(Context.WALLPAPER_SERVICE);
+//        WallpaperInfo wi = wm.getWallpaperInfo();
+//        if (wi != null && wi.getSettingsActivity() != null) {
+//            LabeledIntent li = new LabeledIntent(getPackageName(),
+//                    R.string.configure_wallpaper, 0);
+//            li.setClassName(wi.getPackageName(), wi.getSettingsActivity());
+//            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { li });
+//        }
+        startActivityForResult(chooser, REQUEST_PICK_WALLPAPER);
+    }
+
+    /**
+     * 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) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_HOME:
+                    return true;
+                case KeyEvent.KEYCODE_VOLUME_DOWN:
+                    if (isPropertyEnabled(DUMP_STATE_PROPERTY)) {
+                        dumpState();
+                        return true;
+                    }
+                    break;
+            }
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_HOME:
+                    return true;
+            }
+        }
+
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (isAllAppsVisible()) {
+            showWorkspace(true);
+        } else if (mWorkspace.getOpenFolder() != null) {
+            Folder openFolder = mWorkspace.getOpenFolder();
+            if (openFolder.isEditingName()) {
+                openFolder.dismissEditingName();
+            } else {
+                closeFolder();
+            }
+        } else {
+            mWorkspace.exitWidgetResizeMode();
+
+            // Back button is a no-op here, but give at least some feedback for the button press
+            mWorkspace.showOutlinesTemporarily();
+        }
+    }
+
+    /**
+     * Re-listen when widgets are reset.
+     */
+    private void onAppWidgetReset() {
+        if (mAppWidgetHost != null) {
+            mAppWidgetHost.startListening();
+        }
+    }
+
+    /**
+     * Launches the intent referred by the clicked shortcut.
+     *
+     * @param v The view representing the clicked shortcut.
+     */
+    public void onClick(View v) {
+        // Make sure that rogue clicks don't get through while allapps is launching, or after the
+        // view has detached (it's possible for this to happen if the view is removed mid touch).
+        if (v.getWindowToken() == null) {
+            return;
+        }
+
+        if (!mWorkspace.isFinishedSwitchingState()) {
+            return;
+        }
+
+        Object tag = v.getTag();
+        if (tag instanceof ShortcutInfo) {
+            // Open shortcut
+            final Intent intent = ((ShortcutInfo) tag).intent;
+            int[] pos = new int[2];
+            v.getLocationOnScreen(pos);
+            intent.setSourceBounds(new Rect(pos[0], pos[1],
+                    pos[0] + v.getWidth(), pos[1] + v.getHeight()));
+
+            boolean success = startActivitySafely(v, intent, tag);
+
+            if (success && v instanceof BubbleTextView) {
+                mWaitingForResume = (BubbleTextView) v;
+                mWaitingForResume.setStayPressed(true);
+            }
+        } else if (tag instanceof FolderInfo) {
+            if (v instanceof FolderIcon) {
+                FolderIcon fi = (FolderIcon) v;
+                handleFolderClick(fi);
+            }
+        } else if (v == mAllAppsButton) {
+            if (isAllAppsVisible()) {
+                showWorkspace(true);
+            } else {
+                onClickAllAppsButton(v);
+            }
+        }
+    }
+
+    public boolean onTouch(View v, MotionEvent event) {
+        // this is an intercepted event being forwarded from mWorkspace;
+        // clicking anywhere on the workspace causes the customization drawer to slide down
+        showWorkspace(true);
+        return false;
+    }
+
+    /**
+     * Event handler for the search button
+     *
+     * @param v The view that was clicked.
+     */
+    public void onClickSearchButton(View v) {
+        v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+        onSearchRequested();
+    }
+
+    /**
+     * Event handler for the voice button
+     *
+     * @param v The view that was clicked.
+     */
+    public void onClickVoiceButton(View v) {
+        v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+        try {
+            final SearchManager searchManager =
+                    (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+            ComponentName activityName = searchManager.getGlobalSearchActivity();
+            Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            if (activityName != null) {
+                intent.setPackage(activityName.getPackageName());
+            }
+            startActivity(null, intent, "onClickVoiceButton");
+        } catch (ActivityNotFoundException e) {
+            Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivitySafely(null, intent, "onClickVoiceButton");
+        }
+    }
+
+    /**
+     * Event handler for the "grid" button that appears on the home screen, which
+     * enters all apps mode.
+     *
+     * @param v The view that was clicked.
+     */
+    public void onClickAllAppsButton(View v) {
+        showAllApps(true);
+    }
+
+    public void onTouchDownAllAppsButton(View v) {
+        // Provide the same haptic feedback that the system offers for virtual keys.
+        v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+    }
+
+    public void onClickAppMarketButton(View v) {
+        if (mAppMarketIntent != null) {
+            startActivitySafely(v, mAppMarketIntent, "app market");
+        } else {
+            Log.e(TAG, "Invalid app market intent.");
+        }
+    }
+
+    void startApplicationDetailsActivity(ComponentName componentName) {
+        String packageName = componentName.getPackageName();
+        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+                Uri.fromParts("package", packageName, null));
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        startActivitySafely(null, intent, "startApplicationDetailsActivity");
+    }
+
+    void startApplicationUninstallActivity(ApplicationInfo appInfo) {
+        if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0) {
+            // System applications cannot be installed. For now, show a toast explaining that.
+            // We may give them the option of disabling apps this way.
+            int messageId = R.string.uninstall_system_app_text;
+            Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
+        } else {
+            String packageName = appInfo.componentName.getPackageName();
+            String className = appInfo.componentName.getClassName();
+            Intent intent = new Intent(
+                    Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className));
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+                    Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+            startActivity(intent);
+        }
+    }
+
+    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
+            // private contract between launcher and may be ignored in the future).
+            boolean useLaunchAnimation = (v != null) &&
+                    !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION);
+            if (useLaunchAnimation) {
+                ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0,
+                        v.getMeasuredWidth(), v.getMeasuredHeight());
+
+                startActivity(intent, opts.toBundle());
+            } else {
+                startActivity(intent);
+            }
+            return true;
+        } catch (SecurityException e) {
+            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+            Log.e(TAG, "Launcher does not have the permission to launch " + intent +
+                    ". Make sure to create a MAIN intent-filter for the corresponding activity " +
+                    "or use the exported attribute for this activity. "
+                    + "tag="+ tag + " intent=" + intent, e);
+        }
+        return false;
+    }
+
+    boolean startActivitySafely(View v, Intent intent, Object tag) {
+        boolean success = false;
+        try {
+            success = startActivity(v, intent, tag);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+            Log.e(TAG, "Unable to launch. tag=" + tag + " intent=" + intent, e);
+        }
+        return success;
+    }
+
+    void startActivityForResultSafely(Intent intent, int requestCode) {
+        try {
+            startActivityForResult(intent, requestCode);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+        } catch (SecurityException e) {
+            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+            Log.e(TAG, "Launcher does not have the permission to launch " + intent +
+                    ". Make sure to create a MAIN intent-filter for the corresponding activity " +
+                    "or use the exported attribute for this activity.", e);
+        }
+    }
+
+    private void handleFolderClick(FolderIcon folderIcon) {
+        final FolderInfo info = folderIcon.getFolderInfo();
+        Folder openFolder = mWorkspace.getFolderForTag(info);
+
+        // If the folder info reports that the associated folder is open, then verify that
+        // it is actually opened. There have been a few instances where this gets out of sync.
+        if (info.opened && openFolder == null) {
+            Log.d(TAG, "Folder info marked as open, but associated folder is not open. Screen: "
+                    + info.screen + " (" + info.cellX + ", " + info.cellY + ")");
+            info.opened = false;
+        }
+
+        if (!info.opened && !folderIcon.getFolder().isDestroyed()) {
+            // Close any open folder
+            closeFolder();
+            // Open the requested folder
+            openFolder(folderIcon);
+        } else {
+            // Find the open folder...
+            int folderScreen;
+            if (openFolder != null) {
+                folderScreen = mWorkspace.getPageForView(openFolder);
+                // .. and close it
+                closeFolder(openFolder);
+                if (folderScreen != mWorkspace.getCurrentPage()) {
+                    // Close any folder open on the current screen
+                    closeFolder();
+                    // Pull the folder onto this screen
+                    openFolder(folderIcon);
+                }
+            }
+        }
+    }
+
+    /**
+     * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView
+     * in the DragLayer in the exact absolute location of the original FolderIcon.
+     */
+    private void copyFolderIconToImage(FolderIcon fi) {
+        final int width = fi.getMeasuredWidth();
+        final int height = fi.getMeasuredHeight();
+
+        // Lazy load ImageView, Bitmap and Canvas
+        if (mFolderIconImageView == null) {
+            mFolderIconImageView = new ImageView(this);
+        }
+        if (mFolderIconBitmap == null || mFolderIconBitmap.getWidth() != width ||
+                mFolderIconBitmap.getHeight() != height) {
+            mFolderIconBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+            mFolderIconCanvas = new Canvas(mFolderIconBitmap);
+        }
+
+        DragLayer.LayoutParams lp;
+        if (mFolderIconImageView.getLayoutParams() instanceof DragLayer.LayoutParams) {
+            lp = (DragLayer.LayoutParams) mFolderIconImageView.getLayoutParams();
+        } else {
+            lp = new DragLayer.LayoutParams(width, height);
+        }
+
+        // The layout from which the folder is being opened may be scaled, adjust the starting
+        // view size by this scale factor.
+        float scale = mDragLayer.getDescendantRectRelativeToSelf(fi, mRectForFolderAnimation);
+        lp.customPosition = true;
+        lp.x = mRectForFolderAnimation.left;
+        lp.y = mRectForFolderAnimation.top;
+        lp.width = (int) (scale * width);
+        lp.height = (int) (scale * height);
+
+        mFolderIconCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        fi.draw(mFolderIconCanvas);
+        mFolderIconImageView.setImageBitmap(mFolderIconBitmap);
+        if (fi.getFolder() != null) {
+            mFolderIconImageView.setPivotX(fi.getFolder().getPivotXForIconAnimation());
+            mFolderIconImageView.setPivotY(fi.getFolder().getPivotYForIconAnimation());
+        }
+        // Just in case this image view is still in the drag layer from a previous animation,
+        // we remove it and re-add it.
+        if (mDragLayer.indexOfChild(mFolderIconImageView) != -1) {
+            mDragLayer.removeView(mFolderIconImageView);
+        }
+        mDragLayer.addView(mFolderIconImageView, lp);
+        if (fi.getFolder() != null) {
+            fi.getFolder().bringToFront();
+        }
+    }
+
+    private void growAndFadeOutFolderIcon(FolderIcon fi) {
+        if (fi == null) return;
+        PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
+        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.5f);
+        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.5f);
+
+        FolderInfo info = (FolderInfo) fi.getTag();
+        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            CellLayout cl = (CellLayout) fi.getParent().getParent();
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) fi.getLayoutParams();
+            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+        }
+
+        // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original
+        copyFolderIconToImage(fi);
+        fi.setVisibility(View.INVISIBLE);
+
+        ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha,
+                scaleX, scaleY);
+        oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration));
+        oa.start();
+    }
+
+    private void shrinkAndFadeInFolderIcon(final FolderIcon fi) {
+        if (fi == null) return;
+        PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f);
+        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
+        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
+
+        final CellLayout cl = (CellLayout) fi.getParent().getParent();
+
+        // We remove and re-draw the FolderIcon in-case it has changed
+        mDragLayer.removeView(mFolderIconImageView);
+        copyFolderIconToImage(fi);
+        ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha,
+                scaleX, scaleY);
+        oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration));
+        oa.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (cl != null) {
+                    cl.clearFolderLeaveBehind();
+                    // Remove the ImageView copy of the FolderIcon and make the original visible.
+                    mDragLayer.removeView(mFolderIconImageView);
+                    fi.setVisibility(View.VISIBLE);
+                }
+            }
+        });
+        oa.start();
+    }
+
+    /**
+     * Opens the user folder described by the specified tag. The opening of the folder
+     * is animated relative to the specified View. If the View is null, no animation
+     * is played.
+     *
+     * @param folderInfo The FolderInfo describing the folder to open.
+     */
+    public void openFolder(FolderIcon folderIcon) {
+        Folder folder = folderIcon.getFolder();
+        FolderInfo info = folder.mInfo;
+
+        info.opened = true;
+
+        // 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) {
+            mDragLayer.addView(folder);
+            mDragController.addDropTarget((DropTarget) folder);
+        } else {
+            Log.w(TAG, "Opening folder (" + folder + ") which already has a parent (" +
+                    folder.getParent() + ").");
+        }
+        folder.animateOpen();
+        growAndFadeOutFolderIcon(folderIcon);
+
+        // Notify the accessibility manager that this folder "window" has appeared and occluded
+        // the workspace items
+        folder.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+    }
+
+    public void closeFolder() {
+        Folder folder = mWorkspace.getOpenFolder();
+        if (folder != null) {
+            if (folder.isEditingName()) {
+                folder.dismissEditingName();
+            }
+            closeFolder(folder);
+
+            // Dismiss the folder cling
+            dismissFolderCling(null);
+        }
+    }
+
+    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);
+        }
+        folder.animateClosed();
+
+        // Notify the accessibility manager that this folder "window" has disappeard and no
+        // longer occludeds the workspace items
+        getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    public boolean onLongClick(View v) {
+        if (!isDraggingEnabled()) return false;
+        if (isWorkspaceLocked()) return false;
+        if (mState != State.WORKSPACE) return false;
+
+        if (!(v instanceof CellLayout)) {
+            v = (View) v.getParent().getParent();
+        }
+
+        resetAddInfo();
+        CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();
+        // This happens when long clicking an item with the dpad/trackball
+        if (longClickCellInfo == null) {
+            return true;
+        }
+
+        // The hotseat touch handling does not go through Workspace, and we always allow long press
+        // on hotseat items.
+        final View itemUnderLongClick = longClickCellInfo.cell;
+        boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress();
+        if (allowLongPress && !mDragController.isDragging()) {
+            if (itemUnderLongClick == null) {
+                // User long pressed on empty space
+                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
+                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                startWallpaper();
+            } else {
+                if (!(itemUnderLongClick instanceof Folder)) {
+                    // User long pressed on an item
+                    mWorkspace.startDrag(longClickCellInfo);
+                }
+            }
+        }
+        return true;
+    }
+
+    boolean isHotseatLayout(View layout) {
+        return mHotseat != null && layout != null &&
+                (layout instanceof CellLayout) && (layout == mHotseat.getLayout());
+    }
+    Hotseat getHotseat() {
+        return mHotseat;
+    }
+    SearchDropTargetBar getSearchBar() {
+        return mSearchDropTargetBar;
+    }
+
+    /**
+     * Returns the CellLayout of the specified container at the specified screen.
+     */
+    CellLayout getCellLayout(long container, int screen) {
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            if (mHotseat != null) {
+                return mHotseat.getLayout();
+            } else {
+                return null;
+            }
+        } else {
+            return (CellLayout) mWorkspace.getChildAt(screen);
+        }
+    }
+
+    Workspace getWorkspace() {
+        return mWorkspace;
+    }
+
+    // Now a part of LauncherModel.Callbacks. Used to reorder loading steps.
+    @Override
+    public boolean isAllAppsVisible() {
+        return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE);
+    }
+
+    @Override
+    public boolean isAllAppsButtonRank(int rank) {
+        return mHotseat.isAllAppsButtonRank(rank);
+    }
+
+    /**
+     * Helper method for the cameraZoomIn/cameraZoomOut animations
+     * @param view The view being animated
+     * @param scaleFactor The scale factor used for the zoom
+     */
+    private void setPivotsForZoom(View view, float scaleFactor) {
+        view.setPivotX(view.getWidth() / 2.0f);
+        view.setPivotY(view.getHeight() / 2.0f);
+    }
+
+    void disableWallpaperIfInAllApps() {
+        // Only disable it if we are in all apps
+        if (isAllAppsVisible()) {
+            if (mAppsCustomizeTabHost != null &&
+                    !mAppsCustomizeTabHost.isTransitioning()) {
+                updateWallpaperVisibility(false);
+            }
+        }
+    }
+
+    private void setWorkspaceBackground(boolean workspace) {
+        mLauncherView.setBackground(workspace ?
+                mWorkspaceBackgroundDrawable : null);
+    }
+
+    void updateWallpaperVisibility(boolean visible) {
+        int wpflags = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0;
+        int curflags = getWindow().getAttributes().flags
+                & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+        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) {
+        if (mStateAnimation != null) {
+            mStateAnimation.setDuration(0);
+            mStateAnimation.cancel();
+            mStateAnimation = null;
+        }
+        final Resources res = getResources();
+
+        final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime);
+        final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime);
+        final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor);
+        final View fromView = mWorkspace;
+        final AppsCustomizeTabHost toView = mAppsCustomizeTabHost;
+        final int startDelay =
+                res.getInteger(R.integer.config_workspaceAppsCustomizeAnimationStagger);
+
+        setPivotsForZoom(toView, scale);
+
+        // Shrink workspaces away if going to AppsCustomize from workspace
+        Animator workspaceAnim =
+                mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated);
+
+        if (animated) {
+            toView.setScaleX(scale);
+            toView.setScaleY(scale);
+            final LauncherViewPropertyAnimator scaleAnim = new LauncherViewPropertyAnimator(toView);
+            scaleAnim.
+                scaleX(1f).scaleY(1f).
+                setDuration(duration).
+                setInterpolator(new Workspace.ZoomOutInterpolator());
+
+            toView.setVisibility(View.VISIBLE);
+            toView.setAlpha(0f);
+            final ObjectAnimator alphaAnim = LauncherAnimUtils
+                .ofFloat(toView, "alpha", 0f, 1f)
+                .setDuration(fadeDuration);
+            alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f));
+            alphaAnim.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    if (animation == null) {
+                        throw new RuntimeException("animation is null");
+                    }
+                    float t = (Float) animation.getAnimatedValue();
+                    dispatchOnLauncherTransitionStep(fromView, t);
+                    dispatchOnLauncherTransitionStep(toView, t);
+                }
+            });
+
+            // toView should appear right at the end of the workspace shrink
+            // animation
+            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
+            mStateAnimation.play(scaleAnim).after(startDelay);
+            mStateAnimation.play(alphaAnim).after(startDelay);
+
+            mStateAnimation.addListener(new AnimatorListenerAdapter() {
+                boolean animationCancelled = false;
+
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    updateWallpaperVisibility(true);
+                    // Prepare the position
+                    toView.setTranslationX(0.0f);
+                    toView.setTranslationY(0.0f);
+                    toView.setVisibility(View.VISIBLE);
+                    toView.bringToFront();
+                }
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    dispatchOnLauncherTransitionEnd(fromView, animated, false);
+                    dispatchOnLauncherTransitionEnd(toView, animated, false);
+
+                    if (mWorkspace != null && !springLoaded && !LauncherApplication.isScreenLarge()) {
+                        // Hide the workspace scrollbar
+                        mWorkspace.hideScrollingIndicator(true);
+                        hideDockDivider();
+                    }
+                    if (!animationCancelled) {
+                        updateWallpaperVisibility(false);
+                    }
+
+                    // Hide the search bar
+                    if (mSearchDropTargetBar != null) {
+                        mSearchDropTargetBar.hideSearchBar(false);
+                    }
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    animationCancelled = true;
+                }
+            });
+
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
+
+            boolean delayAnim = false;
+
+            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
+            dispatchOnLauncherTransitionPrepare(toView, animated, false);
+
+            // If any of the objects being animated haven't been measured/laid out
+            // yet, delay the animation until we get a layout pass
+            if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) ||
+                    (mWorkspace.getMeasuredWidth() == 0) ||
+                    (toView.getMeasuredWidth() == 0)) {
+                delayAnim = true;
+            }
+
+            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;
+                    setPivotsForZoom(toView, scale);
+                    dispatchOnLauncherTransitionStart(fromView, animated, false);
+                    dispatchOnLauncherTransitionStart(toView, animated, false);
+                    LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView);
+                }
+            };
+            if (delayAnim) {
+                final ViewTreeObserver observer = toView.getViewTreeObserver();
+                observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+                        public void onGlobalLayout() {
+                            startAnimRunnable.run();
+                            toView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        }
+                    });
+            } else {
+                startAnimRunnable.run();
+            }
+        } 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 && !LauncherApplication.isScreenLarge()) {
+                // Hide the workspace scrollbar
+                mWorkspace.hideScrollingIndicator(true);
+                hideDockDivider();
+
+                // 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);
+            updateWallpaperVisibility(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(State toState, final boolean animated,
+            final boolean springLoaded, final Runnable onCompleteRunnable) {
+
+        if (mStateAnimation != null) {
+            mStateAnimation.setDuration(0);
+            mStateAnimation.cancel();
+            mStateAnimation = null;
+        }
+        Resources res = getResources();
+
+        final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime);
+        final int fadeOutDuration =
+                res.getInteger(R.integer.config_appsCustomizeFadeOutTime);
+        final float scaleFactor = (float)
+                res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor);
+        final View fromView = mAppsCustomizeTabHost;
+        final View toView = mWorkspace;
+        Animator workspaceAnim = null;
+
+        if (toState == State.WORKSPACE) {
+            int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger);
+            workspaceAnim = mWorkspace.getChangeStateAnimation(
+                    Workspace.State.NORMAL, animated, stagger);
+        } else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) {
+            workspaceAnim = mWorkspace.getChangeStateAnimation(
+                    Workspace.State.SPRING_LOADED, animated);
+        }
+
+        setPivotsForZoom(fromView, scaleFactor);
+        updateWallpaperVisibility(true);
+        showHotseat(animated);
+        if (animated) {
+            final LauncherViewPropertyAnimator scaleAnim =
+                    new LauncherViewPropertyAnimator(fromView);
+            scaleAnim.
+                scaleX(scaleFactor).scaleY(scaleFactor).
+                setDuration(duration).
+                setInterpolator(new Workspace.ZoomInInterpolator());
+
+            final ObjectAnimator alphaAnim = LauncherAnimUtils
+                .ofFloat(fromView, "alpha", 1f, 0f)
+                .setDuration(fadeOutDuration);
+            alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator());
+            alphaAnim.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    float t = 1f - (Float) animation.getAnimatedValue();
+                    dispatchOnLauncherTransitionStep(fromView, t);
+                    dispatchOnLauncherTransitionStep(toView, t);
+                }
+            });
+
+            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
+
+            dispatchOnLauncherTransitionPrepare(fromView, animated, true);
+            dispatchOnLauncherTransitionPrepare(toView, animated, true);
+            mAppsCustomizeContent.pauseScrolling();
+
+            mStateAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    updateWallpaperVisibility(true);
+                    fromView.setVisibility(View.GONE);
+                    dispatchOnLauncherTransitionEnd(fromView, animated, true);
+                    dispatchOnLauncherTransitionEnd(toView, animated, true);
+                    if (mWorkspace != null) {
+                        mWorkspace.hideScrollingIndicator(false);
+                    }
+                    if (onCompleteRunnable != null) {
+                        onCompleteRunnable.run();
+                    }
+                    mAppsCustomizeContent.updateCurrentPageScroll();
+                    mAppsCustomizeContent.resumeScrolling();
+                }
+            });
+
+            mStateAnimation.playTogether(scaleAnim, alphaAnim);
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
+            dispatchOnLauncherTransitionStart(fromView, animated, true);
+            dispatchOnLauncherTransitionStart(toView, animated, true);
+            LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView);
+        } 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);
+            mWorkspace.hideScrollingIndicator(false);
+        }
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+            mAppsCustomizeTabHost.onTrimMemory();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        if (!hasFocus) {
+            // When another window occludes launcher (like the notification shade, or recents),
+            // ensure that we enable the wallpaper flag so that transitions are done correctly.
+            updateWallpaperVisibility(true);
+        } else {
+            // When launcher has focus again, disable the wallpaper if we are in AllApps
+            mWorkspace.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    disableWallpaperIfInAllApps();
+                }
+            }, 500);
+        }
+    }
+
+    void showWorkspace(boolean animated) {
+        showWorkspace(animated, null);
+    }
+
+    void showWorkspace(boolean animated, Runnable onCompleteRunnable) {
+        if (mState != State.WORKSPACE) {
+            boolean wasInSpringLoadedMode = (mState == State.APPS_CUSTOMIZE_SPRING_LOADED);
+            mWorkspace.setVisibility(View.VISIBLE);
+            hideAppsCustomizeHelper(State.WORKSPACE, animated, false, onCompleteRunnable);
+
+            // Show the search bar (only animate if we were showing the drop target bar in spring
+            // loaded mode)
+            if (mSearchDropTargetBar != null) {
+                mSearchDropTargetBar.showSearchBar(wasInSpringLoadedMode);
+            }
+
+            // We only need to animate in the dock divider if we're going from spring loaded mode
+            showDockDivider(animated && wasInSpringLoadedMode);
+
+            // Set focus to the AppsCustomize button
+            if (mAllAppsButton != null) {
+                mAllAppsButton.requestFocus();
+            }
+        }
+
+        mWorkspace.flashScrollingIndicator(animated);
+
+        // Change the state *after* we've called all the transition code
+        mState = State.WORKSPACE;
+
+        // Resume the auto-advance of widgets
+        mUserPresent = true;
+        updateRunning();
+
+        // Send an accessibility event to announce the context change
+        getWindow().getDecorView()
+                .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    void showAllApps(boolean animated) {
+        if (mState != State.WORKSPACE) return;
+
+        showAppsCustomizeHelper(animated, false);
+        mAppsCustomizeTabHost.requestFocus();
+
+        // Change the state *after* we've called all the transition code
+        mState = State.APPS_CUSTOMIZE;
+
+        // Pause the auto-advance of widgets until we are out of AllApps
+        mUserPresent = false;
+        updateRunning();
+        closeFolder();
+
+        // Send an accessibility event to announce the context change
+        getWindow().getDecorView()
+                .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    void enterSpringLoadedDragMode() {
+        if (isAllAppsVisible()) {
+            hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true, null);
+            hideDockDivider();
+            mState = State.APPS_CUSTOMIZE_SPRING_LOADED;
+        }
+    }
+
+    void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, boolean extendedDelay,
+            final Runnable onCompleteRunnable) {
+        if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return;
+
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                if (successfulDrop) {
+                    // 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);
+                    showWorkspace(true, onCompleteRunnable);
+                } else {
+                    exitSpringLoadedDragMode();
+                }
+            }
+        }, (extendedDelay ?
+                EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT :
+                EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT));
+    }
+
+    void exitSpringLoadedDragMode() {
+        if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) {
+            final boolean animated = true;
+            final boolean springLoaded = true;
+            showAppsCustomizeHelper(animated, springLoaded);
+            mState = State.APPS_CUSTOMIZE;
+        }
+        // Otherwise, we are not in spring loaded mode, so don't do anything.
+    }
+
+    void hideDockDivider() {
+        if (mQsbDivider != null && mDockDivider != null) {
+            mQsbDivider.setVisibility(View.INVISIBLE);
+            mDockDivider.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    void showDockDivider(boolean animated) {
+        if (mQsbDivider != null && mDockDivider != null) {
+            mQsbDivider.setVisibility(View.VISIBLE);
+            mDockDivider.setVisibility(View.VISIBLE);
+            if (mDividerAnimator != null) {
+                mDividerAnimator.cancel();
+                mQsbDivider.setAlpha(1f);
+                mDockDivider.setAlpha(1f);
+                mDividerAnimator = null;
+            }
+            if (animated) {
+                mDividerAnimator = LauncherAnimUtils.createAnimatorSet();
+                mDividerAnimator.playTogether(LauncherAnimUtils.ofFloat(mQsbDivider, "alpha", 1f),
+                        LauncherAnimUtils.ofFloat(mDockDivider, "alpha", 1f));
+                int duration = 0;
+                if (mSearchDropTargetBar != null) {
+                    duration = mSearchDropTargetBar.getTransitionInDuration();
+                }
+                mDividerAnimator.setDuration(duration);
+                mDividerAnimator.start();
+            }
+        }
+    }
+
+    void lockAllApps() {
+        // TODO
+    }
+
+    void unlockAllApps() {
+        // TODO
+    }
+
+    /**
+     * Shows the hotseat area.
+     */
+    void showHotseat(boolean animated) {
+        if (!LauncherApplication.isScreenLarge()) {
+            if (animated) {
+                if (mHotseat.getAlpha() != 1f) {
+                    int duration = 0;
+                    if (mSearchDropTargetBar != null) {
+                        duration = mSearchDropTargetBar.getTransitionInDuration();
+                    }
+                    mHotseat.animate().alpha(1f).setDuration(duration);
+                }
+            } else {
+                mHotseat.setAlpha(1f);
+            }
+        }
+    }
+
+    /**
+     * Hides the hotseat area.
+     */
+    void hideHotseat(boolean animated) {
+        if (!LauncherApplication.isScreenLarge()) {
+            if (animated) {
+                if (mHotseat.getAlpha() != 0f) {
+                    int duration = 0;
+                    if (mSearchDropTargetBar != null) {
+                        duration = mSearchDropTargetBar.getTransitionOutDuration();
+                    }
+                    mHotseat.animate().alpha(0f).setDuration(duration);
+                }
+            } else {
+                mHotseat.setAlpha(0f);
+            }
+        }
+    }
+
+    /**
+     * Add an item from all apps or customize onto the given workspace screen.
+     * If layout is null, add to the current screen.
+     */
+    void addExternalItemToScreen(ItemInfo itemInfo, final CellLayout layout) {
+        if (!mWorkspace.addExternalItemToScreen(itemInfo, layout)) {
+            showOutOfSpaceMessage(isHotseatLayout(layout));
+        }
+    }
+
+    /** Maps the current orientation to an index for referencing orientation correct global icons */
+    private int getCurrentOrientationIndexForGlobalIcons() {
+        // default - 0, landscape - 1
+        switch (getResources().getConfiguration().orientation) {
+        case Configuration.ORIENTATION_LANDSCAPE:
+            return 1;
+        default:
+            return 0;
+        }
+    }
+
+    private Drawable getExternalPackageToolbarIcon(ComponentName activityName, String resourceName) {
+        try {
+            PackageManager packageManager = getPackageManager();
+            // Look for the toolbar icon specified in the activity meta-data
+            Bundle metaData = packageManager.getActivityInfo(
+                    activityName, PackageManager.GET_META_DATA).metaData;
+            if (metaData != null) {
+                int iconResId = metaData.getInt(resourceName);
+                if (iconResId != 0) {
+                    Resources res = packageManager.getResourcesForActivity(activityName);
+                    return res.getDrawable(iconResId);
+                }
+            }
+        } catch (NameNotFoundException e) {
+            // This can happen if the activity defines an invalid drawable
+            Log.w(TAG, "Failed to load toolbar icon; " + activityName.flattenToShortString() +
+                    " not found", e);
+        } catch (Resources.NotFoundException nfe) {
+            // This can happen if the activity defines an invalid drawable
+            Log.w(TAG, "Failed to load toolbar icon from " + activityName.flattenToShortString(),
+                    nfe);
+        }
+        return null;
+    }
+
+    // if successful in getting icon, return it; otherwise, set button to use default drawable
+    private Drawable.ConstantState updateTextButtonWithIconFromExternalActivity(
+            int buttonId, ComponentName activityName, int fallbackDrawableId,
+            String toolbarResourceName) {
+        Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName);
+        Resources r = getResources();
+        int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width);
+        int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height);
+
+        TextView button = (TextView) findViewById(buttonId);
+        // If we were unable to find the icon via the meta-data, use a generic one
+        if (toolbarIcon == null) {
+            toolbarIcon = r.getDrawable(fallbackDrawableId);
+            toolbarIcon.setBounds(0, 0, w, h);
+            if (button != null) {
+                button.setCompoundDrawables(toolbarIcon, null, null, null);
+            }
+            return null;
+        } else {
+            toolbarIcon.setBounds(0, 0, w, h);
+            if (button != null) {
+                button.setCompoundDrawables(toolbarIcon, null, null, null);
+            }
+            return toolbarIcon.getConstantState();
+        }
+    }
+
+    // if successful in getting icon, return it; otherwise, set button to use default drawable
+    private Drawable.ConstantState updateButtonWithIconFromExternalActivity(
+            int buttonId, ComponentName activityName, int fallbackDrawableId,
+            String toolbarResourceName) {
+        ImageView button = (ImageView) findViewById(buttonId);
+        Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName);
+
+        if (button != null) {
+            // If we were unable to find the icon via the meta-data, use a
+            // generic one
+            if (toolbarIcon == null) {
+                button.setImageResource(fallbackDrawableId);
+            } else {
+                button.setImageDrawable(toolbarIcon);
+            }
+        }
+
+        return toolbarIcon != null ? toolbarIcon.getConstantState() : null;
+
+    }
+
+    private void updateTextButtonWithDrawable(int buttonId, Drawable d) {
+        TextView button = (TextView) findViewById(buttonId);
+        button.setCompoundDrawables(d, null, null, null);
+    }
+
+    private void updateButtonWithDrawable(int buttonId, Drawable.ConstantState d) {
+        ImageView button = (ImageView) findViewById(buttonId);
+        button.setImageDrawable(d.newDrawable(getResources()));
+    }
+
+    private void invalidatePressedFocusedStates(View container, View button) {
+        if (container instanceof HolographicLinearLayout) {
+            HolographicLinearLayout layout = (HolographicLinearLayout) container;
+            layout.invalidatePressedFocusedStates();
+        } else if (button instanceof HolographicImageView) {
+            HolographicImageView view = (HolographicImageView) button;
+            view.invalidatePressedFocusedStates();
+        }
+    }
+
+    private boolean updateGlobalSearchIcon() {
+        final View searchButtonContainer = findViewById(R.id.search_button_container);
+        final ImageView searchButton = (ImageView) findViewById(R.id.search_button);
+        final View voiceButtonContainer = findViewById(R.id.voice_button_container);
+        final View voiceButton = findViewById(R.id.voice_button);
+        final View voiceButtonProxy = findViewById(R.id.voice_button_proxy);
+
+        final SearchManager searchManager =
+                (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+        ComponentName activityName = searchManager.getGlobalSearchActivity();
+        if (activityName != null) {
+            int coi = getCurrentOrientationIndexForGlobalIcons();
+            sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity(
+                    R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo,
+                    TOOLBAR_SEARCH_ICON_METADATA_NAME);
+            if (sGlobalSearchIcon[coi] == null) {
+                sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity(
+                        R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo,
+                        TOOLBAR_ICON_METADATA_NAME);
+            }
+
+            if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.VISIBLE);
+            searchButton.setVisibility(View.VISIBLE);
+            invalidatePressedFocusedStates(searchButtonContainer, searchButton);
+            return true;
+        } else {
+            // We disable both search and voice search when there is no global search provider
+            if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.GONE);
+            if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE);
+            searchButton.setVisibility(View.GONE);
+            voiceButton.setVisibility(View.GONE);
+            if (voiceButtonProxy != null) {
+                voiceButtonProxy.setVisibility(View.GONE);
+            }
+            return false;
+        }
+    }
+
+    private void updateGlobalSearchIcon(Drawable.ConstantState d) {
+        final View searchButtonContainer = findViewById(R.id.search_button_container);
+        final View searchButton = (ImageView) findViewById(R.id.search_button);
+        updateButtonWithDrawable(R.id.search_button, d);
+        invalidatePressedFocusedStates(searchButtonContainer, searchButton);
+    }
+
+    private boolean updateVoiceSearchIcon(boolean searchVisible) {
+        final View voiceButtonContainer = findViewById(R.id.voice_button_container);
+        final View voiceButton = findViewById(R.id.voice_button);
+        final View voiceButtonProxy = findViewById(R.id.voice_button_proxy);
+
+        // We only show/update the voice search icon if the search icon is enabled as well
+        final SearchManager searchManager =
+                (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+        ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity();
+
+        ComponentName activityName = null;
+        if (globalSearchActivity != null) {
+            // Check if the global search activity handles voice search
+            Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+            intent.setPackage(globalSearchActivity.getPackageName());
+            activityName = intent.resolveActivity(getPackageManager());
+        }
+
+        if (activityName == null) {
+            // Fallback: check if an activity other than the global search activity
+            // resolves this
+            Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+            activityName = intent.resolveActivity(getPackageManager());
+        }
+        if (searchVisible && activityName != null) {
+            int coi = getCurrentOrientationIndexForGlobalIcons();
+            sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity(
+                    R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo,
+                    TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME);
+            if (sVoiceSearchIcon[coi] == null) {
+                sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity(
+                        R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo,
+                        TOOLBAR_ICON_METADATA_NAME);
+            }
+            if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.VISIBLE);
+            voiceButton.setVisibility(View.VISIBLE);
+            if (voiceButtonProxy != null) {
+                voiceButtonProxy.setVisibility(View.VISIBLE);
+            }
+            invalidatePressedFocusedStates(voiceButtonContainer, voiceButton);
+            return true;
+        } else {
+            if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE);
+            voiceButton.setVisibility(View.GONE);
+            if (voiceButtonProxy != null) {
+                voiceButtonProxy.setVisibility(View.GONE);
+            }
+            return false;
+        }
+    }
+
+    private void updateVoiceSearchIcon(Drawable.ConstantState d) {
+        final View voiceButtonContainer = findViewById(R.id.voice_button_container);
+        final View voiceButton = findViewById(R.id.voice_button);
+        updateButtonWithDrawable(R.id.voice_button, d);
+        invalidatePressedFocusedStates(voiceButtonContainer, voiceButton);
+    }
+
+    /**
+     * Sets the app market icon
+     */
+    private void updateAppMarketIcon() {
+        final View marketButton = findViewById(R.id.market_button);
+        Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET);
+        // Find the app market activity by resolving an intent.
+        // (If multiple app markets are installed, it will return the ResolverActivity.)
+        ComponentName activityName = intent.resolveActivity(getPackageManager());
+        if (activityName != null) {
+            int coi = getCurrentOrientationIndexForGlobalIcons();
+            mAppMarketIntent = intent;
+            sAppMarketIcon[coi] = updateTextButtonWithIconFromExternalActivity(
+                    R.id.market_button, activityName, R.drawable.ic_launcher_market_holo,
+                    TOOLBAR_ICON_METADATA_NAME);
+            marketButton.setVisibility(View.VISIBLE);
+        } else {
+            // We should hide and disable the view so that we don't try and restore the visibility
+            // of it when we swap between drag & normal states from IconDropTarget subclasses.
+            marketButton.setVisibility(View.GONE);
+            marketButton.setEnabled(false);
+        }
+    }
+
+    private void updateAppMarketIcon(Drawable.ConstantState d) {
+        // Ensure that the new drawable we are creating has the approprate toolbar icon bounds
+        Resources r = getResources();
+        Drawable marketIconDrawable = d.newDrawable(r);
+        int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width);
+        int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height);
+        marketIconDrawable.setBounds(0, 0, w, h);
+
+        updateTextButtonWithDrawable(R.id.market_button, marketIconDrawable);
+    }
+
+    @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(getString(R.string.all_apps_button_label));
+        } else {
+            text.add(getString(R.string.all_apps_home_button_label));
+        }
+        return result;
+    }
+
+    /**
+     * Receives notifications when system dialogs are to be closed.
+     */
+    private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            closeSystemDialogs();
+        }
+    }
+
+    /**
+     * 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.
+     *
+     * 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.
+     */
+    private boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) {
+        if (mPaused) {
+            Log.i(TAG, "Deferring update until onResume");
+            if (deletePreviousRunnables) {
+                while (mOnResumeCallbacks.remove(run)) {
+                }
+            }
+            mOnResumeCallbacks.add(run);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean waitUntilResume(Runnable run) {
+        return waitUntilResume(run, false);
+    }
+
+    /**
+     * If the activity is currently paused, signal that we need to re-run the loader
+     * 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.
+     *
+     * 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.
+     */
+    public boolean setLoadOnResume() {
+        if (mPaused) {
+            Log.i(TAG, "setLoadOnResume");
+            mOnResumeNeedsLoad = true;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public int getCurrentWorkspaceScreen() {
+        if (mWorkspace != null) {
+            return mWorkspace.getCurrentPage();
+        } else {
+            return SCREEN_COUNT / 2;
+        }
+    }
+
+    /**
+     * Refreshes the shortcuts shown on the workspace.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void startBinding() {
+        // If we're starting binding all over again, clear any bind calls we'd postponed in
+        // the past (see waitUntilResume) -- we don't need them since we're starting binding
+        // from scratch again
+        mOnResumeCallbacks.clear();
+
+        final Workspace workspace = mWorkspace;
+        mNewShortcutAnimatePage = -1;
+        mNewShortcutAnimateViews.clear();
+        mWorkspace.clearDropTargets();
+        int count = workspace.getChildCount();
+        for (int i = 0; i < count; i++) {
+            // Use removeAllViewsInLayout() to avoid an extra requestLayout() and invalidate().
+            final CellLayout layoutParent = (CellLayout) workspace.getChildAt(i);
+            layoutParent.removeAllViewsInLayout();
+        }
+        mWidgetsToAdvance.clear();
+        if (mHotseat != null) {
+            mHotseat.resetLayout();
+        }
+    }
+
+    /**
+     * Bind the items start-end from the list.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindItems(final ArrayList<ItemInfo> shortcuts, final int start, final int end) {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    bindItems(shortcuts, start, end);
+                }
+            })) {
+            return;
+        }
+
+        // Get the list of added shortcuts and intersect them with the set of shortcuts here
+        Set<String> newApps = new HashSet<String>();
+        newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps);
+
+        Workspace workspace = mWorkspace;
+        for (int i = start; i < end; i++) {
+            final ItemInfo item = shortcuts.get(i);
+
+            // Short circuit if we are loading dock items for a configuration which has no dock
+            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
+                    mHotseat == null) {
+                continue;
+            }
+
+            switch (item.itemType) {
+                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                    ShortcutInfo info = (ShortcutInfo) item;
+                    String uri = info.intent.toUri(0).toString();
+                    View shortcut = createShortcut(info);
+                    workspace.addInScreen(shortcut, item.container, item.screen, item.cellX,
+                            item.cellY, 1, 1, false);
+                    boolean animateIconUp = false;
+                    synchronized (newApps) {
+                        if (newApps.contains(uri)) {
+                            animateIconUp = newApps.remove(uri);
+                        }
+                    }
+                    if (animateIconUp) {
+                        // Prepare the view to be animated up
+                        shortcut.setAlpha(0f);
+                        shortcut.setScaleX(0f);
+                        shortcut.setScaleY(0f);
+                        mNewShortcutAnimatePage = item.screen;
+                        if (!mNewShortcutAnimateViews.contains(shortcut)) {
+                            mNewShortcutAnimateViews.add(shortcut);
+                        }
+                    }
+                    break;
+                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                    FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this,
+                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
+                            (FolderInfo) item, mIconCache);
+                    workspace.addInScreen(newFolder, item.container, item.screen, item.cellX,
+                            item.cellY, 1, 1, false);
+                    break;
+            }
+        }
+
+        workspace.requestLayout();
+    }
+
+    /**
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindFolders(final HashMap<Long, FolderInfo> folders) {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    bindFolders(folders);
+                }
+            })) {
+            return;
+        }
+        sFolders.clear();
+        sFolders.putAll(folders);
+    }
+
+    /**
+     * Add the views for a widget to the workspace.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindAppWidget(final LauncherAppWidgetInfo item) {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    bindAppWidget(item);
+                }
+            })) {
+            return;
+        }
+
+        final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0;
+        if (DEBUG_WIDGETS) {
+            Log.d(TAG, "bindAppWidget: " + item);
+        }
+        final Workspace workspace = mWorkspace;
+
+        final int appWidgetId = item.appWidgetId;
+        final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
+        if (DEBUG_WIDGETS) {
+            Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider);
+        }
+
+        item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
+
+        item.hostView.setTag(item);
+        item.onBindAppWidget(this);
+
+        workspace.addInScreen(item.hostView, item.container, item.screen, item.cellX,
+                item.cellY, item.spanX, item.spanY, false);
+        addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo);
+
+        workspace.requestLayout();
+
+        if (DEBUG_WIDGETS) {
+            Log.d(TAG, "bound widget id="+item.appWidgetId+" in "
+                    + (SystemClock.uptimeMillis()-start) + "ms");
+        }
+    }
+
+    public void onPageBoundSynchronously(int page) {
+        mSynchronouslyBoundPages.add(page);
+    }
+
+    /**
+     * Callback saying that there aren't any more items to bind.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void finishBindingItems() {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    finishBindingItems();
+                }
+            })) {
+            return;
+        }
+        if (mSavedState != null) {
+            if (!mWorkspace.hasFocus()) {
+                mWorkspace.getChildAt(mWorkspace.getCurrentPage()).requestFocus();
+            }
+            mSavedState = null;
+        }
+
+        mWorkspace.restoreInstanceStateForRemainingPages();
+
+        // If we received the result of any pending adds while the loader was running (e.g. the
+        // widget configuration forced an orientation change), process them now.
+        for (int i = 0; i < sPendingAddList.size(); i++) {
+            completeAdd(sPendingAddList.get(i));
+        }
+        sPendingAddList.clear();
+
+        // Update the market app icon as necessary (the other icons will be managed in response to
+        // package changes in bindSearchablesChanged()
+        updateAppMarketIcon();
+
+        // Animate up any icons as necessary
+        if (mVisible || mWorkspaceLoading) {
+            Runnable newAppsRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    runNewAppsAnimation(false);
+                }
+            };
+
+            boolean willSnapPage = mNewShortcutAnimatePage > -1 &&
+                    mNewShortcutAnimatePage != mWorkspace.getCurrentPage();
+            if (canRunNewAppsAnimation()) {
+                // If the user has not interacted recently, then either snap to the new page to show
+                // the new-apps animation or just run them if they are to appear on the current page
+                if (willSnapPage) {
+                    mWorkspace.snapToPage(mNewShortcutAnimatePage, newAppsRunnable);
+                } else {
+                    runNewAppsAnimation(false);
+                }
+            } else {
+                // If the user has interacted recently, then just add the items in place if they
+                // are on another page (or just normally if they are added to the current page)
+                runNewAppsAnimation(willSnapPage);
+            }
+        }
+
+        mWorkspaceLoading = false;
+    }
+
+    private boolean canRunNewAppsAnimation() {
+        long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime();
+        return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
+    }
+
+    /**
+     * Runs a new animation that scales up icons that were added while Launcher was in the
+     * background.
+     *
+     * @param immediate whether to run the animation or show the results immediately
+     */
+    private void runNewAppsAnimation(boolean immediate) {
+        AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
+        Collection<Animator> bounceAnims = new ArrayList<Animator>();
+
+        // Order these new views spatially so that they animate in order
+        Collections.sort(mNewShortcutAnimateViews, new Comparator<View>() {
+            @Override
+            public int compare(View a, View b) {
+                CellLayout.LayoutParams alp = (CellLayout.LayoutParams) a.getLayoutParams();
+                CellLayout.LayoutParams blp = (CellLayout.LayoutParams) b.getLayoutParams();
+                int cellCountX = LauncherModel.getCellCountX();
+                return (alp.cellY * cellCountX + alp.cellX) - (blp.cellY * cellCountX + blp.cellX);
+            }
+        });
+
+        // Animate each of the views in place (or show them immediately if requested)
+        if (immediate) {
+            for (View v : mNewShortcutAnimateViews) {
+                v.setAlpha(1f);
+                v.setScaleX(1f);
+                v.setScaleY(1f);
+            }
+        } else {
+            for (int i = 0; i < mNewShortcutAnimateViews.size(); ++i) {
+                View v = mNewShortcutAnimateViews.get(i);
+                ValueAnimator bounceAnim = LauncherAnimUtils.ofPropertyValuesHolder(v,
+                        PropertyValuesHolder.ofFloat("alpha", 1f),
+                        PropertyValuesHolder.ofFloat("scaleX", 1f),
+                        PropertyValuesHolder.ofFloat("scaleY", 1f));
+                bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION);
+                bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY);
+                bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator());
+                bounceAnims.add(bounceAnim);
+            }
+            anim.playTogether(bounceAnims);
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (mWorkspace != null) {
+                        mWorkspace.postDelayed(mBuildLayersRunnable, 500);
+                    }
+                }
+            });
+            anim.start();
+        }
+
+        // Clean up
+        mNewShortcutAnimatePage = -1;
+        mNewShortcutAnimateViews.clear();
+        new Thread("clearNewAppsThread") {
+            public void run() {
+                mSharedPrefs.edit()
+                            .putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1)
+                            .putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, null)
+                            .commit();
+            }
+        }.start();
+    }
+
+    @Override
+    public void bindSearchablesChanged() {
+        boolean searchVisible = updateGlobalSearchIcon();
+        boolean voiceVisible = updateVoiceSearchIcon(searchVisible);
+        if (mSearchDropTargetBar != null) {
+            mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible);
+        }
+    }
+
+    /**
+     * Add the icons for all apps.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindAllApplications(final ArrayList<ApplicationInfo> apps) {
+        Runnable setAllAppsRunnable = new Runnable() {
+            public void run() {
+                if (mAppsCustomizeContent != null) {
+                    mAppsCustomizeContent.setApps(apps);
+                }
+            }
+        };
+
+        // Remove the progress bar entirely; we could also make it GONE
+        // but better to remove it since we know it's not going to be used
+        View progressBar = mAppsCustomizeTabHost.
+            findViewById(R.id.apps_customize_progress_bar);
+        if (progressBar != null) {
+            ((ViewGroup)progressBar.getParent()).removeView(progressBar);
+
+            // We just post the call to setApps so the user sees the progress bar
+            // disappear-- otherwise, it just looks like the progress bar froze
+            // which doesn't look great
+            mAppsCustomizeTabHost.post(setAllAppsRunnable);
+        } else {
+            // If we did not initialize the spinner in onCreate, then we can directly set the
+            // list of applications without waiting for any progress bars views to be hidden.
+            setAllAppsRunnable.run();
+        }
+    }
+
+    /**
+     * A package was installed.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindAppsAdded(final ArrayList<ApplicationInfo> apps) {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    bindAppsAdded(apps);
+                }
+            })) {
+            return;
+        }
+
+
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.addApps(apps);
+        }
+    }
+
+    /**
+     * A package was updated.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindAppsUpdated(final ArrayList<ApplicationInfo> apps) {
+        if (waitUntilResume(new Runnable() {
+                public void run() {
+                    bindAppsUpdated(apps);
+                }
+            })) {
+            return;
+        }
+
+        if (mWorkspace != null) {
+            mWorkspace.updateShortcuts(apps);
+        }
+
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.updateApps(apps);
+        }
+    }
+
+    /**
+     * A package was uninstalled.  We take both the super set of packageNames
+     * in addition to specific applications to remove, the reason being that
+     * this can be called when a package is updated as well.  In that scenario,
+     * we only remove specific components from the workspace, where as
+     * package-removal should clear all items by package name.
+     *
+     * Implementation of the method from LauncherModel.Callbacks.
+     */
+    public void bindComponentsRemoved(final ArrayList<String> packageNames,
+                                      final ArrayList<ApplicationInfo> appInfos,
+                                      final boolean matchPackageNamesOnly) {
+        if (waitUntilResume(new Runnable() {
+            public void run() {
+                bindComponentsRemoved(packageNames, appInfos, matchPackageNamesOnly);
+            }
+        })) {
+            return;
+        }
+
+        if (matchPackageNamesOnly) {
+            mWorkspace.removeItemsByPackageName(packageNames);
+        } else {
+            mWorkspace.removeItemsByApplicationInfo(appInfos);
+        }
+
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.removeApps(appInfos);
+        }
+
+        // Notify the drag controller
+        mDragController.onAppsRemoved(appInfos, this);
+    }
+
+    /**
+     * A number of packages were updated.
+     */
+
+    private ArrayList<Object> mWidgetsAndShortcuts;
+    private Runnable mBindPackagesUpdatedRunnable = new Runnable() {
+            public void run() {
+                bindPackagesUpdated(mWidgetsAndShortcuts);
+                mWidgetsAndShortcuts = null;
+            }
+        };
+
+    public void bindPackagesUpdated(final ArrayList<Object> widgetsAndShortcuts) {
+        if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) {
+            mWidgetsAndShortcuts = widgetsAndShortcuts;
+            return;
+        }
+
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts);
+        }
+    }
+
+    private int mapConfigurationOriActivityInfoOri(int configOri) {
+        final Display d = getWindowManager().getDefaultDisplay();
+        int naturalOri = Configuration.ORIENTATION_LANDSCAPE;
+        switch (d.getRotation()) {
+        case Surface.ROTATION_0:
+        case Surface.ROTATION_180:
+            // We are currently in the same basic orientation as the natural orientation
+            naturalOri = configOri;
+            break;
+        case Surface.ROTATION_90:
+        case Surface.ROTATION_270:
+            // We are currently in the other basic orientation to the natural orientation
+            naturalOri = (configOri == Configuration.ORIENTATION_LANDSCAPE) ?
+                    Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
+            break;
+        }
+
+        int[] oriMap = {
+                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT,
+                ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
+                ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT,
+                ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+        };
+        // Since the map starts at portrait, we need to offset if this device's natural orientation
+        // is landscape.
+        int indexOffset = 0;
+        if (naturalOri == Configuration.ORIENTATION_LANDSCAPE) {
+            indexOffset = 1;
+        }
+        return oriMap[(d.getRotation() + indexOffset) % 4];
+    }
+
+    public boolean isRotationEnabled() {
+        boolean enableRotation = sForceEnableRotation ||
+                getResources().getBoolean(R.bool.allow_rotation);
+        return enableRotation;
+    }
+    public void lockScreenOrientation() {
+        if (isRotationEnabled()) {
+            setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources()
+                    .getConfiguration().orientation));
+        }
+    }
+    public void unlockScreenOrientation(boolean immediate) {
+        if (isRotationEnabled()) {
+            if (immediate) {
+                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            } else {
+                mHandler.postDelayed(new Runnable() {
+                    public void run() {
+                        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+                    }
+                }, mRestoreScreenOrientationDelay);
+            }
+        }
+    }
+
+    /* Cling related */
+    private boolean isClingsEnabled() {
+        // disable clings when running in a test harness
+        if(ActivityManager.isRunningInTestHarness()) return false;
+
+        // Restricted secondary users (child mode) will potentially have very few apps
+        // seeded when they start up for the first time. Clings won't work well with that
+//        boolean supportsLimitedUsers =
+//                android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+//        Account[] accounts = AccountManager.get(this).getAccounts();
+//        if (supportsLimitedUsers && accounts.length == 0) {
+//            UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
+//            Bundle restrictions = um.getUserRestrictions();
+//            if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) {
+//               return false;
+//            }
+//        }
+        return true;
+    }
+
+    private Cling initCling(int clingId, int[] positionData, boolean animate, int delay) {
+        final Cling cling = (Cling) findViewById(clingId);
+        if (cling != null) {
+            cling.init(this, positionData);
+            cling.setVisibility(View.VISIBLE);
+            cling.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            if (animate) {
+                cling.buildLayer();
+                cling.setAlpha(0f);
+                cling.animate()
+                    .alpha(1f)
+                    .setInterpolator(new AccelerateInterpolator())
+                    .setDuration(SHOW_CLING_DURATION)
+                    .setStartDelay(delay)
+                    .start();
+            } else {
+                cling.setAlpha(1f);
+            }
+            cling.setFocusableInTouchMode(true);
+            cling.post(new Runnable() {
+                public void run() {
+                    cling.setFocusable(true);
+                    cling.requestFocus();
+                }
+            });
+            mHideFromAccessibilityHelper.setImportantForAccessibilityToNo(
+                    mDragLayer, clingId == R.id.all_apps_cling);
+        }
+        return cling;
+    }
+
+    private void dismissCling(final Cling cling, 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.
+        if (cling != null && cling.getVisibility() != View.GONE) {
+            ObjectAnimator anim = LauncherAnimUtils.ofFloat(cling, "alpha", 0f);
+            anim.setDuration(duration);
+            anim.addListener(new AnimatorListenerAdapter() {
+                public void onAnimationEnd(Animator animation) {
+                    cling.setVisibility(View.GONE);
+                    cling.cleanup();
+                    // We should update the shared preferences on a background thread
+                    new Thread("dismissClingThread") {
+                        public void run() {
+                            SharedPreferences.Editor editor = mSharedPrefs.edit();
+                            editor.putBoolean(flag, true);
+                            editor.commit();
+                        }
+                    }.start();
+                };
+            });
+            anim.start();
+            mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer);
+        }
+    }
+
+    private void removeCling(int id) {
+        final View cling = findViewById(id);
+        if (cling != null) {
+            final ViewGroup parent = (ViewGroup) cling.getParent();
+            parent.post(new Runnable() {
+                @Override
+                public void run() {
+                    parent.removeView(cling);
+                }
+            });
+            mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer);
+        }
+    }
+
+    private boolean skipCustomClingIfNoAccounts() {
+        Cling cling = (Cling) findViewById(R.id.workspace_cling);
+        boolean customCling = cling.getDrawIdentifier().equals("workspace_custom");
+        if (customCling) {
+            AccountManager am = AccountManager.get(this);
+            if (am == null) return false;
+            Account[] accounts = am.getAccountsByType("com.google");
+            return accounts.length == 0;
+        }
+        return false;
+    }
+
+    public void showFirstRunWorkspaceCling() {
+        // Enable the clings only if they have not been dismissed before
+        if (isClingsEnabled() &&
+                !mSharedPrefs.getBoolean(Cling.WORKSPACE_CLING_DISMISSED_KEY, false) &&
+                !skipCustomClingIfNoAccounts() ) {
+            // If we're not using the default workspace layout, replace workspace cling
+            // with a custom workspace cling (usually specified in an overlay)
+            // For now, only do this on tablets
+            if (mSharedPrefs.getInt(LauncherProvider.DEFAULT_WORKSPACE_RESOURCE_ID, 0) != 0 &&
+                    getResources().getBoolean(R.bool.config_useCustomClings)) {
+                // Use a custom cling
+                View cling = findViewById(R.id.workspace_cling);
+                ViewGroup clingParent = (ViewGroup) cling.getParent();
+                int clingIndex = clingParent.indexOfChild(cling);
+                clingParent.removeViewAt(clingIndex);
+                View customCling = mInflater.inflate(R.layout.custom_workspace_cling, clingParent, false);
+                clingParent.addView(customCling, clingIndex);
+                customCling.setId(R.id.workspace_cling);
+            }
+            initCling(R.id.workspace_cling, null, false, 0);
+        } else {
+            removeCling(R.id.workspace_cling);
+        }
+    }
+    public void showFirstRunAllAppsCling(int[] position) {
+        // Enable the clings only if they have not been dismissed before
+        if (isClingsEnabled() &&
+                !mSharedPrefs.getBoolean(Cling.ALLAPPS_CLING_DISMISSED_KEY, false)) {
+            initCling(R.id.all_apps_cling, position, true, 0);
+        } else {
+            removeCling(R.id.all_apps_cling);
+        }
+    }
+    public Cling showFirstRunFoldersCling() {
+        // Enable the clings only if they have not been dismissed before
+        if (isClingsEnabled() &&
+                !mSharedPrefs.getBoolean(Cling.FOLDER_CLING_DISMISSED_KEY, false)) {
+            return initCling(R.id.folder_cling, null, true, 0);
+        } else {
+            removeCling(R.id.folder_cling);
+            return null;
+        }
+    }
+    public boolean isFolderClingVisible() {
+        Cling cling = (Cling) findViewById(R.id.folder_cling);
+        if (cling != null) {
+            return cling.getVisibility() == View.VISIBLE;
+        }
+        return false;
+    }
+    public void dismissWorkspaceCling(View v) {
+        Cling cling = (Cling) findViewById(R.id.workspace_cling);
+        dismissCling(cling, Cling.WORKSPACE_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION);
+    }
+    public void dismissAllAppsCling(View v) {
+        Cling cling = (Cling) findViewById(R.id.all_apps_cling);
+        dismissCling(cling, Cling.ALLAPPS_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION);
+    }
+    public void dismissFolderCling(View v) {
+        Cling cling = (Cling) findViewById(R.id.folder_cling);
+        dismissCling(cling, Cling.FOLDER_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION);
+    }
+
+    /**
+     * Prints out out state for debugging.
+     */
+    public void dumpState() {
+        Log.d(TAG, "BEGIN launcher3 dump state for launcher " + this);
+        Log.d(TAG, "mSavedState=" + mSavedState);
+        Log.d(TAG, "mWorkspaceLoading=" + mWorkspaceLoading);
+        Log.d(TAG, "mRestoring=" + mRestoring);
+        Log.d(TAG, "mWaitingForResult=" + mWaitingForResult);
+        Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState);
+        Log.d(TAG, "sFolders.size=" + sFolders.size());
+        mModel.dumpState();
+
+        if (mAppsCustomizeContent != null) {
+            mAppsCustomizeContent.dumpState();
+        }
+        Log.d(TAG, "END launcher3 dump state");
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        super.dump(prefix, fd, writer, args);
+        writer.println(" ");
+        writer.println("Debug logs: ");
+        for (int i = 0; i < sDumpLogs.size(); i++) {
+            writer.println("  " + sDumpLogs.get(i));
+        }
+    }
+
+    public static void dumpDebugLogsToConsole() {
+        Log.d(TAG, "");
+        Log.d(TAG, "*********************");
+        Log.d(TAG, "Launcher debug logs: ");
+        for (int i = 0; i < sDumpLogs.size(); i++) {
+            Log.d(TAG, "  " + sDumpLogs.get(i));
+        }
+        Log.d(TAG, "*********************");
+        Log.d(TAG, "");
+    }
+}
+
+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);
+}
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
new file mode 100644
index 0000000..01f72a7
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -0,0 +1,129 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import java.util.HashSet;
+
+public class LauncherAnimUtils {
+    static HashSet<Animator> sAnimators = new HashSet<Animator>();
+    static Animator.AnimatorListener sEndAnimListener = new Animator.AnimatorListener() {
+        public void onAnimationStart(Animator animation) {
+        }
+
+        public void onAnimationRepeat(Animator animation) {
+        }
+
+        public void onAnimationEnd(Animator animation) {
+            sAnimators.remove(animation);
+        }
+
+        public void onAnimationCancel(Animator animation) {
+            sAnimators.remove(animation);
+        }
+    };
+
+    public static void cancelOnDestroyActivity(Animator a) {
+        sAnimators.add(a);
+        a.addListener(sEndAnimListener);
+    }
+
+    // Helper method. Assumes a draw is pending, and that if the animation's duration is 0
+    // it should be cancelled
+    public static void startAnimationAfterNextDraw(final Animator animator, final View view) {
+        view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
+                private boolean mStarted = false;
+                public void onDraw() {
+                    if (mStarted) return;
+                    mStarted = true;
+                    // Use this as a signal that the animation was cancelled
+                    if (animator.getDuration() == 0) {
+                        return;
+                    }
+                    animator.start();
+
+                    final ViewTreeObserver.OnDrawListener listener = this;
+                    view.post(new Runnable() {
+                            public void run() {
+                                view.getViewTreeObserver().removeOnDrawListener(listener);
+                            }
+                        });
+                }
+            });
+    }
+
+    public static void onDestroyActivity() {
+        HashSet<Animator> animators = new HashSet<Animator>(sAnimators);
+        for (Animator a : animators) {
+            if (a.isRunning()) {
+                a.cancel();
+            } else {
+                sAnimators.remove(a);
+            }
+        }
+    }
+
+    public static AnimatorSet createAnimatorSet() {
+        AnimatorSet anim = new AnimatorSet();
+        cancelOnDestroyActivity(anim);
+        return anim;
+    }
+
+    public static ValueAnimator ofFloat(View target, float... values) {
+        ValueAnimator anim = new ValueAnimator();
+        anim.setFloatValues(values);
+        cancelOnDestroyActivity(anim);
+        return anim;
+    }
+
+    public static ObjectAnimator ofFloat(View target, String propertyName, float... values) {
+        ObjectAnimator anim = new ObjectAnimator();
+        anim.setTarget(target);
+        anim.setPropertyName(propertyName);
+        anim.setFloatValues(values);
+        cancelOnDestroyActivity(anim);
+        new FirstFrameAnimatorHelper(anim, target);
+        return anim;
+    }
+
+    public static ObjectAnimator ofPropertyValuesHolder(View target,
+            PropertyValuesHolder... values) {
+        ObjectAnimator anim = new ObjectAnimator();
+        anim.setTarget(target);
+        anim.setValues(values);
+        cancelOnDestroyActivity(anim);
+        new FirstFrameAnimatorHelper(anim, target);
+        return anim;
+    }
+
+    public static ObjectAnimator ofPropertyValuesHolder(Object target,
+            View view, PropertyValuesHolder... values) {
+        ObjectAnimator anim = new ObjectAnimator();
+        anim.setTarget(target);
+        anim.setValues(values);
+        cancelOnDestroyActivity(anim);
+        new FirstFrameAnimatorHelper(anim, view);
+        return anim;
+    }
+}
diff --git a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java b/src/com/android/launcher3/LauncherAnimatorUpdateListener.java
new file mode 100644
index 0000000..ec9fd4d
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAnimatorUpdateListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
new file mode 100644
index 0000000..7b08d44
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2009 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.AppWidgetHost;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+
+/**
+ * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
+ * which correctly captures all long-press events. This ensures that users can
+ * always pick up and move widgets.
+ */
+public class LauncherAppWidgetHost extends AppWidgetHost {
+
+    Launcher mLauncher;
+
+    public LauncherAppWidgetHost(Launcher launcher, int hostId) {
+        super(launcher, hostId);
+        mLauncher = launcher;
+    }
+
+    @Override
+    protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
+            AppWidgetProviderInfo appWidget) {
+        return new LauncherAppWidgetHostView(context);
+    }
+
+    @Override
+    public void stopListening() {
+        super.stopListening();
+        clearViews();
+    }
+
+    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));
+    }
+}
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
new file mode 100644
index 0000000..6157a87
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2009 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.AppWidgetHostView;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+import com.android.launcher3.R;
+
+/**
+ * {@inheritDoc}
+ */
+public class LauncherAppWidgetHostView extends AppWidgetHostView {
+    private CheckLongPressHelper mLongPressHelper;
+    private LayoutInflater mInflater;
+    private Context mContext;
+    private int mPreviousOrientation;
+
+    public LauncherAppWidgetHostView(Context context) {
+        super(context);
+        mContext = context;
+        mLongPressHelper = new CheckLongPressHelper(this);
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    @Override
+    protected View getErrorView() {
+        return mInflater.inflate(R.layout.appwidget_error, this, false);
+    }
+
+    @Override
+    public void updateAppWidget(RemoteViews remoteViews) {
+        // Store the orientation in which the widget was inflated
+        mPreviousOrientation = mContext.getResources().getConfiguration().orientation;
+        super.updateAppWidget(remoteViews);
+    }
+
+    public boolean orientationChangedSincedInflation() {
+        int orientation = mContext.getResources().getConfiguration().orientation;
+        if (mPreviousOrientation != orientation) {
+           return true;
+       }
+       return false;
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        // Consume any touch events for ourselves after longpress is triggered
+        if (mLongPressHelper.hasPerformedLongPress()) {
+            mLongPressHelper.cancelLongPress();
+            return true;
+        }
+
+        // Watch for longpress events at this level to make sure
+        // users can always pick up this widget
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                mLongPressHelper.postCheckForLongPress();
+                break;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mLongPressHelper.cancelLongPress();
+                break;
+        }
+
+        // Otherwise continue letting touch events fall through to children
+        return false;
+    }
+
+    @Override
+    public void cancelLongPress() {
+        super.cancelLongPress();
+
+        mLongPressHelper.cancelLongPress();
+    }
+
+    @Override
+    public int getDescendantFocusability() {
+        return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
+    }
+}
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java
new file mode 100644
index 0000000..3fc67cb
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2009 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.AppWidgetHostView;
+import android.content.ComponentName;
+import android.content.ContentValues;
+
+/**
+ * Represents a widget (either instantiated or about to be) in the Launcher.
+ */
+class LauncherAppWidgetInfo extends ItemInfo {
+
+    /**
+     * Indicates that the widget hasn't been instantiated yet.
+     */
+    static final int NO_ID = -1;
+
+    /**
+     * Identifier for this widget when talking with
+     * {@link android.appwidget.AppWidgetManager} for updates.
+     */
+    int appWidgetId = NO_ID;
+
+    ComponentName providerName;
+
+    // TODO: Are these necessary here?
+    int minWidth = -1;
+    int minHeight = -1;
+
+    private boolean mHasNotifiedInitialWidgetSizeChanged;
+
+    /**
+     * View that holds this widget after it's been created.  This view isn't created
+     * until Launcher knows it's needed.
+     */
+    AppWidgetHostView hostView = null;
+
+    LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+        this.appWidgetId = appWidgetId;
+        this.providerName = providerName;
+
+        // Since the widget isn't instantiated yet, we don't know these values. Set them to -1
+        // to indicate that they should be calculated based on the layout and minWidth/minHeight
+        spanX = -1;
+        spanY = -1;
+    }
+
+    @Override
+    void onAddToDatabase(ContentValues values) {
+        super.onAddToDatabase(values);
+        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
+    }
+
+    /**
+     * When we bind the widget, we should notify the widget that the size has changed if we have not
+     * done so already (only really for default workspace widgets).
+     */
+    void onBindAppWidget(Launcher launcher) {
+        if (!mHasNotifiedInitialWidgetSizeChanged) {
+            notifyWidgetSizeChanged(launcher);
+        }
+    }
+
+    /**
+     * Trigger an update callback to the widget to notify it that its size has changed.
+     */
+    void notifyWidgetSizeChanged(Launcher launcher) {
+        AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY);
+        mHasNotifiedInitialWidgetSizeChanged = true;
+    }
+
+    @Override
+    public String toString() {
+        return "AppWidget(id=" + Integer.toString(appWidgetId) + ")";
+    }
+
+    @Override
+    void unbind() {
+        super.unbind();
+        hostView = null;
+    }
+}
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
new file mode 100644
index 0000000..45e2425
--- /dev/null
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -0,0 +1,151 @@
+/*
+ * 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.app.Application;
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.os.Handler;
+
+import com.android.launcher3.R;
+
+import java.lang.ref.WeakReference;
+
+public class LauncherApplication extends Application {
+    private LauncherModel mModel;
+    private IconCache mIconCache;
+    private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb;
+    private static boolean sIsScreenLarge;
+    private static float sScreenDensity;
+    private static int sLongPressTimeout = 300;
+    private static final String sSharedPreferencesKey = "com.android.launcher3.prefs";
+    WeakReference<LauncherProvider> mLauncherProvider;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        // set sIsScreenXLarge and sScreenDensity *before* creating icon cache
+        sIsScreenLarge = getResources().getBoolean(R.bool.is_large_screen);
+        sScreenDensity = getResources().getDisplayMetrics().density;
+
+        mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(this);
+        mIconCache = new IconCache(this);
+        mModel = new LauncherModel(this, mIconCache);
+
+        // Register intent receivers
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        registerReceiver(mModel, filter);
+        filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        registerReceiver(mModel, filter);
+        filter = new IntentFilter();
+        filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
+        registerReceiver(mModel, filter);
+        filter = new IntentFilter();
+        filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
+        registerReceiver(mModel, filter);
+
+        // Register for changes to the favorites
+        ContentResolver resolver = getContentResolver();
+        resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
+                mFavoritesObserver);
+    }
+
+    /**
+     * There's no guarantee that this function is ever called.
+     */
+    @Override
+    public void onTerminate() {
+        super.onTerminate();
+
+        unregisterReceiver(mModel);
+
+        ContentResolver resolver = getContentResolver();
+        resolver.unregisterContentObserver(mFavoritesObserver);
+    }
+
+    /**
+     * Receives notifications whenever the user favorites have changed.
+     */
+    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();
+        }
+    };
+
+    LauncherModel setLauncher(Launcher launcher) {
+        mModel.initialize(launcher);
+        return mModel;
+    }
+
+    IconCache getIconCache() {
+        return mIconCache;
+    }
+
+    LauncherModel getModel() {
+        return mModel;
+    }
+
+    WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() {
+        return mWidgetPreviewCacheDb;
+    }
+
+   void setLauncherProvider(LauncherProvider provider) {
+        mLauncherProvider = new WeakReference<LauncherProvider>(provider);
+    }
+
+    LauncherProvider getLauncherProvider() {
+        return mLauncherProvider.get();
+    }
+
+    public static String getSharedPreferencesKey() {
+        return sSharedPreferencesKey;
+    }
+
+    public static boolean isScreenLarge() {
+        return sIsScreenLarge;
+    }
+
+    public static boolean isScreenLandscape(Context context) {
+        return context.getResources().getConfiguration().orientation ==
+            Configuration.ORIENTATION_LANDSCAPE;
+    }
+
+    public static float getScreenDensity() {
+        return sScreenDensity;
+    }
+
+    public static int getLongPressTimeout() {
+        return sLongPressTimeout;
+    }
+}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
new file mode 100644
index 0000000..5459af2
--- /dev/null
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -0,0 +1,2620 @@
+/*
+ * 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.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.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Intent.ShortcutIconResource;
+import android.content.pm.ActivityInfo;
+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.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.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.launcher3.R;
+import com.android.launcher3.InstallWidgetReceiver.WidgetMimeTypeHandlerData;
+
+import java.lang.ref.WeakReference;
+import java.net.URISyntaxException;
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Maintains in-memory state of the Launcher. It is expected that there should be only one
+ * LauncherModel object held in a static. Also provide APIs for updating the database state
+ * for the Launcher.
+ */
+public class LauncherModel extends BroadcastReceiver {
+    static final boolean DEBUG_LOADERS = false;
+    static final String TAG = "Launcher.Model";
+
+    private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
+    private final boolean mAppsCanBeOnExternalStorage;
+    private int mBatchSize; // 0 is all apps at once
+    private int mAllAppsLoadDelay; // milliseconds between batches
+
+    private final LauncherApplication mApp;
+    private final Object mLock = new Object();
+    private DeferredHandler mHandler = new DeferredHandler();
+    private LoaderTask mLoaderTask;
+    private boolean mIsLoaderTaskRunning;
+    private volatile boolean mFlushingWorkerThread;
+
+    // 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;
+
+
+    private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
+    static {
+        sWorkerThread.start();
+    }
+    private 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;
+
+    // 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
+    // draw (in Workspace) to initiate the binding of the remaining side pages.  Any time we start
+    // a normal load, we also clear this set of Runnables.
+    static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>();
+
+    private WeakReference<Callbacks> mCallbacks;
+
+    // < only access in worker thread >
+    private AllAppsList mBgAllAppsList;
+
+    // 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
+    // static data structures to be referenced outside of the worker thread except on the first
+    // load after configuration change.
+    static final Object sBgLock = new Object();
+
+    // 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>();
+
+    // 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
+    //       shortcuts within folders).
+    static final ArrayList<ItemInfo> sBgWorkspaceItems = new ArrayList<ItemInfo>();
+
+    // sBgAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget()
+    static final ArrayList<LauncherAppWidgetInfo> sBgAppWidgets =
+        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[]>();
+    // </ only access in worker thread >
+
+    private IconCache mIconCache;
+    private Bitmap mDefaultIcon;
+
+    private static int mCellCountX;
+    private static int mCellCountY;
+
+    protected int mPreviousConfigMcc;
+
+    public interface Callbacks {
+        public boolean setLoadOnResume();
+        public int getCurrentWorkspaceScreen();
+        public void startBinding();
+        public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end);
+        public void bindFolders(HashMap<Long,FolderInfo> folders);
+        public void finishBindingItems();
+        public void bindAppWidget(LauncherAppWidgetInfo info);
+        public void bindAllApplications(ArrayList<ApplicationInfo> apps);
+        public void bindAppsAdded(ArrayList<ApplicationInfo> apps);
+        public void bindAppsUpdated(ArrayList<ApplicationInfo> apps);
+        public void bindComponentsRemoved(ArrayList<String> packageNames,
+                        ArrayList<ApplicationInfo> appInfos,
+                        boolean matchPackageNamesOnly);
+        public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts);
+        public boolean isAllAppsVisible();
+        public boolean isAllAppsButtonRank(int rank);
+        public void bindSearchablesChanged();
+        public void onPageBoundSynchronously(int page);
+    }
+
+    LauncherModel(LauncherApplication app, IconCache iconCache) {
+        mAppsCanBeOnExternalStorage = !Environment.isExternalStorageEmulated();
+        mApp = app;
+        mBgAllAppsList = new AllAppsList(iconCache);
+        mIconCache = iconCache;
+
+        mDefaultIcon = Utilities.createIconBitmap(
+                mIconCache.getFullResDefaultActivityIcon(), app);
+
+        final Resources res = app.getResources();
+        mAllAppsLoadDelay = res.getInteger(R.integer.config_allAppsBatchLoadDelay);
+        mBatchSize = res.getInteger(R.integer.config_allAppsBatchSize);
+        Configuration config = res.getConfiguration();
+        mPreviousConfigMcc = config.mcc;
+    }
+
+    /** 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) {
+        if (sWorkerThread.getThreadId() == Process.myTid()) {
+            // If we are on the worker thread, post onto the main handler
+            mHandler.post(r);
+        } else {
+            r.run();
+        }
+    }
+
+    /** Runs the specified runnable immediately if called from the worker thread, otherwise it is
+     * posted on the worker thread handler. */
+    private static void runOnWorkerThread(Runnable r) {
+        if (sWorkerThread.getThreadId() == Process.myTid()) {
+            r.run();
+        } else {
+            // If we are not on the worker thread, then post to the worker handler
+            sWorker.post(r);
+        }
+    }
+
+    public Bitmap getFallbackIcon() {
+        return Bitmap.createBitmap(mDefaultIcon);
+    }
+
+    public void unbindItemInfosAndClearQueuedBindRunnables() {
+        if (sWorkerThread.getThreadId() == Process.myTid()) {
+            throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " +
+                    "main thread");
+        }
+
+        // Clear any deferred bind runnables
+        mDeferredBindRunnables.clear();
+        // Remove any queued bind runnables
+        mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE);
+        // Unbind all the workspace items
+        unbindWorkspaceItemsOnMainThread();
+    }
+
+    /** Unbinds all the sBgWorkspaceItems and sBgAppWidgets on the main thread */
+    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>();
+        synchronized (sBgLock) {
+            tmpWorkspaceItems.addAll(sBgWorkspaceItems);
+            tmpAppWidgets.addAll(sBgAppWidgets);
+        }
+        Runnable r = new Runnable() {
+                @Override
+                public void run() {
+                   for (ItemInfo item : tmpWorkspaceItems) {
+                       item.unbind();
+                   }
+                   for (ItemInfo item : tmpAppWidgets) {
+                       item.unbind();
+                   }
+                }
+            };
+        runOnMainThread(r);
+    }
+
+    /**
+     * Adds an item to the DB if it was not created previously, or move it to a new
+     * <container, screen, cellX, cellY>
+     */
+    static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container,
+            int screen, int cellX, int cellY) {
+        if (item.container == ItemInfo.NO_ID) {
+            // From all apps
+            addItemToDatabase(context, item, container, screen, cellX, cellY, false);
+        } else {
+            // From somewhere else
+            moveItemInDatabase(context, item, container, screen, cellX, cellY);
+        }
+    }
+
+    static void checkItemInfoLocked(
+            final long itemId, final ItemInfo item, StackTraceElement[] stackTrace) {
+        ItemInfo modelItem = sBgItemsIdMap.get(itemId);
+        if (modelItem != null && item != modelItem) {
+            // check all the data is consistent
+            if (modelItem instanceof ShortcutInfo && item instanceof ShortcutInfo) {
+                ShortcutInfo modelShortcut = (ShortcutInfo) modelItem;
+                ShortcutInfo shortcut = (ShortcutInfo) item;
+                if (modelShortcut.title.toString().equals(shortcut.title.toString()) &&
+                        modelShortcut.intent.filterEquals(shortcut.intent) &&
+                        modelShortcut.id == shortcut.id &&
+                        modelShortcut.itemType == shortcut.itemType &&
+                        modelShortcut.container == shortcut.container &&
+                        modelShortcut.screen == shortcut.screen &&
+                        modelShortcut.cellX == shortcut.cellX &&
+                        modelShortcut.cellY == shortcut.cellY &&
+                        modelShortcut.spanX == shortcut.spanX &&
+                        modelShortcut.spanY == shortcut.spanY &&
+                        ((modelShortcut.dropPos == null && shortcut.dropPos == null) ||
+                        (modelShortcut.dropPos != null &&
+                                shortcut.dropPos != null &&
+                                modelShortcut.dropPos[0] == shortcut.dropPos[0] &&
+                        modelShortcut.dropPos[1] == shortcut.dropPos[1]))) {
+                    // For all intents and purposes, this is the same object
+                    return;
+                }
+            }
+
+            // the modelItem needs to match up perfectly with item if our model is
+            // to be consistent with the database-- for now, just require
+            // modelItem == item or the equality check above
+            String msg = "item: " + ((item != null) ? item.toString() : "null") +
+                    "modelItem: " +
+                    ((modelItem != null) ? modelItem.toString() : "null") +
+                    "Error: ItemInfo passed to checkItemInfo doesn't match original";
+            RuntimeException e = new RuntimeException(msg);
+            if (stackTrace != null) {
+                e.setStackTrace(stackTrace);
+            }
+            throw e;
+        }
+    }
+
+    static void checkItemInfo(final ItemInfo item) {
+        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
+        final long itemId = item.id;
+        Runnable r = new Runnable() {
+            public void run() {
+                synchronized (sBgLock) {
+                    checkItemInfoLocked(itemId, item, stackTrace);
+                }
+            }
+        };
+        runOnWorkerThread(r);
+    }
+
+    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 ContentResolver cr = context.getContentResolver();
+
+        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
+        Runnable r = new Runnable() {
+            public void run() {
+                cr.update(uri, values, null, null);
+
+                // Lock on mBgLock *after* the db operation
+                synchronized (sBgLock) {
+                    checkItemInfoLocked(itemId, item, stackTrace);
+
+                    if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                            item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                        // Item is in a folder, make sure this folder exists
+                        if (!sBgFolders.containsKey(item.container)) {
+                            // An items container is being set to a that of an item which is not in
+                            // the list of Folders.
+                            String msg = "item: " + item + " container being set to: " +
+                                    item.container + ", not in the list of folders";
+                            Log.e(TAG, msg);
+                            Launcher.dumpDebugLogsToConsole();
+                        }
+                    }
+
+                    // Items are added/removed from the corresponding FolderInfo elsewhere, such
+                    // as in Workspace.onDrop. Here, we just add/remove them from the list of items
+                    // that are on the desktop, as appropriate
+                    ItemInfo modelItem = sBgItemsIdMap.get(itemId);
+                    if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
+                            modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                        switch (modelItem.itemType) {
+                            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                                if (!sBgWorkspaceItems.contains(modelItem)) {
+                                    sBgWorkspaceItems.add(modelItem);
+                                }
+                                break;
+                            default:
+                                break;
+                        }
+                    } else {
+                        sBgWorkspaceItems.remove(modelItem);
+                    }
+                }
+            }
+        };
+        runOnWorkerThread(r);
+    }
+
+    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,
+            final int screen, final int cellX, final int cellY) {
+        String transaction = "DbDebug    Modify item (" + item.title + ") in db, id: " + item.id +
+                " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY +
+                ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")";
+        Launcher.sDumpLogs.add(transaction);
+        Log.d(TAG, transaction);
+        item.container = container;
+        item.cellX = cellX;
+        item.cellY = cellY;
+
+        // We store hotseat items in canonical form which is this orientation invariant position
+        // in the hotseat
+        if (context instanceof Launcher && screen < 0 &&
+                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
+        } else {
+            item.screen = screen;
+        }
+
+        final ContentValues values = new ContentValues();
+        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.SCREEN, item.screen);
+
+        updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase");
+    }
+
+    /**
+     * Move and/or resize item in the DB to a new <container, screen, cellX, cellY, spanX, spanY>
+     */
+    static void modifyItemInDatabase(Context context, final ItemInfo item, final long container,
+            final int screen, final int cellX, final int cellY, final int spanX, final int spanY) {
+        String transaction = "DbDebug    Modify item (" + item.title + ") in db, id: " + item.id +
+                " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY +
+                ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")";
+        Launcher.sDumpLogs.add(transaction);
+        Log.d(TAG, transaction);
+        item.cellX = cellX;
+        item.cellY = cellY;
+        item.spanX = spanX;
+        item.spanY = spanY;
+
+        // We store hotseat items in canonical form which is this orientation invariant position
+        // in the hotseat
+        if (context instanceof Launcher && screen < 0 &&
+                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
+        } else {
+            item.screen = screen;
+        }
+
+        final ContentValues values = new ContentValues();
+        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.SPANX, item.spanX);
+        values.put(LauncherSettings.Favorites.SPANY, item.spanY);
+        values.put(LauncherSettings.Favorites.SCREEN, item.screen);
+
+        updateItemInDatabaseHelper(context, values, item, "modifyItemInDatabase");
+    }
+
+    /**
+     * Update an item to the database in a specified container.
+     */
+    static void updateItemInDatabase(Context context, final ItemInfo item) {
+        final ContentValues values = new ContentValues();
+        item.onAddToDatabase(values);
+        item.updateValuesWithCoordinates(values, item.cellX, item.cellY);
+        updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase");
+    }
+
+    /**
+     * Returns true if the shortcuts already exists in the database.
+     * we identify a shortcut by its title and intent.
+     */
+    static boolean shortcutExists(Context context, String title, Intent intent) {
+        final ContentResolver cr = context.getContentResolver();
+        Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI,
+            new String[] { "title", "intent" }, "title=? and intent=?",
+            new String[] { title, intent.toUri(0) }, null);
+        boolean result = false;
+        try {
+            result = c.moveToFirst();
+        } finally {
+            c.close();
+        }
+        return result;
+    }
+
+    /**
+     * 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 }, 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);
+
+        try {
+            while (c.moveToNext()) {
+                ItemInfo item = new ItemInfo();
+                item.cellX = c.getInt(cellXIndex);
+                item.cellY = c.getInt(cellYIndex);
+                item.spanX = c.getInt(spanXIndex);
+                item.spanY = c.getInt(spanYIndex);
+                item.container = c.getInt(containerIndex);
+                item.itemType = c.getInt(itemTypeIndex);
+                item.screen = c.getInt(screenIndex);
+
+                items.add(item);
+            }
+        } catch (Exception e) {
+            items.clear();
+        } finally {
+            c.close();
+        }
+
+        return items;
+    }
+
+    /**
+     * 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) {
+        final ContentResolver cr = context.getContentResolver();
+        Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null,
+                "_id=? and (itemType=? or itemType=?)",
+                new String[] { String.valueOf(id),
+                        String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}, null);
+
+        try {
+            if (c.moveToFirst()) {
+                final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+                final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
+                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);
+
+                FolderInfo folderInfo = null;
+                switch (c.getInt(itemTypeIndex)) {
+                    case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                        folderInfo = findOrMakeFolder(folderList, id);
+                        break;
+                }
+
+                folderInfo.title = c.getString(titleIndex);
+                folderInfo.id = id;
+                folderInfo.container = c.getInt(containerIndex);
+                folderInfo.screen = c.getInt(screenIndex);
+                folderInfo.cellX = c.getInt(cellXIndex);
+                folderInfo.cellY = c.getInt(cellYIndex);
+
+                return folderInfo;
+            }
+        } finally {
+            c.close();
+        }
+
+        return null;
+    }
+
+    /**
+     * 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 int screen, final int cellX, final int cellY, final boolean notify) {
+        item.container = container;
+        item.cellX = cellX;
+        item.cellY = cellY;
+        // We store hotseat items in canonical form which is this orientation invariant position
+        // in the hotseat
+        if (context instanceof Launcher && screen < 0 &&
+                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
+        } else {
+            item.screen = screen;
+        }
+
+        final ContentValues values = new ContentValues();
+        final ContentResolver cr = context.getContentResolver();
+        item.onAddToDatabase(values);
+
+        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+        item.id = app.getLauncherProvider().generateNewId();
+        values.put(LauncherSettings.Favorites._ID, item.id);
+        item.updateValuesWithCoordinates(values, item.cellX, item.cellY);
+
+        Runnable r = new Runnable() {
+            public void run() {
+                String transaction = "DbDebug    Add item (" + item.title + ") to db, id: "
+                        + item.id + " (" + container + ", " + screen + ", " + cellX + ", "
+                        + cellY + ")";
+                Launcher.sDumpLogs.add(transaction);
+                Log.d(TAG, transaction);
+
+                cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
+                        LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
+
+                // Lock on mBgLock *after* the db operation
+                synchronized (sBgLock) {
+                    checkItemInfoLocked(item.id, item, null);
+                    sBgItemsIdMap.put(item.id, item);
+                    switch (item.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                            sBgFolders.put(item.id, (FolderInfo) item);
+                            // Fall through
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
+                                    item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                                sBgWorkspaceItems.add(item);
+                            } else {
+                                if (!sBgFolders.containsKey(item.container)) {
+                                    // Adding an item to a folder that doesn't exist.
+                                    String msg = "adding item: " + item + " to a folder that " +
+                                            " doesn't exist";
+                                    Log.e(TAG, msg);
+                                    Launcher.dumpDebugLogsToConsole();
+                                }
+                            }
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                            sBgAppWidgets.add((LauncherAppWidgetInfo) item);
+                            break;
+                    }
+                }
+            }
+        };
+        runOnWorkerThread(r);
+    }
+
+    /**
+     * Creates a new unique child id, for a given cell span across all layouts.
+     */
+    static int getCellLayoutChildId(
+            long container, int screen, int localCellX, int localCellY, int spanX, int spanY) {
+        return (((int) container & 0xFF) << 24)
+                | (screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF);
+    }
+
+    static int getCellCountX() {
+        return mCellCountX;
+    }
+
+    static int getCellCountY() {
+        return mCellCountY;
+    }
+
+    /**
+     * Updates the model orientation helper to take into account the current layout dimensions
+     * when performing local/canonical coordinate transformations.
+     */
+    static void updateWorkspaceLayoutCells(int shortAxisCellCount, int longAxisCellCount) {
+        mCellCountX = shortAxisCellCount;
+        mCellCountY = longAxisCellCount;
+    }
+
+    /**
+     * Removes the specified item from the database
+     * @param context
+     * @param item
+     */
+    static void deleteItemFromDatabase(Context context, final ItemInfo item) {
+        final ContentResolver cr = context.getContentResolver();
+        final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false);
+
+        Runnable r = new Runnable() {
+            public void run() {
+                String transaction = "DbDebug    Delete item (" + item.title + ") from db, id: "
+                        + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX +
+                        ", " + item.cellY + ")";
+                Launcher.sDumpLogs.add(transaction);
+                Log.d(TAG, transaction);
+
+                cr.delete(uriToDelete, null, null);
+
+                // Lock on mBgLock *after* the db operation
+                synchronized (sBgLock) {
+                    switch (item.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                            sBgFolders.remove(item.id);
+                            for (ItemInfo info: sBgItemsIdMap.values()) {
+                                if (info.container == item.id) {
+                                    // We are deleting a folder which still contains items that
+                                    // think they are contained by that folder.
+                                    String msg = "deleting a folder (" + item + ") which still " +
+                                            "contains items (" + info + ")";
+                                    Log.e(TAG, msg);
+                                    Launcher.dumpDebugLogsToConsole();
+                                }
+                            }
+                            sBgWorkspaceItems.remove(item);
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                            sBgWorkspaceItems.remove(item);
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                            sBgAppWidgets.remove((LauncherAppWidgetInfo) item);
+                            break;
+                    }
+                    sBgItemsIdMap.remove(item.id);
+                    sBgDbIconCache.remove(item);
+                }
+            }
+        };
+        runOnWorkerThread(r);
+    }
+
+    /**
+     * Remove the contents of the specified folder from the database
+     */
+    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);
+                // 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,
+                        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);
+                    }
+                }
+            }
+        };
+        runOnWorkerThread(r);
+    }
+
+    /**
+     * Set this as the current Launcher activity object for the loader.
+     */
+    public void initialize(Callbacks callbacks) {
+        synchronized (mLock) {
+            mCallbacks = new WeakReference<Callbacks>(callbacks);
+        }
+    }
+
+    /**
+     * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
+     * ACTION_PACKAGE_CHANGED.
+     */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent);
+
+        final String action = intent.getAction();
+
+        if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+                || Intent.ACTION_PACKAGE_REMOVED.equals(action)
+                || Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            final String packageName = intent.getData().getSchemeSpecificPart();
+            final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+
+            int op = PackageUpdatedTask.OP_NONE;
+
+            if (packageName == null || packageName.length() == 0) {
+                // they sent us a bad intent
+                return;
+            }
+
+            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+                op = PackageUpdatedTask.OP_UPDATE;
+            } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+                if (!replacing) {
+                    op = PackageUpdatedTask.OP_REMOVE;
+                }
+                // else, we are replacing the package, so a PACKAGE_ADDED will be sent
+                // later, we will update the package at this time
+            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+                if (!replacing) {
+                    op = PackageUpdatedTask.OP_ADD;
+                } else {
+                    op = PackageUpdatedTask.OP_UPDATE;
+                }
+            }
+
+            if (op != PackageUpdatedTask.OP_NONE) {
+                enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }));
+            }
+
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
+            // First, schedule to add these apps back in.
+            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages));
+            // Then, rebind everything.
+            startLoaderFromBackground();
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
+            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            enqueuePackageUpdated(new PackageUpdatedTask(
+                        PackageUpdatedTask.OP_UNAVAILABLE, packages));
+        } else 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)) {
+            if (mCallbacks != null) {
+                Callbacks callbacks = mCallbacks.get();
+                if (callbacks != null) {
+                    callbacks.bindSearchablesChanged();
+                }
+            }
+        }
+    }
+
+    private void forceReload() {
+        resetLoadedState(true, true);
+
+        // Do this here because if the launcher activity is running it will be restarted.
+        // If it's not running startLoaderFromBackground will merely tell it that it needs
+        // to reload.
+        startLoaderFromBackground();
+    }
+
+    public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) {
+        synchronized (mLock) {
+            // Stop any existing loaders first, so they don't set mAllAppsLoaded or
+            // mWorkspaceLoaded to true later
+            stopLoaderLocked();
+            if (resetAllAppsLoaded) mAllAppsLoaded = false;
+            if (resetWorkspaceLoaded) mWorkspaceLoaded = false;
+        }
+    }
+
+    /**
+     * When the launcher is in the background, it's possible for it to miss paired
+     * configuration changes.  So whenever we trigger the loader from the background
+     * tell the launcher that it needs to re-run the loader when it comes back instead
+     * of doing it now.
+     */
+    public void startLoaderFromBackground() {
+        boolean runLoader = false;
+        if (mCallbacks != null) {
+            Callbacks callbacks = mCallbacks.get();
+            if (callbacks != null) {
+                // Only actually run the loader if they're not paused.
+                if (!callbacks.setLoadOnResume()) {
+                    runLoader = true;
+                }
+            }
+        }
+        if (runLoader) {
+            startLoader(false, -1);
+        }
+    }
+
+    // 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;
+        LoaderTask oldTask = mLoaderTask;
+        if (oldTask != null) {
+            if (oldTask.isLaunching()) {
+                isLaunching = true;
+            }
+            oldTask.stopLocked();
+        }
+        return isLaunching;
+    }
+
+    public void startLoader(boolean isLaunching, int synchronousBindPage) {
+        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.
+            mDeferredBindRunnables.clear();
+
+            // 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, isLaunching);
+                if (synchronousBindPage > -1 && mAllAppsLoaded && mWorkspaceLoaded) {
+                    mLoaderTask.runBindSynchronousPage(synchronousBindPage);
+                } else {
+                    sWorkerThread.setPriority(Thread.NORM_PRIORITY);
+                    sWorker.post(mLoaderTask);
+                }
+            }
+        }
+    }
+
+    void bindRemainingSynchronousPages() {
+        // Post the remaining side pages to be loaded
+        if (!mDeferredBindRunnables.isEmpty()) {
+            for (final Runnable r : mDeferredBindRunnables) {
+                mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE);
+            }
+            mDeferredBindRunnables.clear();
+        }
+    }
+
+    public void stopLoader() {
+        synchronized (mLock) {
+            if (mLoaderTask != null) {
+                mLoaderTask.stopLocked();
+            }
+        }
+    }
+
+    public boolean isAllAppsLoaded() {
+        return mAllAppsLoaded;
+    }
+
+    boolean isLoadingWorkspace() {
+        synchronized (mLock) {
+            if (mLoaderTask != null) {
+                return mLoaderTask.isLoadingWorkspace();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Runnable for the thread that loads the contents of the launcher:
+     *   - workspace icons
+     *   - widgets
+     *   - all apps icons
+     */
+    private class LoaderTask implements Runnable {
+        private Context mContext;
+        private boolean mIsLaunching;
+        private boolean mIsLoadingAndBindingWorkspace;
+        private boolean mStopped;
+        private boolean mLoadAndBindStepFinished;
+
+        private HashMap<Object, CharSequence> mLabelCache;
+
+        LoaderTask(Context context, boolean isLaunching) {
+            mContext = context;
+            mIsLaunching = isLaunching;
+            mLabelCache = new HashMap<Object, CharSequence>();
+        }
+
+        boolean isLaunching() {
+            return mIsLaunching;
+        }
+
+        boolean isLoadingWorkspace() {
+            return mIsLoadingAndBindingWorkspace;
+        }
+
+        private void loadAndBindWorkspace() {
+            mIsLoadingAndBindingWorkspace = true;
+
+            // Load the workspace
+            if (DEBUG_LOADERS) {
+                Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
+            }
+
+            if (!mWorkspaceLoaded) {
+                loadWorkspace();
+                synchronized (LoaderTask.this) {
+                    if (mStopped) {
+                        return;
+                    }
+                    mWorkspaceLoaded = true;
+                }
+            }
+
+            // Bind the workspace
+            bindWorkspace(-1);
+        }
+
+        private void waitForIdle() {
+            // Wait until the either we're stopped or the other threads are done.
+            // This way we don't start loading all apps until the workspace has settled
+            // down.
+            synchronized (LoaderTask.this) {
+                final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
+
+                mHandler.postIdle(new Runnable() {
+                        public void run() {
+                            synchronized (LoaderTask.this) {
+                                mLoadAndBindStepFinished = true;
+                                if (DEBUG_LOADERS) {
+                                    Log.d(TAG, "done with previous binding step");
+                                }
+                                LoaderTask.this.notify();
+                            }
+                        }
+                    });
+
+                while (!mStopped && !mLoadAndBindStepFinished && !mFlushingWorkerThread) {
+                    try {
+                        // Just in case mFlushingWorkerThread changes but we aren't woken up,
+                        // wait no longer than 1sec at a time
+                        this.wait(1000);
+                    } catch (InterruptedException ex) {
+                        // Ignore
+                    }
+                }
+                if (DEBUG_LOADERS) {
+                    Log.d(TAG, "waited "
+                            + (SystemClock.uptimeMillis()-workspaceWaitTime)
+                            + "ms for previous step to finish binding");
+                }
+            }
+        }
+
+        void runBindSynchronousPage(int synchronousBindPage) {
+            if (synchronousBindPage < 0) {
+                // Ensure that we have a valid page index to load synchronously
+                throw new RuntimeException("Should not call runBindSynchronousPage() without " +
+                        "valid page index");
+            }
+            if (!mAllAppsLoaded || !mWorkspaceLoaded) {
+                // Ensure that we don't try and bind a specified page when the pages have not been
+                // loaded already (we should load everything asynchronously in that case)
+                throw new RuntimeException("Expecting AllApps and Workspace to be loaded");
+            }
+            synchronized (mLock) {
+                if (mIsLoaderTaskRunning) {
+                    // Ensure that we are never running the background loading at this point since
+                    // we also touch the background collections
+                    throw new RuntimeException("Error! Background loading is already running");
+                }
+            }
+
+            // XXX: Throw an exception if we are already loading (since we touch the worker thread
+            //      data structures, we can't allow any other thread to touch that data, but because
+            //      this call is synchronous, we can get away with not locking).
+
+            // The LauncherModel is static in the LauncherApplication and mHandler may have queued
+            // operations from the previous activity.  We need to ensure that all queued operations
+            // are executed before any synchronous binding work is done.
+            mHandler.flush();
+
+            // 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);
+            // XXX: For now, continue posting the binding of AllApps as there are other issues that
+            //      arise from that.
+            onlyBindAllApps();
+        }
+
+        public void run() {
+            synchronized (mLock) {
+                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).
+            final Callbacks cbk = mCallbacks.get();
+            final boolean loadWorkspaceFirst = cbk != null ? (!cbk.isAllAppsVisible()) : true;
+
+            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 (loadWorkspaceFirst) {
+                    if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
+                    loadAndBindWorkspace();
+                } else {
+                    if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps");
+                    loadAndBindAllApps();
+                }
+
+                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 (loadWorkspaceFirst) {
+                    if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
+                    loadAndBindAllApps();
+                } else {
+                    if (DEBUG_LOADERS) Log.d(TAG, "step 2: special: loading workspace");
+                    loadAndBindWorkspace();
+                }
+
+                // 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();
+            }
+
+            // Clear out this reference, otherwise we end up holding it until all of the
+            // callback runnables are done.
+            mContext = null;
+
+            synchronized (mLock) {
+                // If we are still the last one to be scheduled, remove ourselves.
+                if (mLoaderTask == this) {
+                    mLoaderTask = null;
+                }
+                mIsLoaderTaskRunning = false;
+            }
+        }
+
+        public void stopLocked() {
+            synchronized (LoaderTask.this) {
+                mStopped = true;
+                this.notify();
+            }
+        }
+
+        /**
+         * Gets the callbacks object.  If we've been stopped, or if the launcher object
+         * has somehow been garbage collected, return null instead.  Pass in the Callbacks
+         * object that was around when the deferred message was scheduled, and if there's
+         * a new Callbacks object around then also return null.  This will save us from
+         * calling onto it with data that will be ignored.
+         */
+        Callbacks tryGetCallbacks(Callbacks oldCallbacks) {
+            synchronized (mLock) {
+                if (mStopped) {
+                    return null;
+                }
+
+                if (mCallbacks == null) {
+                    return null;
+                }
+
+                final Callbacks callbacks = mCallbacks.get();
+                if (callbacks != oldCallbacks) {
+                    return null;
+                }
+                if (callbacks == null) {
+                    Log.w(TAG, "no mCallbacks");
+                    return null;
+                }
+
+                return callbacks;
+            }
+        }
+
+        // check & update map of what's occupied; used to discard overlapping/invalid items
+        private boolean checkItemPlacement(ItemInfo occupied[][][], ItemInfo item) {
+            int containerIndex = item.screen;
+            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                // Return early if we detect that an item is under the hotseat button
+                if (mCallbacks == null || mCallbacks.get().isAllAppsButtonRank(item.screen)) {
+                    return false;
+                }
+
+                // We use the last index to refer to the hotseat and the screen as the rank, so
+                // test and update the occupied state accordingly
+                if (occupied[Launcher.SCREEN_COUNT][item.screen][0] != null) {
+                    Log.e(TAG, "Error loading shortcut into hotseat " + item
+                        + " into position (" + item.screen + ":" + item.cellX + "," + item.cellY
+                        + ") occupied by " + occupied[Launcher.SCREEN_COUNT][item.screen][0]);
+                    return false;
+                } else {
+                    occupied[Launcher.SCREEN_COUNT][item.screen][0] = item;
+                    return true;
+                }
+            } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                // Skip further checking if it is not the hotseat or workspace container
+                return true;
+            }
+
+            // Check if any workspace icons overlap with each other
+            for (int x = item.cellX; x < (item.cellX+item.spanX); x++) {
+                for (int y = item.cellY; y < (item.cellY+item.spanY); y++) {
+                    if (occupied[containerIndex][x][y] != null) {
+                        Log.e(TAG, "Error loading shortcut " + item
+                            + " into cell (" + containerIndex + "-" + item.screen + ":"
+                            + x + "," + y
+                            + ") occupied by "
+                            + occupied[containerIndex][x][y]);
+                        return false;
+                    }
+                }
+            }
+            for (int x = item.cellX; x < (item.cellX+item.spanX); x++) {
+                for (int y = item.cellY; y < (item.cellY+item.spanY); y++) {
+                    occupied[containerIndex][x][y] = item;
+                }
+            }
+
+            return 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();
+
+            // Make sure the default workspace is loaded, if needed
+            mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0);
+
+            synchronized (sBgLock) {
+                sBgWorkspaceItems.clear();
+                sBgAppWidgets.clear();
+                sBgFolders.clear();
+                sBgItemsIdMap.clear();
+                sBgDbIconCache.clear();
+
+                final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
+
+                final Cursor c = contentResolver.query(
+                        LauncherSettings.Favorites.CONTENT_URI, 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 ItemInfo occupied[][][] =
+                        new ItemInfo[Launcher.SCREEN_COUNT + 1][mCellCountX + 1][mCellCountY + 1];
+
+                try {
+                    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 appWidgetIdIndex = c.getColumnIndexOrThrow(
+                            LauncherSettings.Favorites.APPWIDGET_ID);
+                    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 uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
+                    //final int displayModeIndex = c.getColumnIndexOrThrow(
+                    //        LauncherSettings.Favorites.DISPLAY_MODE);
+
+                    ShortcutInfo info;
+                    String intentDescription;
+                    LauncherAppWidgetInfo appWidgetInfo;
+                    int container;
+                    long id;
+                    Intent intent;
+
+                    while (!mStopped && c.moveToNext()) {
+                        try {
+                            int itemType = c.getInt(itemTypeIndex);
+
+                            switch (itemType) {
+                            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                                intentDescription = c.getString(intentIndex);
+                                try {
+                                    intent = Intent.parseUri(intentDescription, 0);
+                                } catch (URISyntaxException e) {
+                                    continue;
+                                }
+
+                                if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
+                                    info = getShortcutInfo(manager, intent, context, c, iconIndex,
+                                            titleIndex, mLabelCache);
+                                } else {
+                                    info = getShortcutInfo(c, context, iconTypeIndex,
+                                            iconPackageIndex, iconResourceIndex, iconIndex,
+                                            titleIndex);
+
+                                    // App shortcuts that used to be automatically added to Launcher
+                                    // didn't always have the correct intent flags set, so do that
+                                    // here
+                                    if (intent.getAction() != null &&
+                                        intent.getCategories() != null &&
+                                        intent.getAction().equals(Intent.ACTION_MAIN) &&
+                                        intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
+                                        intent.addFlags(
+                                            Intent.FLAG_ACTIVITY_NEW_TASK |
+                                            Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                                    }
+                                }
+
+                                if (info != null) {
+                                    info.intent = intent;
+                                    info.id = c.getLong(idIndex);
+                                    container = c.getInt(containerIndex);
+                                    info.container = container;
+                                    info.screen = c.getInt(screenIndex);
+                                    info.cellX = c.getInt(cellXIndex);
+                                    info.cellY = c.getInt(cellYIndex);
+
+                                    // check & update map of what's occupied
+                                    if (!checkItemPlacement(occupied, info)) {
+                                        break;
+                                    }
+
+                                    switch (container) {
+                                    case LauncherSettings.Favorites.CONTAINER_DESKTOP:
+                                    case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
+                                        sBgWorkspaceItems.add(info);
+                                        break;
+                                    default:
+                                        // Item is in a user folder
+                                        FolderInfo folderInfo =
+                                                findOrMakeFolder(sBgFolders, container);
+                                        folderInfo.add(info);
+                                        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 {
+                                    // Failed to load the shortcut, probably because the
+                                    // activity manager couldn't resolve it (maybe the app
+                                    // was uninstalled), or the db row was somehow screwed up.
+                                    // Delete it.
+                                    id = c.getLong(idIndex);
+                                    Log.e(TAG, "Error loading shortcut " + id + ", removing it");
+                                    contentResolver.delete(LauncherSettings.Favorites.getContentUri(
+                                                id, false), null, null);
+                                }
+                                break;
+
+                            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                                id = c.getLong(idIndex);
+                                FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id);
+
+                                folderInfo.title = c.getString(titleIndex);
+                                folderInfo.id = id;
+                                container = c.getInt(containerIndex);
+                                folderInfo.container = container;
+                                folderInfo.screen = c.getInt(screenIndex);
+                                folderInfo.cellX = c.getInt(cellXIndex);
+                                folderInfo.cellY = c.getInt(cellYIndex);
+
+                                // check & update map of what's occupied
+                                if (!checkItemPlacement(occupied, folderInfo)) {
+                                    break;
+                                }
+                                switch (container) {
+                                    case LauncherSettings.Favorites.CONTAINER_DESKTOP:
+                                    case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
+                                        sBgWorkspaceItems.add(folderInfo);
+                                        break;
+                                }
+
+                                sBgItemsIdMap.put(folderInfo.id, folderInfo);
+                                sBgFolders.put(folderInfo.id, folderInfo);
+                                break;
+
+                            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                                // Read all Launcher-specific widget details
+                                int appWidgetId = c.getInt(appWidgetIdIndex);
+                                id = c.getLong(idIndex);
+
+                                final AppWidgetProviderInfo provider =
+                                        widgets.getAppWidgetInfo(appWidgetId);
+
+                                if (!isSafeMode && (provider == null || provider.provider == null ||
+                                        provider.provider.getPackageName() == null)) {
+                                    String log = "Deleting widget that isn't installed anymore: id="
+                                        + id + " appWidgetId=" + appWidgetId;
+                                    Log.e(TAG, log);
+                                    Launcher.sDumpLogs.add(log);
+                                    itemsToRemove.add(id);
+                                } else {
+                                    appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
+                                            provider.provider);
+                                    appWidgetInfo.id = id;
+                                    appWidgetInfo.screen = c.getInt(screenIndex);
+                                    appWidgetInfo.cellX = c.getInt(cellXIndex);
+                                    appWidgetInfo.cellY = c.getInt(cellYIndex);
+                                    appWidgetInfo.spanX = c.getInt(spanXIndex);
+                                    appWidgetInfo.spanY = c.getInt(spanYIndex);
+                                    int[] minSpan = Launcher.getMinSpanForWidget(context, provider);
+                                    appWidgetInfo.minSpanX = minSpan[0];
+                                    appWidgetInfo.minSpanY = minSpan[1];
+
+                                    container = c.getInt(containerIndex);
+                                    if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                                        container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                                        Log.e(TAG, "Widget found where container != " +
+                                            "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
+                                        continue;
+                                    }
+                                    appWidgetInfo.container = c.getInt(containerIndex);
+
+                                    // check & update map of what's occupied
+                                    if (!checkItemPlacement(occupied, appWidgetInfo)) {
+                                        break;
+                                    }
+                                    sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo);
+                                    sBgAppWidgets.add(appWidgetInfo);
+                                }
+                                break;
+                            }
+                        } catch (Exception e) {
+                            Log.w(TAG, "Desktop items loading interrupted:", e);
+                        }
+                    }
+                } finally {
+                    c.close();
+                }
+
+                if (itemsToRemove.size() > 0) {
+                    ContentProviderClient client = contentResolver.acquireContentProviderClient(
+                                    LauncherSettings.Favorites.CONTENT_URI);
+                    // 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);
+                        }
+                    }
+                }
+
+                if (DEBUG_LOADERS) {
+                    Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms");
+                    Log.d(TAG, "workspace layout: ");
+                    for (int y = 0; y < mCellCountY; y++) {
+                        String line = "";
+                        for (int s = 0; s < Launcher.SCREEN_COUNT; s++) {
+                            if (s > 0) {
+                                line += " | ";
+                            }
+                            for (int x = 0; x < mCellCountX; x++) {
+                                line += ((occupied[s][x][y] != null) ? "#" : ".");
+                            }
+                        }
+                        Log.d(TAG, "[ " + line + " ]");
+                    }
+                }
+            }
+        }
+
+        /** Filters the set of items who are directly or indirectly (via another container) on the
+         * specified screen. */
+        private void filterCurrentWorkspaceItems(int currentScreen,
+                ArrayList<ItemInfo> allWorkspaceItems,
+                ArrayList<ItemInfo> currentScreenItems,
+                ArrayList<ItemInfo> otherScreenItems) {
+            // Purge any null ItemInfos
+            Iterator<ItemInfo> iter = allWorkspaceItems.iterator();
+            while (iter.hasNext()) {
+                ItemInfo i = iter.next();
+                if (i == null) {
+                    iter.remove();
+                }
+            }
+
+            // If we aren't filtering on a screen, then the set of items to load is the full set of
+            // items given.
+            if (currentScreen < 0) {
+                currentScreenItems.addAll(allWorkspaceItems);
+            }
+
+            // Order the set of items by their containers first, this allows use to walk through the
+            // list sequentially, build up a list of containers that are in the specified screen,
+            // as well as all items in those containers.
+            Set<Long> itemsOnScreen = new HashSet<Long>();
+            Collections.sort(allWorkspaceItems, new Comparator<ItemInfo>() {
+                @Override
+                public int compare(ItemInfo lhs, ItemInfo rhs) {
+                    return (int) (lhs.container - rhs.container);
+                }
+            });
+            for (ItemInfo info : allWorkspaceItems) {
+                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                    if (info.screen == currentScreen) {
+                        currentScreenItems.add(info);
+                        itemsOnScreen.add(info.id);
+                    } else {
+                        otherScreenItems.add(info);
+                    }
+                } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                    currentScreenItems.add(info);
+                    itemsOnScreen.add(info.id);
+                } else {
+                    if (itemsOnScreen.contains(info.container)) {
+                        currentScreenItems.add(info);
+                        itemsOnScreen.add(info.id);
+                    } else {
+                        otherScreenItems.add(info);
+                    }
+                }
+            }
+        }
+
+        /** Filters the set of widgets which are on the specified screen. */
+        private void filterCurrentAppWidgets(int currentScreen,
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                ArrayList<LauncherAppWidgetInfo> currentScreenWidgets,
+                ArrayList<LauncherAppWidgetInfo> otherScreenWidgets) {
+            // If we aren't filtering on a screen, then the set of items to load is the full set of
+            // widgets given.
+            if (currentScreen < 0) {
+                currentScreenWidgets.addAll(appWidgets);
+            }
+
+            for (LauncherAppWidgetInfo widget : appWidgets) {
+                if (widget == null) continue;
+                if (widget.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                        widget.screen == currentScreen) {
+                    currentScreenWidgets.add(widget);
+                } else {
+                    otherScreenWidgets.add(widget);
+                }
+            }
+        }
+
+        /** Filters the set of folders which are on the specified screen. */
+        private void filterCurrentFolders(int currentScreen,
+                HashMap<Long, ItemInfo> itemsIdMap,
+                HashMap<Long, FolderInfo> folders,
+                HashMap<Long, FolderInfo> currentScreenFolders,
+                HashMap<Long, FolderInfo> otherScreenFolders) {
+            // If we aren't filtering on a screen, then the set of items to load is the full set of
+            // widgets given.
+            if (currentScreen < 0) {
+                currentScreenFolders.putAll(folders);
+            }
+
+            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.screen == currentScreen) {
+                    currentScreenFolders.put(id, folder);
+                } else {
+                    otherScreenFolders.put(id, folder);
+                }
+            }
+        }
+
+        /** Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to
+         * right) */
+        private void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) {
+            // XXX: review this
+            Collections.sort(workspaceItems, new Comparator<ItemInfo>() {
+                @Override
+                public int compare(ItemInfo lhs, ItemInfo rhs) {
+                    int cellCountX = LauncherModel.getCellCountX();
+                    int cellCountY = LauncherModel.getCellCountY();
+                    int screenOffset = cellCountX * cellCountY;
+                    int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat
+                    long lr = (lhs.container * containerOffset + lhs.screen * screenOffset +
+                            lhs.cellY * cellCountX + lhs.cellX);
+                    long rr = (rhs.container * containerOffset + rhs.screen * screenOffset +
+                            rhs.cellY * cellCountX + rhs.cellX);
+                    return (int) (lr - rr);
+                }
+            });
+        }
+
+        private void bindWorkspaceItems(final Callbacks oldCallbacks,
+                final ArrayList<ItemInfo> workspaceItems,
+                final ArrayList<LauncherAppWidgetInfo> appWidgets,
+                final HashMap<Long, FolderInfo> folders,
+                ArrayList<Runnable> deferredBindRunnables) {
+
+            final boolean postOnMainThread = (deferredBindRunnables != null);
+
+            // Bind the workspace items
+            int N = workspaceItems.size();
+            for (int i = 0; i < N; i += ITEMS_CHUNK) {
+                final int start = i;
+                final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
+                final Runnable r = new Runnable() {
+                    @Override
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            callbacks.bindItems(workspaceItems, start, start+chunkSize);
+                        }
+                    }
+                };
+                if (postOnMainThread) {
+                    deferredBindRunnables.add(r);
+                } else {
+                    runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+                }
+            }
+
+            // Bind the folders
+            if (!folders.isEmpty()) {
+                final Runnable r = new Runnable() {
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            callbacks.bindFolders(folders);
+                        }
+                    }
+                };
+                if (postOnMainThread) {
+                    deferredBindRunnables.add(r);
+                } else {
+                    runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+                }
+            }
+
+            // Bind the widgets, one at a time
+            N = appWidgets.size();
+            for (int i = 0; i < N; i++) {
+                final LauncherAppWidgetInfo widget = appWidgets.get(i);
+                final Runnable r = new Runnable() {
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            callbacks.bindAppWidget(widget);
+                        }
+                    }
+                };
+                if (postOnMainThread) {
+                    deferredBindRunnables.add(r);
+                } else {
+                    runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+                }
+            }
+        }
+
+        /**
+         * Binds all loaded data to actual views on the main thread.
+         */
+        private void bindWorkspace(int synchronizeBindPage) {
+            final long t = SystemClock.uptimeMillis();
+            Runnable r;
+
+            // Don't use these two variables in any of the callback runnables.
+            // Otherwise we hold a reference to them.
+            final Callbacks oldCallbacks = mCallbacks.get();
+            if (oldCallbacks == null) {
+                // This launcher has exited and nobody bothered to tell us.  Just bail.
+                Log.w(TAG, "LoaderTask running with no launcher");
+                return;
+            }
+
+            final boolean isLoadingSynchronously = (synchronizeBindPage > -1);
+            final int currentScreen = isLoadingSynchronously ? synchronizeBindPage :
+                oldCallbacks.getCurrentWorkspaceScreen();
+
+            // Load all the items that are on the current page first (and in the process, unbind
+            // all the existing workspace items before we call startBinding() below.
+            unbindWorkspaceItemsOnMainThread();
+            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>();
+            synchronized (sBgLock) {
+                workspaceItems.addAll(sBgWorkspaceItems);
+                appWidgets.addAll(sBgAppWidgets);
+                folders.putAll(sBgFolders);
+                itemsIdMap.putAll(sBgItemsIdMap);
+            }
+
+            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>();
+            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>();
+            ArrayList<LauncherAppWidgetInfo> currentAppWidgets =
+                    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>();
+
+            // Separate the items that are on the current screen, and all the other remaining items
+            filterCurrentWorkspaceItems(currentScreen, workspaceItems, currentWorkspaceItems,
+                    otherWorkspaceItems);
+            filterCurrentAppWidgets(currentScreen, appWidgets, currentAppWidgets,
+                    otherAppWidgets);
+            filterCurrentFolders(currentScreen, itemsIdMap, folders, currentFolders,
+                    otherFolders);
+            sortWorkspaceItemsSpatially(currentWorkspaceItems);
+            sortWorkspaceItemsSpatially(otherWorkspaceItems);
+
+            // Tell the workspace that we're about to start binding items
+            r = new Runnable() {
+                public void run() {
+                    Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                    if (callbacks != null) {
+                        callbacks.startBinding();
+                    }
+                }
+            };
+            runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+
+            // Load items on the current page
+            bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets,
+                    currentFolders, null);
+            if (isLoadingSynchronously) {
+                r = new Runnable() {
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            callbacks.onPageBoundSynchronously(currentScreen);
+                        }
+                    }
+                };
+                runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+            }
+
+            // Load all the remaining pages (if we are loading synchronously, we want to defer this
+            // work until after the first render)
+            mDeferredBindRunnables.clear();
+            bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders,
+                    (isLoadingSynchronously ? mDeferredBindRunnables : null));
+
+            // Tell the workspace that we're done binding items
+            r = new Runnable() {
+                public void run() {
+                    Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                    if (callbacks != null) {
+                        callbacks.finishBindingItems();
+                    }
+
+                    // If we're profiling, ensure this is the last thing in the queue.
+                    if (DEBUG_LOADERS) {
+                        Log.d(TAG, "bound workspace in "
+                            + (SystemClock.uptimeMillis()-t) + "ms");
+                    }
+
+                    mIsLoadingAndBindingWorkspace = false;
+                }
+            };
+            if (isLoadingSynchronously) {
+                mDeferredBindRunnables.add(r);
+            } else {
+                runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE);
+            }
+        }
+
+        private void loadAndBindAllApps() {
+            if (DEBUG_LOADERS) {
+                Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
+            }
+            if (!mAllAppsLoaded) {
+                loadAllAppsByBatch();
+                synchronized (LoaderTask.this) {
+                    if (mStopped) {
+                        return;
+                    }
+                    mAllAppsLoaded = true;
+                }
+            } else {
+                onlyBindAllApps();
+            }
+        }
+
+        private void onlyBindAllApps() {
+            final Callbacks oldCallbacks = mCallbacks.get();
+            if (oldCallbacks == null) {
+                // This launcher has exited and nobody bothered to tell us.  Just bail.
+                Log.w(TAG, "LoaderTask running with no launcher (onlyBindAllApps)");
+                return;
+            }
+
+            // shallow copy
+            @SuppressWarnings("unchecked")
+            final ArrayList<ApplicationInfo> list
+                    = (ArrayList<ApplicationInfo>) mBgAllAppsList.data.clone();
+            Runnable r = new Runnable() {
+                public void run() {
+                    final long t = SystemClock.uptimeMillis();
+                    final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                    if (callbacks != null) {
+                        callbacks.bindAllApplications(list);
+                    }
+                    if (DEBUG_LOADERS) {
+                        Log.d(TAG, "bound all " + list.size() + " apps from cache in "
+                                + (SystemClock.uptimeMillis()-t) + "ms");
+                    }
+                }
+            };
+            boolean isRunningOnMainThread = !(sWorkerThread.getThreadId() == Process.myTid());
+            if (oldCallbacks.isAllAppsVisible() && isRunningOnMainThread) {
+                r.run();
+            } else {
+                mHandler.post(r);
+            }
+        }
+
+        private void loadAllAppsByBatch() {
+            final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
+
+            // Don't use these two variables in any of the callback runnables.
+            // Otherwise we hold a reference to them.
+            final Callbacks oldCallbacks = mCallbacks.get();
+            if (oldCallbacks == null) {
+                // This launcher has exited and nobody bothered to tell us.  Just bail.
+                Log.w(TAG, "LoaderTask running with no launcher (loadAllAppsByBatch)");
+                return;
+            }
+
+            final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+
+            final PackageManager packageManager = mContext.getPackageManager();
+            List<ResolveInfo> apps = null;
+
+            int N = Integer.MAX_VALUE;
+
+            int startIndex;
+            int i=0;
+            int batchSize = -1;
+            while (i < N && !mStopped) {
+                if (i == 0) {
+                    mBgAllAppsList.clear();
+                    final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
+                    apps = packageManager.queryIntentActivities(mainIntent, 0);
+                    if (DEBUG_LOADERS) {
+                        Log.d(TAG, "queryIntentActivities took "
+                                + (SystemClock.uptimeMillis()-qiaTime) + "ms");
+                    }
+                    if (apps == null) {
+                        return;
+                    }
+                    N = apps.size();
+                    if (DEBUG_LOADERS) {
+                        Log.d(TAG, "queryIntentActivities got " + N + " apps");
+                    }
+                    if (N == 0) {
+                        // There are no apps?!?
+                        return;
+                    }
+                    if (mBatchSize == 0) {
+                        batchSize = N;
+                    } else {
+                        batchSize = mBatchSize;
+                    }
+
+                    final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
+                    Collections.sort(apps,
+                            new LauncherModel.ShortcutNameComparator(packageManager, mLabelCache));
+                    if (DEBUG_LOADERS) {
+                        Log.d(TAG, "sort took "
+                                + (SystemClock.uptimeMillis()-sortTime) + "ms");
+                    }
+                }
+
+                final long t2 = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
+
+                startIndex = i;
+                for (int j=0; i<N && j<batchSize; j++) {
+                    // This builds the icon bitmaps.
+                    mBgAllAppsList.add(new ApplicationInfo(packageManager, apps.get(i),
+                            mIconCache, mLabelCache));
+                    i++;
+                }
+
+                final boolean first = i <= batchSize;
+                final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                final ArrayList<ApplicationInfo> added = mBgAllAppsList.added;
+                mBgAllAppsList.added = new ArrayList<ApplicationInfo>();
+
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        final long t = SystemClock.uptimeMillis();
+                        if (callbacks != null) {
+                            if (first) {
+                                callbacks.bindAllApplications(added);
+                            } else {
+                                callbacks.bindAppsAdded(added);
+                            }
+                            if (DEBUG_LOADERS) {
+                                Log.d(TAG, "bound " + added.size() + " apps in "
+                                    + (SystemClock.uptimeMillis() - t) + "ms");
+                            }
+                        } else {
+                            Log.i(TAG, "not binding apps: no Launcher activity");
+                        }
+                    }
+                });
+
+                if (DEBUG_LOADERS) {
+                    Log.d(TAG, "batch of " + (i-startIndex) + " icons processed in "
+                            + (SystemClock.uptimeMillis()-t2) + "ms");
+                }
+
+                if (mAllAppsLoadDelay > 0 && i < N) {
+                    try {
+                        if (DEBUG_LOADERS) {
+                            Log.d(TAG, "sleeping for " + mAllAppsLoadDelay + "ms");
+                        }
+                        Thread.sleep(mAllAppsLoadDelay);
+                    } catch (InterruptedException exc) { }
+                }
+            }
+
+            if (DEBUG_LOADERS) {
+                Log.d(TAG, "cached all " + N + " apps in "
+                        + (SystemClock.uptimeMillis()-t) + "ms"
+                        + (mAllAppsLoadDelay > 0 ? " (including delay)" : ""));
+            }
+        }
+
+        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());
+            }
+        }
+    }
+
+    void enqueuePackageUpdated(PackageUpdatedTask task) {
+        sWorker.post(task);
+    }
+
+    private class PackageUpdatedTask implements Runnable {
+        int mOp;
+        String[] mPackages;
+
+        public static final int OP_NONE = 0;
+        public static final int OP_ADD = 1;
+        public static final int OP_UPDATE = 2;
+        public static final int OP_REMOVE = 3; // uninstlled
+        public static final int OP_UNAVAILABLE = 4; // external media unmounted
+
+
+        public PackageUpdatedTask(int op, String[] packages) {
+            mOp = op;
+            mPackages = packages;
+        }
+
+        public void run() {
+            final Context context = mApp;
+
+            final String[] packages = mPackages;
+            final int N = packages.length;
+            switch (mOp) {
+                case OP_ADD:
+                    for (int i=0; i<N; i++) {
+                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
+                        mBgAllAppsList.addPackage(context, packages[i]);
+                    }
+                    break;
+                case OP_UPDATE:
+                    for (int i=0; i<N; i++) {
+                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
+                        mBgAllAppsList.updatePackage(context, packages[i]);
+                        LauncherApplication app =
+                                (LauncherApplication) context.getApplicationContext();
+                        WidgetPreviewLoader.removeFromDb(
+                                app.getWidgetPreviewCacheDb(), packages[i]);
+                    }
+                    break;
+                case OP_REMOVE:
+                case OP_UNAVAILABLE:
+                    for (int i=0; i<N; i++) {
+                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
+                        mBgAllAppsList.removePackage(packages[i]);
+                        LauncherApplication app =
+                                (LauncherApplication) context.getApplicationContext();
+                        WidgetPreviewLoader.removeFromDb(
+                                app.getWidgetPreviewCacheDb(), packages[i]);
+                    }
+                    break;
+            }
+
+            ArrayList<ApplicationInfo> added = null;
+            ArrayList<ApplicationInfo> modified = null;
+            final ArrayList<ApplicationInfo> removedApps = new ArrayList<ApplicationInfo>();
+
+            if (mBgAllAppsList.added.size() > 0) {
+                added = new ArrayList<ApplicationInfo>(mBgAllAppsList.added);
+                mBgAllAppsList.added.clear();
+            }
+            if (mBgAllAppsList.modified.size() > 0) {
+                modified = new ArrayList<ApplicationInfo>(mBgAllAppsList.modified);
+                mBgAllAppsList.modified.clear();
+            }
+            if (mBgAllAppsList.removed.size() > 0) {
+                removedApps.addAll(mBgAllAppsList.removed);
+                mBgAllAppsList.removed.clear();
+            }
+
+            final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null;
+            if (callbacks == null) {
+                Log.w(TAG, "Nobody to tell about the new app.  Launcher is probably loading.");
+                return;
+            }
+
+            if (added != null) {
+                final ArrayList<ApplicationInfo> addedFinal = added;
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
+                        if (callbacks == cb && cb != null) {
+                            callbacks.bindAppsAdded(addedFinal);
+                        }
+                    }
+                });
+            }
+            if (modified != null) {
+                final ArrayList<ApplicationInfo> modifiedFinal = modified;
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
+                        if (callbacks == cb && cb != null) {
+                            callbacks.bindAppsUpdated(modifiedFinal);
+                        }
+                    }
+                });
+            }
+            // If a package has been removed, or an app has been removed as a result of
+            // an update (for example), make the removed callback.
+            if (mOp == OP_REMOVE || !removedApps.isEmpty()) {
+                final boolean permanent = (mOp == OP_REMOVE);
+                final ArrayList<String> removedPackageNames =
+                        new ArrayList<String>(Arrays.asList(packages));
+
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
+                        if (callbacks == cb && cb != null) {
+                            callbacks.bindComponentsRemoved(removedPackageNames,
+                                    removedApps, permanent);
+                        }
+                    }
+                });
+            }
+
+            final ArrayList<Object> widgetsAndShortcuts =
+                getSortedWidgetsAndShortcuts(context);
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
+                    if (callbacks == cb && cb != null) {
+                        callbacks.bindPackagesUpdated(widgetsAndShortcuts);
+                    }
+                }
+            });
+        }
+    }
+
+    // Returns a list of ResolveInfos/AppWindowInfos in sorted order
+    public static ArrayList<Object> getSortedWidgetsAndShortcuts(Context context) {
+        PackageManager packageManager = context.getPackageManager();
+        final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
+        widgetsAndShortcuts.addAll(AppWidgetManager.getInstance(context).getInstalledProviders());
+        Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+        widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0));
+        Collections.sort(widgetsAndShortcuts,
+            new LauncherModel.WidgetAndShortcutNameComparator(packageManager));
+        return widgetsAndShortcuts;
+    }
+
+    /**
+     * 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, Context context) {
+        return getShortcutInfo(manager, intent, context, null, -1, -1, null);
+    }
+
+    /**
+     * 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, Context context,
+            Cursor c, int iconIndex, int titleIndex, HashMap<Object, CharSequence> labelCache) {
+        Bitmap icon = null;
+        final ShortcutInfo info = new ShortcutInfo();
+
+        ComponentName componentName = intent.getComponent();
+        if (componentName == null) {
+            return null;
+        }
+
+        try {
+            PackageInfo pi = manager.getPackageInfo(componentName.getPackageName(), 0);
+            if (!pi.applicationInfo.enabled) {
+                // If we return null here, the corresponding item will be removed from the launcher
+                // db and will not appear in the workspace.
+                return null;
+            }
+        } catch (NameNotFoundException e) {
+            Log.d(TAG, "getPackInfo failed for package " + componentName.getPackageName());
+        }
+
+        // TODO: See if the PackageManager knows about this case.  If it doesn't
+        // then return null & delete this.
+
+        // 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.
+
+        // Attempt to use queryIntentActivities to get the ResolveInfo (with IntentFilter info) and
+        // if that fails, or is ambiguious, fallback to the standard way of getting the resolve info
+        // via resolveActivity().
+        ResolveInfo resolveInfo = null;
+        ComponentName oldComponent = intent.getComponent();
+        Intent newIntent = new Intent(intent.getAction(), null);
+        newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        newIntent.setPackage(oldComponent.getPackageName());
+        List<ResolveInfo> infos = manager.queryIntentActivities(newIntent, 0);
+        for (ResolveInfo i : infos) {
+            ComponentName cn = new ComponentName(i.activityInfo.packageName,
+                    i.activityInfo.name);
+            if (cn.equals(oldComponent)) {
+                resolveInfo = i;
+            }
+        }
+        if (resolveInfo == null) {
+            resolveInfo = manager.resolveActivity(intent, 0);
+        }
+        if (resolveInfo != null) {
+            icon = mIconCache.getIcon(componentName, resolveInfo, labelCache);
+        }
+        // the db
+        if (icon == null) {
+            if (c != null) {
+                icon = getIconFromCursor(c, iconIndex, context);
+            }
+        }
+        // the fallback icon
+        if (icon == null) {
+            icon = getFallbackIcon();
+            info.usingFallbackIcon = true;
+        }
+        info.setIcon(icon);
+
+        // from the resource
+        if (resolveInfo != null) {
+            ComponentName key = LauncherModel.getComponentNameFromResolveInfo(resolveInfo);
+            if (labelCache != null && labelCache.containsKey(key)) {
+                info.title = labelCache.get(key);
+            } else {
+                info.title = resolveInfo.activityInfo.loadLabel(manager);
+                if (labelCache != null) {
+                    labelCache.put(key, info.title);
+                }
+            }
+        }
+        // from the db
+        if (info.title == null) {
+            if (c != null) {
+                info.title =  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;
+        return info;
+    }
+
+    /**
+     * Returns the set of workspace ShortcutInfos with the specified intent.
+     */
+    static ArrayList<ItemInfo> getWorkspaceShortcutItemInfosWithIntent(Intent intent) {
+        ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
+        synchronized (sBgLock) {
+            for (ItemInfo info : sBgWorkspaceItems) {
+                if (info instanceof ShortcutInfo) {
+                    ShortcutInfo shortcut = (ShortcutInfo) info;
+                    if (shortcut.intent.toUri(0).equals(intent.toUri(0))) {
+                        items.add(shortcut);
+                    }
+                }
+            }
+        }
+        return items;
+    }
+
+    /**
+     * 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;
+        final ShortcutInfo info = new ShortcutInfo();
+        info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+
+        // 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);
+            PackageManager packageManager = context.getPackageManager();
+            info.customIcon = false;
+            // the resource
+            try {
+                Resources resources = packageManager.getResourcesForApplication(packageName);
+                if (resources != null) {
+                    final int id = resources.getIdentifier(resourceName, null, null);
+                    icon = Utilities.createIconBitmap(
+                            mIconCache.getFullResIcon(resources, id), context);
+                }
+            } catch (Exception e) {
+                // drop this.  we have other places to look for icons
+            }
+            // the db
+            if (icon == null) {
+                icon = getIconFromCursor(c, iconIndex, context);
+            }
+            // the fallback icon
+            if (icon == null) {
+                icon = getFallbackIcon();
+                info.usingFallbackIcon = true;
+            }
+            break;
+        case LauncherSettings.Favorites.ICON_TYPE_BITMAP:
+            icon = getIconFromCursor(c, iconIndex, context);
+            if (icon == null) {
+                icon = getFallbackIcon();
+                info.customIcon = false;
+                info.usingFallbackIcon = true;
+            } else {
+                info.customIcon = true;
+            }
+            break;
+        default:
+            icon = getFallbackIcon();
+            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;
+        }
+    }
+
+    ShortcutInfo addShortcut(Context context, Intent data, long container, int screen,
+            int cellX, int cellY, boolean notify) {
+        final ShortcutInfo info = infoFromShortcutIntent(context, data, null);
+        if (info == null) {
+            return null;
+        }
+        addItemToDatabase(context, info, container, screen, cellX, cellY, notify);
+
+        return info;
+    }
+
+    /**
+     * Attempts to find an AppWidgetProviderInfo that matches the given component.
+     */
+    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;
+    }
+
+    /**
+     * Returns a list of all the widgets that can handle configuration with a particular mimeType.
+     */
+    List<WidgetMimeTypeHandlerData> resolveWidgetsForMimeType(Context context, String mimeType) {
+        final PackageManager packageManager = context.getPackageManager();
+        final List<WidgetMimeTypeHandlerData> supportedConfigurationActivities =
+            new ArrayList<WidgetMimeTypeHandlerData>();
+
+        final Intent supportsIntent =
+            new Intent(InstallWidgetReceiver.ACTION_SUPPORTS_CLIPDATA_MIMETYPE);
+        supportsIntent.setType(mimeType);
+
+        // Create a set of widget configuration components that we can test against
+        final List<AppWidgetProviderInfo> widgets =
+            AppWidgetManager.getInstance(context).getInstalledProviders();
+        final HashMap<ComponentName, AppWidgetProviderInfo> configurationComponentToWidget =
+            new HashMap<ComponentName, AppWidgetProviderInfo>();
+        for (AppWidgetProviderInfo info : widgets) {
+            configurationComponentToWidget.put(info.configure, info);
+        }
+
+        // Run through each of the intents that can handle this type of clip data, and cross
+        // reference them with the components that are actual configuration components
+        final List<ResolveInfo> activities = packageManager.queryIntentActivities(supportsIntent,
+                PackageManager.MATCH_DEFAULT_ONLY);
+        for (ResolveInfo info : activities) {
+            final ActivityInfo activityInfo = info.activityInfo;
+            final ComponentName infoComponent = new ComponentName(activityInfo.packageName,
+                    activityInfo.name);
+            if (configurationComponentToWidget.containsKey(infoComponent)) {
+                supportedConfigurationActivities.add(
+                        new InstallWidgetReceiver.WidgetMimeTypeHandlerData(info,
+                                configurationComponentToWidget.get(infoComponent)));
+            }
+        }
+        return supportedConfigurationActivities;
+    }
+
+    ShortcutInfo infoFromShortcutIntent(Context context, Intent data, Bitmap fallbackIcon) {
+        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
+        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
+        Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+
+        if (intent == null) {
+            // If the intent is null, we can't construct a valid ShortcutInfo, so we return null
+            Log.e(TAG, "Can't construct ShorcutInfo with null intent");
+            return null;
+        }
+
+        Bitmap icon = null;
+        boolean customIcon = false;
+        ShortcutIconResource iconResource = null;
+
+        if (bitmap != null && bitmap instanceof Bitmap) {
+            icon = Utilities.createIconBitmap(new FastBitmapDrawable((Bitmap)bitmap), context);
+            customIcon = true;
+        } else {
+            Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+            if (extra != null && extra instanceof ShortcutIconResource) {
+                try {
+                    iconResource = (ShortcutIconResource) extra;
+                    final PackageManager packageManager = context.getPackageManager();
+                    Resources resources = packageManager.getResourcesForApplication(
+                            iconResource.packageName);
+                    final int id = resources.getIdentifier(iconResource.resourceName, null, null);
+                    icon = Utilities.createIconBitmap(
+                            mIconCache.getFullResIcon(resources, id), context);
+                } catch (Exception e) {
+                    Log.w(TAG, "Could not load shortcut icon: " + extra);
+                }
+            }
+        }
+
+        final ShortcutInfo info = new ShortcutInfo();
+
+        if (icon == null) {
+            if (fallbackIcon != null) {
+                icon = fallbackIcon;
+            } else {
+                icon = getFallbackIcon();
+                info.usingFallbackIcon = true;
+            }
+        }
+        info.setIcon(icon);
+
+        info.title = name;
+        info.intent = intent;
+        info.customIcon = customIcon;
+        info.iconResource = iconResource;
+
+        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 (!mAppsCanBeOnExternalStorage) {
+            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) {
+        // See if a placeholder was created for us already
+        FolderInfo folderInfo = folders.get(id);
+        if (folderInfo == null) {
+            // No placeholder -- create a new instance
+            folderInfo = new FolderInfo();
+            folders.put(id, folderInfo);
+        }
+        return folderInfo;
+    }
+
+    public static final Comparator<ApplicationInfo> getAppNameComparator() {
+        final Collator collator = Collator.getInstance();
+        return new Comparator<ApplicationInfo>() {
+            public final int compare(ApplicationInfo a, ApplicationInfo b) {
+                int result = collator.compare(a.title.toString(), b.title.toString());
+                if (result == 0) {
+                    result = a.componentName.compareTo(b.componentName);
+                }
+                return result;
+            }
+        };
+    }
+    public static final Comparator<ApplicationInfo> APP_INSTALL_TIME_COMPARATOR
+            = new Comparator<ApplicationInfo>() {
+        public final int compare(ApplicationInfo a, ApplicationInfo b) {
+            if (a.firstInstallTime < b.firstInstallTime) return 1;
+            if (a.firstInstallTime > b.firstInstallTime) return -1;
+            return 0;
+        }
+    };
+    public static final Comparator<AppWidgetProviderInfo> getWidgetNameComparator() {
+        final Collator collator = Collator.getInstance();
+        return new Comparator<AppWidgetProviderInfo>() {
+            public final int compare(AppWidgetProviderInfo a, AppWidgetProviderInfo b) {
+                return collator.compare(a.label.toString(), b.label.toString());
+            }
+        };
+    }
+    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<ResolveInfo> {
+        private Collator mCollator;
+        private PackageManager mPackageManager;
+        private HashMap<Object, CharSequence> mLabelCache;
+        ShortcutNameComparator(PackageManager pm) {
+            mPackageManager = pm;
+            mLabelCache = new HashMap<Object, CharSequence>();
+            mCollator = Collator.getInstance();
+        }
+        ShortcutNameComparator(PackageManager pm, HashMap<Object, CharSequence> labelCache) {
+            mPackageManager = pm;
+            mLabelCache = labelCache;
+            mCollator = Collator.getInstance();
+        }
+        public final int compare(ResolveInfo a, ResolveInfo b) {
+            CharSequence labelA, labelB;
+            ComponentName keyA = LauncherModel.getComponentNameFromResolveInfo(a);
+            ComponentName keyB = LauncherModel.getComponentNameFromResolveInfo(b);
+            if (mLabelCache.containsKey(keyA)) {
+                labelA = mLabelCache.get(keyA);
+            } else {
+                labelA = a.loadLabel(mPackageManager).toString();
+
+                mLabelCache.put(keyA, labelA);
+            }
+            if (mLabelCache.containsKey(keyB)) {
+                labelB = mLabelCache.get(keyB);
+            } else {
+                labelB = b.loadLabel(mPackageManager).toString();
+
+                mLabelCache.put(keyB, labelB);
+            }
+            return mCollator.compare(labelA, labelB);
+        }
+    };
+    public static class WidgetAndShortcutNameComparator implements Comparator<Object> {
+        private Collator mCollator;
+        private PackageManager mPackageManager;
+        private HashMap<Object, String> mLabelCache;
+        WidgetAndShortcutNameComparator(PackageManager pm) {
+            mPackageManager = pm;
+            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) ?
+                    ((AppWidgetProviderInfo) a).label :
+                    ((ResolveInfo) a).loadLabel(mPackageManager).toString();
+                mLabelCache.put(a, labelA);
+            }
+            if (mLabelCache.containsKey(b)) {
+                labelB = mLabelCache.get(b);
+            } else {
+                labelB = (b instanceof AppWidgetProviderInfo) ?
+                    ((AppWidgetProviderInfo) b).label :
+                    ((ResolveInfo) b).loadLabel(mPackageManager).toString();
+                mLabelCache.put(b, labelB);
+            }
+            return mCollator.compare(labelA, labelB);
+        }
+    };
+
+    public void dumpState() {
+        Log.d(TAG, "mCallbacks=" + mCallbacks);
+        ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mBgAllAppsList.data);
+        ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.added", mBgAllAppsList.added);
+        ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.removed", mBgAllAppsList.removed);
+        ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.modified", mBgAllAppsList.modified);
+        if (mLoaderTask != null) {
+            mLoaderTask.dumpState();
+        } else {
+            Log.d(TAG, "mLoaderTask=null");
+        }
+    }
+}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
new file mode 100644
index 0000000..fb12f71
--- /dev/null
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -0,0 +1,1193 @@
+/*
+ * 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.app.SearchManager;
+import android.appwidget.AppWidgetHost;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.database.SQLException;
+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.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.launcher3.R;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+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.List;
+
+public class LauncherProvider extends ContentProvider {
+    private static final String TAG = "Launcher.LauncherProvider";
+    private static final boolean LOGD = false;
+
+    private static final String DATABASE_NAME = "launcher.db";
+
+    private static final int DATABASE_VERSION = 12;
+
+    static final String AUTHORITY = "com.android.launcher3.settings";
+
+    static final String TABLE_FAVORITES = "favorites";
+    static final String PARAMETER_NOTIFY = "notify";
+    static final String DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED =
+            "DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED";
+    static final String DEFAULT_WORKSPACE_RESOURCE_ID =
+            "DEFAULT_WORKSPACE_RESOURCE_ID";
+
+    private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
+            "com.android.launcher3.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
+
+    /**
+     * {@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;
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new DatabaseHelper(getContext());
+        ((LauncherApplication) getContext()).setLauncherProvider(this);
+        return true;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        SqlArguments args = new SqlArguments(uri, null, null);
+        if (TextUtils.isEmpty(args.where)) {
+            return "vnd.android.cursor.dir/" + args.table;
+        } else {
+            return "vnd.android.cursor.item/" + args.table;
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+
+        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(args.table);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
+        result.setNotificationUri(getContext().getContentResolver(), uri);
+
+        return result;
+    }
+
+    private static long dbInsertAndCheck(DatabaseHelper helper,
+            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
+        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
+            throw new RuntimeException("Error: attempting to add item without specifying an id");
+        }
+        return db.insert(table, nullColumnHack, values);
+    }
+
+    private static void deleteId(SQLiteDatabase db, long id) {
+        Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
+        SqlArguments args = new SqlArguments(uri, null, null);
+        db.delete(args.table, args.where, args.args);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues initialValues) {
+        SqlArguments args = new SqlArguments(uri);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
+        if (rowId <= 0) return null;
+
+        uri = ContentUris.withAppendedId(uri, rowId);
+        sendNotify(uri);
+
+        return uri;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        SqlArguments args = new SqlArguments(uri);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            int numValues = values.length;
+            for (int i = 0; i < numValues; i++) {
+                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
+                    return 0;
+                }
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+
+        sendNotify(uri);
+        return values.length;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int count = db.delete(args.table, args.where, args.args);
+        if (count > 0) sendNotify(uri);
+
+        return count;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int count = db.update(args.table, values, args.where, args.args);
+        if (count > 0) sendNotify(uri);
+
+        return count;
+    }
+
+    private void sendNotify(Uri uri) {
+        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
+        if (notify == null || "true".equals(notify)) {
+            getContext().getContentResolver().notifyChange(uri, null);
+        }
+    }
+
+    public long generateNewId() {
+        return mOpenHelper.generateNewId();
+    }
+
+    /**
+     * @param workspaceResId that can be 0 to use default or non-zero for specific resource
+     */
+    synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) {
+        String spKey = LauncherApplication.getSharedPreferencesKey();
+        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
+        if (sp.getBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, false)) {
+            int workspaceResId = origWorkspaceResId;
+
+            // Use default workspace resource if none provided
+            if (workspaceResId == 0) {
+                workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace);
+            }
+
+            // Populate favorites table with initial favorites
+            SharedPreferences.Editor editor = sp.edit();
+            editor.remove(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED);
+            if (origWorkspaceResId != 0) {
+                editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId);
+            }
+            mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId);
+            editor.commit();
+        }
+    }
+
+    private static class DatabaseHelper extends SQLiteOpenHelper {
+        private static final String TAG_FAVORITES = "favorites";
+        private static final String TAG_FAVORITE = "favorite";
+        private static final String TAG_CLOCK = "clock";
+        private static final String TAG_SEARCH = "search";
+        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_EXTRA = "extra";
+
+        private final Context mContext;
+        private final AppWidgetHost mAppWidgetHost;
+        private long mMaxId = -1;
+
+        DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+            mContext = context;
+            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
+
+            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
+            // the DB here
+            if (mMaxId == -1) {
+                mMaxId = initializeMaxId(getWritableDatabase());
+            }
+        }
+
+        /**
+         * 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");
+
+            mMaxId = 1;
+
+            db.execSQL("CREATE TABLE favorites (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    "title TEXT," +
+                    "intent TEXT," +
+                    "container INTEGER," +
+                    "screen INTEGER," +
+                    "cellX INTEGER," +
+                    "cellY INTEGER," +
+                    "spanX INTEGER," +
+                    "spanY INTEGER," +
+                    "itemType INTEGER," +
+                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
+                    "isShortcut INTEGER," +
+                    "iconType INTEGER," +
+                    "iconPackage TEXT," +
+                    "iconResource TEXT," +
+                    "icon BLOB," +
+                    "uri TEXT," +
+                    "displayMode INTEGER" +
+                    ");");
+
+            // Database was just created, so wipe any previous widgets
+            if (mAppWidgetHost != null) {
+                mAppWidgetHost.deleteHost();
+                sendAppWidgetResetNotify();
+            }
+
+            if (!convertDatabase(db)) {
+                // Set a shared pref so that we know we need to load the default workspace later
+                setFlagToLoadDefaultWorkspaceLater();
+            }
+        }
+
+        private void setFlagToLoadDefaultWorkspaceLater() {
+            String spKey = LauncherApplication.getSharedPreferencesKey();
+            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+            SharedPreferences.Editor editor = sp.edit();
+            editor.putBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, true);
+            editor.commit();
+        }
+
+        private boolean convertDatabase(SQLiteDatabase db) {
+            if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
+            boolean converted = false;
+
+            final Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
+                    "/old_favorites?notify=true");
+            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 && cursor.getCount() > 0) {
+                try {
+                    converted = copyFromCursor(db, cursor) > 0;
+                } finally {
+                    cursor.close();
+                }
+
+                if (converted) {
+                    resolver.delete(uri, null, null);
+                }
+            }
+
+            if (converted) {
+                // Convert widgets from this import into widgets
+                if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
+                convertWidgets(db);
+            }
+
+            return converted;
+        }
+
+        private int copyFromCursor(SQLiteDatabase db, Cursor c) {
+            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));
+                rows[i++] = values;
+            }
+
+            db.beginTransaction();
+            int total = 0;
+            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;
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (LOGD) Log.d(TAG, "onUpgrade triggered");
+
+            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();
+                }
+
+                // Convert existing widgets only if table upgrade was successful
+                if (version == 3) {
+                    convertWidgets(db);
+                }
+            }
+
+            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();
+                }
+
+               // We added the fast track.
+                if (updateContactsShortcuts(db)) {
+                    version = 6;
+                }
+            }
+
+            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 (mMaxId == -1) {
+                    mMaxId = initializeMaxId(db);
+                }
+
+                // Add default hotseat icons
+                loadFavorites(db, 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 != DATABASE_VERSION) {
+                Log.w(TAG, "Destroying all old data.");
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
+                onCreate(db);
+            }
+        }
+
+        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";
+            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());
+
+                final int idIndex = c.getColumnIndex(Favorites._ID);
+                final int intentIndex = c.getColumnIndex(Favorites.INTENT);
+
+                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.setTransactionSuccessful();
+            } catch (SQLException ex) {
+                Log.w(TAG, "Problem while upgrading contacts", ex);
+                return false;
+            } finally {
+                db.endTransaction();
+                if (c != null) {
+                    c.close();
+                }
+            }
+
+            return true;
+        }
+
+        private void normalizeIcons(SQLiteDatabase db) {
+            Log.d(TAG, "normalizing icons");
+
+            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.resampleIconBitmap(
+                                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.setTransactionSuccessful();
+            } catch (SQLException ex) {
+                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
+            } finally {
+                db.endTransaction();
+                if (update != null) {
+                    update.close();
+                }
+                if (c != null) {
+                    c.close();
+                }
+            }
+        }
+
+        // Generates a new ID to use for an object in your database. This method should be only
+        // called from the main UI thread. As an exception, we do call it when we call the
+        // constructor from the worker thread; however, this doesn't extend until after the
+        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
+        // after that point
+        public long generateNewId() {
+            if (mMaxId < 0) {
+                throw new RuntimeException("Error: max id was not initialized");
+            }
+            mMaxId += 1;
+            return mMaxId;
+        }
+
+        private long initializeMaxId(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 id");
+            }
+
+            return id;
+        }
+
+        /**
+         * 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();
+                }
+            }
+        }
+
+        private static final void beginDocument(XmlPullParser parser, String firstElementName)
+                throws XmlPullParserException, IOException {
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                ;
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new XmlPullParserException("No start tag found");
+            }
+
+            if (!parser.getName().equals(firstElementName)) {
+                throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
+                        ", expected " + firstElementName);
+            }
+        }
+
+        /**
+         * Loads the default set of favorite packages from an xml file.
+         *
+         * @param db The database to write the values into
+         * @param filterContainerId The specific container id of items to load
+         */
+        private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) {
+            Intent intent = new Intent(Intent.ACTION_MAIN, null);
+            intent.addCategory(Intent.CATEGORY_LAUNCHER);
+            ContentValues values = new ContentValues();
+
+            PackageManager packageManager = mContext.getPackageManager();
+            int allAppsButtonRank =
+                    mContext.getResources().getInteger(R.integer.hotseat_all_apps_index);
+            int i = 0;
+            try {
+                XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId);
+                AttributeSet attrs = Xml.asAttributeSet(parser);
+                beginDocument(parser, TAG_FAVORITES);
+
+                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;
+                    }
+
+                    boolean added = false;
+                    final String name = parser.getName();
+
+                    TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
+
+                    long container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+                    if (a.hasValue(R.styleable.Favorite_container)) {
+                        container = Long.valueOf(a.getString(R.styleable.Favorite_container));
+                    }
+
+                    String screen = a.getString(R.styleable.Favorite_screen);
+                    String x = a.getString(R.styleable.Favorite_x);
+                    String y = a.getString(R.styleable.Favorite_y);
+
+                    // If we are adding to the hotseat, the screen is used as the position in the
+                    // hotseat. This screen can't be at position 0 because AllApps is in the
+                    // zeroth position.
+                    if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                            && Integer.valueOf(screen) == allAppsButtonRank) {
+                        throw new RuntimeException("Invalid screen position for hotseat item");
+                    }
+
+                    values.clear();
+                    values.put(LauncherSettings.Favorites.CONTAINER, container);
+                    values.put(LauncherSettings.Favorites.SCREEN, screen);
+                    values.put(LauncherSettings.Favorites.CELLX, x);
+                    values.put(LauncherSettings.Favorites.CELLY, y);
+
+                    if (TAG_FAVORITE.equals(name)) {
+                        long id = addAppShortcut(db, values, a, packageManager, intent);
+                        added = id >= 0;
+                    } else if (TAG_SEARCH.equals(name)) {
+                        added = addSearchWidget(db, values);
+                    } else if (TAG_CLOCK.equals(name)) {
+                        added = addClockWidget(db, values);
+                    } else if (TAG_APPWIDGET.equals(name)) {
+                        added = addAppWidget(parser, attrs, type, db, values, a, packageManager);
+                    } else if (TAG_SHORTCUT.equals(name)) {
+                        long id = addUriShortcut(db, values, a);
+                        added = id >= 0;
+                    } else if (TAG_FOLDER.equals(name)) {
+                        String title;
+                        int titleResId =  a.getResourceId(R.styleable.Favorite_title, -1);
+                        if (titleResId != -1) {
+                            title = mContext.getResources().getString(titleResId);
+                        } else {
+                            title = mContext.getResources().getString(R.string.folder_name);
+                        }
+                        values.put(LauncherSettings.Favorites.TITLE, title);
+                        long folderId = addFolder(db, values);
+                        added = folderId >= 0;
+
+                        ArrayList<Long> folderItems = new ArrayList<Long>();
+
+                        int folderDepth = parser.getDepth();
+                        while ((type = parser.next()) != XmlPullParser.END_TAG ||
+                                parser.getDepth() > folderDepth) {
+                            if (type != XmlPullParser.START_TAG) {
+                                continue;
+                            }
+                            final String folder_item_name = parser.getName();
+
+                            TypedArray ar = mContext.obtainStyledAttributes(attrs,
+                                    R.styleable.Favorite);
+                            values.clear();
+                            values.put(LauncherSettings.Favorites.CONTAINER, folderId);
+
+                            if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) {
+                                long id =
+                                    addAppShortcut(db, values, ar, packageManager, intent);
+                                if (id >= 0) {
+                                    folderItems.add(id);
+                                }
+                            } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0) {
+                                long id = addUriShortcut(db, values, ar);
+                                if (id >= 0) {
+                                    folderItems.add(id);
+                                }
+                            } else {
+                                throw new RuntimeException("Folders can " +
+                                        "contain only shortcuts");
+                            }
+                            ar.recycle();
+                        }
+                        // We can only have folders with >= 2 items, so we need to remove the
+                        // folder and clean up if less than 2 items were included, or some
+                        // failed to add, and less than 2 were actually added
+                        if (folderItems.size() < 2 && folderId >= 0) {
+                            // We just delete the folder and any items that made it
+                            deleteId(db, folderId);
+                            if (folderItems.size() > 0) {
+                                deleteId(db, folderItems.get(0));
+                            }
+                            added = false;
+                        }
+                    }
+                    if (added) i++;
+                    a.recycle();
+                }
+            } catch (XmlPullParserException e) {
+                Log.w(TAG, "Got exception parsing favorites.", e);
+            } catch (IOException e) {
+                Log.w(TAG, "Got exception parsing favorites.", e);
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Got exception parsing favorites.", e);
+            }
+
+            return i;
+        }
+
+        private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a,
+                PackageManager packageManager, Intent intent) {
+            long id = -1;
+            ActivityInfo info;
+            String packageName = a.getString(R.styleable.Favorite_packageName);
+            String className = a.getString(R.styleable.Favorite_className);
+            try {
+                ComponentName cn;
+                try {
+                    cn = new ComponentName(packageName, className);
+                    info = packageManager.getActivityInfo(cn, 0);
+                } catch (PackageManager.NameNotFoundException nnfe) {
+                    String[] packages = packageManager.currentToCanonicalPackageNames(
+                        new String[] { packageName });
+                    cn = new ComponentName(packages[0], className);
+                    info = packageManager.getActivityInfo(cn, 0);
+                }
+                id = generateNewId();
+                intent.setComponent(cn);
+                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                values.put(Favorites.INTENT, intent.toUri(0));
+                values.put(Favorites.TITLE, info.loadLabel(packageManager).toString());
+                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
+                values.put(Favorites.SPANX, 1);
+                values.put(Favorites.SPANY, 1);
+                values.put(Favorites._ID, generateNewId());
+                if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
+                    return -1;
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(TAG, "Unable to add favorite: " + packageName +
+                        "/" + className, e);
+            }
+            return id;
+        }
+
+        private long addFolder(SQLiteDatabase db, ContentValues values) {
+            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
+            values.put(Favorites.SPANX, 1);
+            values.put(Favorites.SPANY, 1);
+            long id = generateNewId();
+            values.put(Favorites._ID, id);
+            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) {
+                return -1;
+            } else {
+                return id;
+            }
+        }
+
+        private ComponentName getSearchWidgetProvider() {
+            SearchManager searchManager =
+                    (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
+            if (searchComponent == null) return null;
+            return getProviderInPackage(searchComponent.getPackageName());
+        }
+
+        /**
+         * Gets an appwidget provider from the given package. If the package contains more than
+         * one appwidget provider, an arbitrary one is returned.
+         */
+        private ComponentName getProviderInPackage(String packageName) {
+            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
+            List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
+            if (providers == null) return null;
+            final int providerCount = providers.size();
+            for (int i = 0; i < providerCount; i++) {
+                ComponentName provider = providers.get(i).provider;
+                if (provider != null && provider.getPackageName().equals(packageName)) {
+                    return provider;
+                }
+            }
+            return null;
+        }
+
+        private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) {
+            ComponentName cn = getSearchWidgetProvider();
+            return addAppWidget(db, values, cn, 4, 1, null);
+        }
+
+        private boolean addClockWidget(SQLiteDatabase db, ContentValues values) {
+            ComponentName cn = new ComponentName("com.android.alarmclock",
+                    "com.android.alarmclock.AnalogAppWidgetProvider");
+            return addAppWidget(db, values, cn, 2, 2, null);
+        }
+
+        private boolean addAppWidget(XmlResourceParser parser, AttributeSet attrs, int type,
+                SQLiteDatabase db, ContentValues values, TypedArray a,
+                PackageManager packageManager) throws XmlPullParserException, IOException {
+
+            String packageName = a.getString(R.styleable.Favorite_packageName);
+            String className = a.getString(R.styleable.Favorite_className);
+
+            if (packageName == null || className == null) {
+                return false;
+            }
+
+            boolean hasPackage = true;
+            ComponentName cn = new ComponentName(packageName, className);
+            try {
+                packageManager.getReceiverInfo(cn, 0);
+            } catch (Exception e) {
+                String[] packages = packageManager.currentToCanonicalPackageNames(
+                        new String[] { packageName });
+                cn = new ComponentName(packages[0], className);
+                try {
+                    packageManager.getReceiverInfo(cn, 0);
+                } catch (Exception e1) {
+                    hasPackage = false;
+                }
+            }
+
+            if (hasPackage) {
+                int spanX = a.getInt(R.styleable.Favorite_spanX, 0);
+                int spanY = a.getInt(R.styleable.Favorite_spanY, 0);
+
+                // Read the extras
+                Bundle extras = new Bundle();
+                int widgetDepth = parser.getDepth();
+                while ((type = parser.next()) != XmlPullParser.END_TAG ||
+                        parser.getDepth() > widgetDepth) {
+                    if (type != XmlPullParser.START_TAG) {
+                        continue;
+                    }
+
+                    TypedArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Extra);
+                    if (TAG_EXTRA.equals(parser.getName())) {
+                        String key = ar.getString(R.styleable.Extra_key);
+                        String value = ar.getString(R.styleable.Extra_value);
+                        if (key != null && value != null) {
+                            extras.putString(key, value);
+                        } else {
+                            throw new RuntimeException("Widget extras must have a key and value");
+                        }
+                    } else {
+                        throw new RuntimeException("Widgets can contain only extras");
+                    }
+                    ar.recycle();
+                }
+
+                return addAppWidget(db, values, cn, spanX, spanY, extras);
+            }
+
+            return false;
+        }
+
+        private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn,
+                int spanX, int spanY, Bundle extras) {
+            boolean allocatedAppWidgets = false;
+            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
+
+            try {
+                int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
+
+                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
+                values.put(Favorites.SPANX, spanX);
+                values.put(Favorites.SPANY, spanY);
+                values.put(Favorites.APPWIDGET_ID, appWidgetId);
+                values.put(Favorites._ID, generateNewId());
+                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
+
+                allocatedAppWidgets = true;
+
+                // TODO: need to check return value
+                appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn);
+
+                // Send a broadcast to configure the widget
+                if (extras != null && !extras.isEmpty()) {
+                    Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
+                    intent.setComponent(cn);
+                    intent.putExtras(extras);
+                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+                    mContext.sendBroadcast(intent);
+                }
+            } catch (RuntimeException ex) {
+                Log.e(TAG, "Problem allocating appWidgetId", ex);
+            }
+
+            return allocatedAppWidgets;
+        }
+
+        private long addUriShortcut(SQLiteDatabase db, ContentValues values,
+                TypedArray a) {
+            Resources r = mContext.getResources();
+
+            final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0);
+            final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0);
+
+            Intent intent;
+            String uri = null;
+            try {
+                uri = a.getString(R.styleable.Favorite_uri);
+                intent = Intent.parseUri(uri, 0);
+            } catch (URISyntaxException e) {
+                Log.w(TAG, "Shortcut has malformed uri: " + uri);
+                return -1; // Oh well
+            }
+
+            if (iconResId == 0 || titleResId == 0) {
+                Log.w(TAG, "Shortcut is missing title or icon resource ID");
+                return -1;
+            }
+
+            long id = generateNewId();
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            values.put(Favorites.INTENT, intent.toUri(0));
+            values.put(Favorites.TITLE, r.getString(titleResId));
+            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
+            values.put(Favorites.SPANX, 1);
+            values.put(Favorites.SPANY, 1);
+            values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
+            values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
+            values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId));
+            values.put(Favorites._ID, id);
+
+            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
+                return -1;
+            }
+            return id;
+        }
+    }
+
+    /**
+     * Build a query string that will match any row where the column matches
+     * anything in the values list.
+     */
+    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 ");
+            }
+        }
+        return selectWhere.toString();
+    }
+
+    static class SqlArguments {
+        public final String table;
+        public final String where;
+        public final String[] args;
+
+        SqlArguments(Uri url, String where, String[] args) {
+            if (url.getPathSegments().size() == 1) {
+                this.table = url.getPathSegments().get(0);
+                this.where = where;
+                this.args = args;
+            } else if (url.getPathSegments().size() != 2) {
+                throw new IllegalArgumentException("Invalid URI: " + url);
+            } else if (!TextUtils.isEmpty(where)) {
+                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
+            } else {
+                this.table = url.getPathSegments().get(0);
+                this.where = "_id=" + ContentUris.parseId(url);
+                this.args = null;
+            }
+        }
+
+        SqlArguments(Uri url) {
+            if (url.getPathSegments().size() == 1) {
+                table = url.getPathSegments().get(0);
+                where = null;
+                args = null;
+            } else {
+                throw new IllegalArgumentException("Invalid URI: " + url);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
new file mode 100644
index 0000000..7d2b843
--- /dev/null
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -0,0 +1,236 @@
+/*
+ * 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.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * Settings related utilities.
+ */
+class LauncherSettings {
+    static interface BaseLauncherColumns extends BaseColumns {
+        /**
+         * Descriptive name of the gesture that can be displayed to the user.
+         * <P>Type: TEXT</P>
+         */
+        static final String TITLE = "title";
+
+        /**
+         * The Intent URL of the gesture, describing what it points to. This
+         * value is given to {@link android.content.Intent#parseUri(String, int)} to create
+         * an Intent that can be launched.
+         * <P>Type: TEXT</P>
+         */
+        static final String INTENT = "intent";
+
+        /**
+         * The type of the gesture
+         *
+         * <P>Type: INTEGER</P>
+         */
+        static final String ITEM_TYPE = "itemType";
+
+        /**
+         * The gesture is an application
+         */
+        static final int ITEM_TYPE_APPLICATION = 0;
+
+        /**
+         * The gesture is an application created shortcut
+         */
+        static final int ITEM_TYPE_SHORTCUT = 1;
+
+        /**
+         * The icon type.
+         * <P>Type: INTEGER</P>
+         */
+        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;
+
+        /**
+         * The icon is a bitmap.
+         */
+        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";
+
+        /**
+         * The icon resource id, if icon type is ICON_TYPE_RESOURCE.
+         * <P>Type: TEXT</P>
+         */
+        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";
+    }
+
+    /**
+     * 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");
+
+        /**
+         * The content:// style URL for this table. When this Uri is used, no notification is
+         * sent if the content changes.
+         */
+        static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" +
+                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
+                "?" + LauncherProvider.PARAMETER_NOTIFY + "=false");
+
+        /**
+         * 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);
+        }
+
+        /**
+         * The container holding the favorite
+         * <P>Type: INTEGER</P>
+         */
+        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;
+
+        /**
+         * The screen holding the favorite (if container is CONTAINER_DESKTOP)
+         * <P>Type: INTEGER</P>
+         */
+        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";
+
+        /**
+         * The Y coordinate of the cell holding the favorite
+         * (if container is CONTAINER_DESKTOP)
+         * <P>Type: INTEGER</P>
+         */
+        static final String CELLY = "cellY";
+
+        /**
+         * The X span of the cell holding the favorite
+         * <P>Type: INTEGER</P>
+         */
+        static final String SPANX = "spanX";
+
+        /**
+         * The Y span of the cell holding the favorite
+         * <P>Type: INTEGER</P>
+         */
+        static final String SPANY = "spanY";
+
+        /**
+         * The favorite is a user created folder
+         */
+        static final int ITEM_TYPE_FOLDER = 2;
+
+        /**
+        * The favorite is a live folder
+        *
+        * Note: live folders can no longer be added to Launcher, and any live folders which
+        * 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.
+        */
+        static final int ITEM_TYPE_LIVE_FOLDER = 3;
+
+        /**
+         * The favorite is a widget
+         */
+        static final int ITEM_TYPE_APPWIDGET = 4;
+
+        /**
+         * The favorite is a clock
+         */
+        static final int ITEM_TYPE_WIDGET_CLOCK = 1000;
+
+        /**
+         * The favorite is a search widget
+         */
+        static final int ITEM_TYPE_WIDGET_SEARCH = 1001;
+
+        /**
+         * The favorite is a photo frame
+         */
+        static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002;
+
+        /**
+         * The appWidgetId of the widget
+         *
+         * <P>Type: INTEGER</P>
+         */
+        static final String APPWIDGET_ID = "appWidgetId";
+        
+        /**
+         * Indicates whether this favorite is an application-created shortcut or not.
+         * If the value is 0, the favorite is not an application-created shortcut, if the
+         * value is 1, it is an application-created shortcut.
+         * <P>Type: INTEGER</P>
+         */
+        @Deprecated
+        static final String IS_SHORTCUT = "isShortcut";
+
+        /**
+         * The URI associated with the favorite. It is used, for instance, by
+         * live folders to find the content provider.
+         * <P>Type: TEXT</P>
+         */
+        static final String URI = "uri";
+
+        /**
+         * The display mode if the item is a live folder.
+         * <P>Type: INTEGER</P>
+         *
+         * @see android.provider.LiveFolders#DISPLAY_MODE_GRID
+         * @see android.provider.LiveFolders#DISPLAY_MODE_LIST
+         */
+        static final String DISPLAY_MODE = "displayMode";
+    }
+}
diff --git a/src/com/android/launcher3/LauncherViewPropertyAnimator.java b/src/com/android/launcher3/LauncherViewPropertyAnimator.java
new file mode 100644
index 0000000..8a9c35d
--- /dev/null
+++ b/src/com/android/launcher3/LauncherViewPropertyAnimator.java
@@ -0,0 +1,266 @@
+/*
+ * 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.TimeInterpolator;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+
+public class LauncherViewPropertyAnimator extends Animator implements AnimatorListener {
+    enum Properties {
+            TRANSLATION_X,
+            TRANSLATION_Y,
+            SCALE_X,
+            SCALE_Y,
+            ROTATION_Y,
+            ALPHA,
+            START_DELAY,
+            DURATION,
+            INTERPOLATOR
+    }
+    EnumSet<Properties> mPropertiesToSet = EnumSet.noneOf(Properties.class);
+    ViewPropertyAnimator mViewPropertyAnimator;
+    View mTarget;
+
+    float mTranslationX;
+    float mTranslationY;
+    float mScaleX;
+    float mScaleY;
+    float mRotationY;
+    float mAlpha;
+    long mStartDelay;
+    long mDuration;
+    TimeInterpolator mInterpolator;
+    ArrayList<Animator.AnimatorListener> mListeners;
+    boolean mRunning = false;
+    FirstFrameAnimatorHelper mFirstFrameHelper;
+
+    public LauncherViewPropertyAnimator(View target) {
+        mTarget = target;
+        mListeners = new ArrayList<Animator.AnimatorListener>();
+    }
+
+    @Override
+    public void addListener(Animator.AnimatorListener listener) {
+        mListeners.add(listener);
+    }
+
+    @Override
+    public void cancel() {
+        if (mViewPropertyAnimator != null) {
+            mViewPropertyAnimator.cancel();
+        }
+    }
+
+    @Override
+    public Animator clone() {
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public void end() {
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
+    public ArrayList<Animator.AnimatorListener> getListeners() {
+        return mListeners;
+    }
+
+    @Override
+    public long getStartDelay() {
+        return mStartDelay;
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        for (int i = 0; i < mListeners.size(); i++) {
+            Animator.AnimatorListener listener = mListeners.get(i);
+            listener.onAnimationCancel(this);
+        }
+        mRunning = false;
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animation) {
+        for (int i = 0; i < mListeners.size(); i++) {
+            Animator.AnimatorListener listener = mListeners.get(i);
+            listener.onAnimationEnd(this);
+        }
+        mRunning = false;
+    }
+
+    @Override
+    public void onAnimationRepeat(Animator animation) {
+        for (int i = 0; i < mListeners.size(); i++) {
+            Animator.AnimatorListener listener = mListeners.get(i);
+            listener.onAnimationRepeat(this);
+        }
+    }
+
+    @Override
+    public void onAnimationStart(Animator animation) {
+        // This is the first time we get a handle to the internal ValueAnimator
+        // used by the ViewPropertyAnimator.
+        mFirstFrameHelper.onAnimationStart(animation);
+
+        for (int i = 0; i < mListeners.size(); i++) {
+            Animator.AnimatorListener listener = mListeners.get(i);
+            listener.onAnimationStart(this);
+        }
+        mRunning = true;
+    }
+
+    @Override
+    public boolean isRunning() {
+        return mRunning;
+    }
+
+    @Override
+    public boolean isStarted() {
+        return mViewPropertyAnimator != null;
+    }
+
+    @Override
+    public void removeAllListeners() {
+        mListeners.clear();
+    }
+
+    @Override
+    public void removeListener(Animator.AnimatorListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public Animator setDuration(long duration) {
+        mPropertiesToSet.add(Properties.DURATION);
+        mDuration = duration;
+        return this;
+    }
+
+    @Override
+    public void setInterpolator(TimeInterpolator value) {
+        mPropertiesToSet.add(Properties.INTERPOLATOR);
+        mInterpolator = value;
+    }
+
+    @Override
+    public void setStartDelay(long startDelay) {
+        mPropertiesToSet.add(Properties.START_DELAY);
+        mStartDelay = startDelay;
+    }
+
+    @Override
+    public void setTarget(Object target) {
+        throw new RuntimeException("Not implemented");
+    }
+
+    @Override
+    public void setupEndValues() {
+
+    }
+
+    @Override
+    public void setupStartValues() {
+    }
+
+    @Override
+    public void start() {
+        mViewPropertyAnimator = mTarget.animate();
+
+        // FirstFrameAnimatorHelper hooks itself up to the updates on the animator,
+        // and then adjusts the play time to keep the first two frames jank-free
+        mFirstFrameHelper = new FirstFrameAnimatorHelper(mViewPropertyAnimator, mTarget);
+
+        if (mPropertiesToSet.contains(Properties.TRANSLATION_X)) {
+            mViewPropertyAnimator.translationX(mTranslationX);
+        }
+        if (mPropertiesToSet.contains(Properties.TRANSLATION_Y)) {
+            mViewPropertyAnimator.translationY(mTranslationY);
+        }
+        if (mPropertiesToSet.contains(Properties.SCALE_X)) {
+            mViewPropertyAnimator.scaleX(mScaleX);
+        }
+        if (mPropertiesToSet.contains(Properties.ROTATION_Y)) {
+            mViewPropertyAnimator.rotationY(mRotationY);
+        }
+        if (mPropertiesToSet.contains(Properties.SCALE_Y)) {
+            mViewPropertyAnimator.scaleY(mScaleY);
+        }
+        if (mPropertiesToSet.contains(Properties.ALPHA)) {
+            mViewPropertyAnimator.alpha(mAlpha);
+        }
+        if (mPropertiesToSet.contains(Properties.START_DELAY)) {
+            mViewPropertyAnimator.setStartDelay(mStartDelay);
+        }
+        if (mPropertiesToSet.contains(Properties.DURATION)) {
+            mViewPropertyAnimator.setDuration(mDuration);
+        }
+        if (mPropertiesToSet.contains(Properties.INTERPOLATOR)) {
+            mViewPropertyAnimator.setInterpolator(mInterpolator);
+        }
+        mViewPropertyAnimator.setListener(this);
+        mViewPropertyAnimator.start();
+        LauncherAnimUtils.cancelOnDestroyActivity(this);
+    }
+
+    public LauncherViewPropertyAnimator translationX(float value) {
+        mPropertiesToSet.add(Properties.TRANSLATION_X);
+        mTranslationX = value;
+        return this;
+    }
+
+    public LauncherViewPropertyAnimator translationY(float value) {
+        mPropertiesToSet.add(Properties.TRANSLATION_Y);
+        mTranslationY = value;
+        return this;
+    }
+
+    public LauncherViewPropertyAnimator scaleX(float value) {
+        mPropertiesToSet.add(Properties.SCALE_X);
+        mScaleX = value;
+        return this;
+    }
+
+    public LauncherViewPropertyAnimator scaleY(float value) {
+        mPropertiesToSet.add(Properties.SCALE_Y);
+        mScaleY = value;
+        return this;
+    }
+
+    public LauncherViewPropertyAnimator rotationY(float value) {
+        mPropertiesToSet.add(Properties.ROTATION_Y);
+        mRotationY = value;
+        return this;
+    }
+
+    public LauncherViewPropertyAnimator alpha(float value) {
+        mPropertiesToSet.add(Properties.ALPHA);
+        mAlpha = value;
+        return this;
+    }
+}
diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java
new file mode 100644
index 0000000..ded01a6
--- /dev/null
+++ b/src/com/android/launcher3/PackageChangedReceiver.java
@@ -0,0 +1,19 @@
+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;
+        }
+        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+        WidgetPreviewLoader.removeFromDb(app.getWidgetPreviewCacheDb(), packageName);
+    }
+}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
new file mode 100644
index 0000000..8716a33
--- /dev/null
+++ b/src/com/android/launcher3/PagedView.java
@@ -0,0 +1,1981 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.Scroller;
+
+import com.android.launcher3.R;
+
+import java.util.ArrayList;
+
+/**
+ * An abstraction of the original Workspace which supports browsing through a
+ * sequential list of "pages"
+ */
+public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener {
+    private static final String TAG = "PagedView";
+    private static final boolean DEBUG = false;
+    protected static final int INVALID_PAGE = -1;
+
+    // the min drag distance for a fling to register, to prevent random page shifts
+    private static final int MIN_LENGTH_FOR_FLING = 25;
+
+    protected static final int PAGE_SNAP_ANIMATION_DURATION = 550;
+    protected static final int MAX_PAGE_SNAP_DURATION = 750;
+    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.14f;
+
+    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;
+
+    // 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;
+
+    static final int AUTOMATIC_PAGE_SPACING = -1;
+
+    protected int mFlingThresholdVelocity;
+    protected int mMinFlingVelocity;
+    protected int mMinSnapVelocity;
+
+    protected float mDensity;
+    protected float mSmoothingTime;
+    protected float mTouchX;
+
+    protected boolean mFirstLayout = true;
+
+    protected int mCurrentPage;
+    protected int mNextPage = INVALID_PAGE;
+    protected int mMaxScrollX;
+    protected Scroller mScroller;
+    private VelocityTracker mVelocityTracker;
+
+    private float mDownMotionX;
+    protected float mLastMotionX;
+    protected float mLastMotionXRemainder;
+    protected float mLastMotionY;
+    protected float mTotalMotionX;
+    private int mLastScreenCenter = -1;
+    private int[] mChildOffsets;
+    private int[] mChildRelativeOffsets;
+    private int[] mChildOffsetsWithLayoutScale;
+
+    protected final static int TOUCH_STATE_REST = 0;
+    protected final static int TOUCH_STATE_SCROLLING = 1;
+    protected final static int TOUCH_STATE_PREV_PAGE = 2;
+    protected final static int TOUCH_STATE_NEXT_PAGE = 3;
+    protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f;
+
+    protected int mTouchState = TOUCH_STATE_REST;
+    protected boolean mForceScreenScrolled = false;
+
+    protected OnLongClickListener mLongClickListener;
+
+    protected boolean mAllowLongPress = true;
+
+    protected int mTouchSlop;
+    private int mPagingTouchSlop;
+    private int mMaximumVelocity;
+    private int mMinimumWidth;
+    protected int mPageSpacing;
+    protected int mPageLayoutPaddingTop;
+    protected int mPageLayoutPaddingBottom;
+    protected int mPageLayoutPaddingLeft;
+    protected int mPageLayoutPaddingRight;
+    protected int mPageLayoutWidthGap;
+    protected int mPageLayoutHeightGap;
+    protected int mCellCountX = 0;
+    protected int mCellCountY = 0;
+    protected boolean mCenterPagesVertically;
+    protected boolean mAllowOverScroll = true;
+    protected int mUnboundedScrollX;
+    protected int[] mTempVisiblePagesRange = new int[2];
+    protected boolean mForceDrawAllChildrenNextFrame;
+
+    // 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;
+
+    // parameter that adjusts the layout to be optimized for pages with that scale factor
+    protected float mLayoutScale = 1.0f;
+
+    protected static final int INVALID_POINTER = -1;
+
+    protected int mActivePointerId = INVALID_POINTER;
+
+    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 = true;
+
+    // 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 mIsPageMoving = false;
+
+    // All syncs and layout passes are deferred until data is ready.
+    protected boolean mIsDataReady = false;
+
+    // Scrolling indicator
+    private ValueAnimator mScrollIndicatorAnimator;
+    private View mScrollIndicator;
+    private int mScrollIndicatorPaddingLeft;
+    private int mScrollIndicatorPaddingRight;
+    private boolean mHasScrollIndicator = true;
+    private boolean mShouldShowScrollIndicator = false;
+    private boolean mShouldShowScrollIndicatorImmediately = false;
+    protected static final int sScrollIndicatorFadeInDuration = 150;
+    protected static final int sScrollIndicatorFadeOutDuration = 650;
+    protected static final int sScrollIndicatorFlashDuration = 650;
+    private boolean mScrollingPaused = false;
+
+    // If set, will defer loading associated pages until the scrolling settles
+    private boolean mDeferLoadAssociatedPagesUntilScrollCompletes;
+
+    public interface PageSwitchListener {
+        void onPageSwitch(View newPage, int newPageIndex);
+    }
+
+    public PagedView(Context context) {
+        this(context, null);
+    }
+
+    public PagedView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PagedView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.PagedView, defStyle, 0);
+        setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0));
+        mPageLayoutPaddingTop = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutPaddingTop, 0);
+        mPageLayoutPaddingBottom = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutPaddingBottom, 0);
+        mPageLayoutPaddingLeft = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutPaddingLeft, 0);
+        mPageLayoutPaddingRight = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutPaddingRight, 0);
+        mPageLayoutWidthGap = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutWidthGap, 0);
+        mPageLayoutHeightGap = a.getDimensionPixelSize(
+                R.styleable.PagedView_pageLayoutHeightGap, 0);
+        mScrollIndicatorPaddingLeft =
+            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0);
+        mScrollIndicatorPaddingRight =
+            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0);
+        a.recycle();
+
+        setHapticFeedbackEnabled(false);
+        init();
+    }
+
+    /**
+     * Initializes various states for this workspace.
+     */
+    protected void init() {
+        mDirtyPageContent = new ArrayList<Boolean>();
+        mDirtyPageContent.ensureCapacity(32);
+        mScroller = new Scroller(getContext(), new ScrollInterpolator());
+        mCurrentPage = 0;
+        mCenterPagesVertically = true;
+
+        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mDensity = getResources().getDisplayMetrics().density;
+
+        mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
+        mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity);
+        mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity);
+        setOnHierarchyChangeListener(this);
+    }
+
+    public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
+        mPageSwitchListener = pageSwitchListener;
+        if (mPageSwitchListener != null) {
+            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
+        }
+    }
+
+    /**
+     * 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() {
+        return mCurrentPage;
+    }
+    int getNextPage() {
+        return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
+    }
+
+    int getPageCount() {
+        return getChildCount();
+    }
+
+    View getPageAt(int index) {
+        return getChildAt(index);
+    }
+
+    protected int indexToPage(int index) {
+        return index;
+    }
+
+    /**
+     * Updates the scroll of the current page immediately to its final scroll position.  We use this
+     * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of
+     * the previous tab page.
+     */
+    protected void updateCurrentPageScroll() {
+        // If the current page is invalid, just reset the scroll position to zero
+        int newX = 0;
+        if (0 <= mCurrentPage && mCurrentPage < getPageCount()) {
+            int offset = getChildOffset(mCurrentPage);
+            int relOffset = getRelativeChildOffset(mCurrentPage);
+            newX = offset - relOffset;
+        }
+        scrollTo(newX, 0);
+        mScroller.setFinalX(newX);
+        mScroller.forceFinished(true);
+    }
+
+    /**
+     * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation
+     * ends, {@link #resumeScrolling()} should be called, along with
+     * {@link #updateCurrentPageScroll()} to correctly set the final state and re-enable scrolling.
+     */
+    void pauseScrolling() {
+        mScroller.forceFinished(true);
+        cancelScrollingIndicatorAnimations();
+        mScrollingPaused = true;
+    }
+
+    /**
+     * Enables scrolling again.
+     * @see #pauseScrolling()
+     */
+    void resumeScrolling() {
+        mScrollingPaused = false;
+    }
+    /**
+     * Sets the current page.
+     */
+    void setCurrentPage(int currentPage) {
+        if (!mScroller.isFinished()) {
+            mScroller.abortAnimation();
+        }
+        // don't introduce any checks like mCurrentPage == currentPage here-- if we change the
+        // the default
+        if (getChildCount() == 0) {
+            return;
+        }
+
+
+        mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
+        updateCurrentPageScroll();
+        updateScrollingIndicator();
+        notifyPageSwitchListener();
+        invalidate();
+    }
+
+    protected void notifyPageSwitchListener() {
+        if (mPageSwitchListener != null) {
+            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
+        }
+    }
+
+    protected void pageBeginMoving() {
+        if (!mIsPageMoving) {
+            mIsPageMoving = true;
+            onPageBeginMoving();
+        }
+    }
+
+    protected void pageEndMoving() {
+        if (mIsPageMoving) {
+            mIsPageMoving = false;
+            onPageEndMoving();
+        }
+    }
+
+    protected boolean isPageMoving() {
+        return mIsPageMoving;
+    }
+
+    // a method that subclasses can override to add behavior
+    protected void onPageBeginMoving() {
+    }
+
+    // a method that subclasses can override to add behavior
+    protected void onPageEndMoving() {
+    }
+
+    /**
+     * Registers the specified listener on each page contained in this workspace.
+     *
+     * @param l The listener used to respond to long clicks.
+     */
+    @Override
+    public void setOnLongClickListener(OnLongClickListener l) {
+        mLongClickListener = l;
+        final int count = getPageCount();
+        for (int i = 0; i < count; i++) {
+            getPageAt(i).setOnLongClickListener(l);
+        }
+    }
+
+    @Override
+    public void scrollBy(int x, int y) {
+        scrollTo(mUnboundedScrollX + x, getScrollY() + y);
+    }
+
+    @Override
+    public void scrollTo(int x, int y) {
+        final boolean isRtl = isLayoutRtl();
+        mUnboundedScrollX = x;
+
+        boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0);
+        boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX);
+        if (isXBeforeFirstPage) {
+            super.scrollTo(0, y);
+            if (mAllowOverScroll) {
+                if (isRtl) {
+                    overScroll(x - mMaxScrollX);
+                } else {
+                    overScroll(x);
+                }
+            }
+        } else if (isXAfterLastPage) {
+            super.scrollTo(mMaxScrollX, y);
+            if (mAllowOverScroll) {
+                if (isRtl) {
+                    overScroll(x);
+                } else {
+                    overScroll(x - mMaxScrollX);
+                }
+            }
+        } else {
+            mOverScrollX = x;
+            super.scrollTo(x, y);
+        }
+
+        mTouchX = x;
+        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
+    }
+
+    // we moved this functionality to a helper function so SmoothPagedView can reuse it
+    protected boolean computeScrollHelper() {
+        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()) {
+                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
+            }
+            invalidate();
+            return true;
+        } else if (mNextPage != INVALID_PAGE) {
+            mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
+            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) {
+                pageEndMoving();
+            }
+
+            // Notify the user when the page changes
+            AccessibilityManager accessibilityManager = (AccessibilityManager)
+                    getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+            if (accessibilityManager.isEnabled()) {
+                AccessibilityEvent ev =
+                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+                ev.getText().add(getCurrentPageDescription());
+                sendAccessibilityEventUnchecked(ev);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void computeScroll() {
+        computeScrollHelper();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (!mIsDataReady) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        if (widthMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
+        }
+
+        // Return early if we aren't given a proper dimension
+        if (widthSize <= 0 || heightSize <= 0) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+
+        /* Allow the height to be set as WRAP_CONTENT. This allows the particular case
+         * of the All apps view on XLarge displays to not take up more space then it needs. Width
+         * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect
+         * each page to have the same width.
+         */
+        int maxChildHeight = 0;
+
+        final int verticalPadding = getPaddingTop() + getPaddingBottom();
+        final int horizontalPadding = getPaddingLeft() + getPaddingRight();
+
+
+        // The children are given the same width and height as the workspace
+        // unless they were set to WRAP_CONTENT
+        if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize);
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            // disallowing padding in paged view (just pass 0)
+            final View child = getPageAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            int childWidthMode;
+            if (lp.width == LayoutParams.WRAP_CONTENT) {
+                childWidthMode = MeasureSpec.AT_MOST;
+            } else {
+                childWidthMode = MeasureSpec.EXACTLY;
+            }
+
+            int childHeightMode;
+            if (lp.height == LayoutParams.WRAP_CONTENT) {
+                childHeightMode = MeasureSpec.AT_MOST;
+            } else {
+                childHeightMode = MeasureSpec.EXACTLY;
+            }
+
+            final int childWidthMeasureSpec =
+                MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode);
+            final int childHeightMeasureSpec =
+                MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode);
+
+            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
+            if (DEBUG) Log.d(TAG, "\tmeasure-child" + i + ": " + child.getMeasuredWidth() + ", "
+                    + child.getMeasuredHeight());
+        }
+
+        if (heightMode == MeasureSpec.AT_MOST) {
+            heightSize = maxChildHeight + verticalPadding;
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+
+        // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions.
+        // We also wait until we set the measured dimensions before flushing the cache as well, to
+        // ensure that the cache is filled with good values.
+        invalidateCachedOffsets();
+
+        if (childCount > 0) {
+            if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth() + ", "
+                    + getChildWidth(0));
+
+            // Calculate the variable page spacing if necessary
+            if (mPageSpacing == AUTOMATIC_PAGE_SPACING) {
+                // The gap between pages in the PagedView should be equal to the gap from the page
+                // to the edge of the screen (so it is not visible in the current screen).  To
+                // account for unequal padding on each side of the paged view, we take the maximum
+                // of the left/right gap and use that as the gap between each page.
+                int offset = getRelativeChildOffset(0);
+                int spacing = Math.max(offset, widthSize - offset -
+                        getChildAt(0).getMeasuredWidth());
+                setPageSpacing(spacing);
+            }
+        }
+
+        updateScrollingIndicatorPosition();
+
+        if (childCount > 0) {
+            final int index = isLayoutRtl() ? 0 : childCount - 1;
+            mMaxScrollX = getChildOffset(index) - getRelativeChildOffset(index);
+        } else {
+            mMaxScrollX = 0;
+        }
+    }
+
+    protected void scrollToNewPageWithoutMovingPages(int newCurrentPage) {
+        int newX = getChildOffset(newCurrentPage) - getRelativeChildOffset(newCurrentPage);
+        int delta = newX - getScrollX();
+
+        final int pageCount = getChildCount();
+        for (int i = 0; i < pageCount; i++) {
+            View page = (View) getPageAt(i);
+            page.setX(page.getX() + delta);
+        }
+        setCurrentPage(newCurrentPage);
+    }
+
+    // A layout scale of 1.0f assumes that the pages, in their unshrunken state, have a
+    // scale of 1.0f. A layout scale of 0.8f assumes the pages have a scale of 0.8f, and
+    // tightens the layout accordingly
+    public void setLayoutScale(float childrenScale) {
+        mLayoutScale = childrenScale;
+        invalidateCachedOffsets();
+
+        // Now we need to do a re-layout, but preserving absolute X and Y coordinates
+        int childCount = getChildCount();
+        float childrenX[] = new float[childCount];
+        float childrenY[] = new float[childCount];
+        for (int i = 0; i < childCount; i++) {
+            final View child = getPageAt(i);
+            childrenX[i] = child.getX();
+            childrenY[i] = child.getY();
+        }
+        // Trigger a full re-layout (never just call onLayout directly!)
+        int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
+        int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
+        requestLayout();
+        measure(widthSpec, heightSpec);
+        layout(getLeft(), getTop(), getRight(), getBottom());
+        for (int i = 0; i < childCount; i++) {
+            final View child = getPageAt(i);
+            child.setX(childrenX[i]);
+            child.setY(childrenY[i]);
+        }
+
+        // Also, the page offset has changed  (since the pages are now smaller);
+        // update the page offset, but again preserving absolute X and Y coordinates
+        scrollToNewPageWithoutMovingPages(mCurrentPage);
+    }
+
+    public void setPageSpacing(int pageSpacing) {
+        mPageSpacing = pageSpacing;
+        invalidateCachedOffsets();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (!mIsDataReady) {
+            return;
+        }
+
+        if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
+        final int verticalPadding = getPaddingTop() + getPaddingBottom();
+        final int childCount = getChildCount();
+        final boolean isRtl = isLayoutRtl();
+
+        final int startIndex = isRtl ? childCount - 1 : 0;
+        final int endIndex = isRtl ? -1 : childCount;
+        final int delta = isRtl ? -1 : 1;
+        int childLeft = getRelativeChildOffset(startIndex);
+        for (int i = startIndex; i != endIndex; i += delta) {
+            final View child = getPageAt(i);
+            if (child.getVisibility() != View.GONE) {
+                final int childWidth = getScaledMeasuredWidth(child);
+                final int childHeight = child.getMeasuredHeight();
+                int childTop = getPaddingTop();
+                if (mCenterPagesVertically) {
+                    childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2;
+                }
+
+                if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop);
+                child.layout(childLeft, childTop,
+                        childLeft + child.getMeasuredWidth(), childTop + childHeight);
+                childLeft += childWidth + mPageSpacing;
+            }
+        }
+
+        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
+            setHorizontalScrollBarEnabled(false);
+            updateCurrentPageScroll();
+            setHorizontalScrollBarEnabled(true);
+            mFirstLayout = false;
+        }
+    }
+
+    protected void screenScrolled(int screenCenter) {
+        if (isScrollingIndicatorEnabled()) {
+            updateScrollingIndicator();
+        }
+        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();
+        }
+    }
+
+    @Override
+    public void onChildViewAdded(View parent, View child) {
+        // This ensures that when children are added, they get the correct transforms / alphas
+        // in accordance with any scroll effects.
+        mForceScreenScrolled = true;
+        invalidate();
+        invalidateCachedOffsets();
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+    }
+
+    protected void invalidateCachedOffsets() {
+        int count = getChildCount();
+        if (count == 0) {
+            mChildOffsets = null;
+            mChildRelativeOffsets = null;
+            mChildOffsetsWithLayoutScale = null;
+            return;
+        }
+
+        mChildOffsets = new int[count];
+        mChildRelativeOffsets = new int[count];
+        mChildOffsetsWithLayoutScale = new int[count];
+        for (int i = 0; i < count; i++) {
+            mChildOffsets[i] = -1;
+            mChildRelativeOffsets[i] = -1;
+            mChildOffsetsWithLayoutScale[i] = -1;
+        }
+    }
+
+    protected int getChildOffset(int index) {
+        final boolean isRtl = isLayoutRtl();
+        int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ?
+                mChildOffsets : mChildOffsetsWithLayoutScale;
+
+        if (childOffsets != null && childOffsets[index] != -1) {
+            return childOffsets[index];
+        } else {
+            if (getChildCount() == 0)
+                return 0;
+
+            final int startIndex = isRtl ? getChildCount() - 1 : 0;
+            final int endIndex = isRtl ? index : index;
+            final int delta = isRtl ? -1 : 1;
+            int offset = getRelativeChildOffset(startIndex);
+            for (int i = startIndex; i != endIndex; i += delta) {
+                offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing;
+            }
+            if (childOffsets != null) {
+                childOffsets[index] = offset;
+            }
+            return offset;
+        }
+    }
+
+    protected int getRelativeChildOffset(int index) {
+        if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) {
+            return mChildRelativeOffsets[index];
+        } else {
+            final int padding = getPaddingLeft() + getPaddingRight();
+            final int offset = getPaddingLeft() +
+                    (getMeasuredWidth() - padding - getChildWidth(index)) / 2;
+            if (mChildRelativeOffsets != null) {
+                mChildRelativeOffsets[index] = offset;
+            }
+            return offset;
+        }
+    }
+
+    protected int getScaledMeasuredWidth(View child) {
+        // This functions are called enough times that it actually makes a difference in the
+        // profiler -- so just inline the max() here
+        final int measuredWidth = child.getMeasuredWidth();
+        final int minWidth = mMinimumWidth;
+        final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth;
+        return (int) (maxWidth * mLayoutScale + 0.5f);
+    }
+
+    protected void getVisiblePages(int[] range) {
+        final boolean isRtl = isLayoutRtl();
+        final int pageCount = getChildCount();
+
+        if (pageCount > 0) {
+            final int screenWidth = getMeasuredWidth();
+            int leftScreen = isRtl ? pageCount - 1 : 0;
+            int rightScreen = 0;
+            int endIndex = isRtl ? 0 : pageCount - 1;
+            int delta = isRtl ? -1 : 1;
+            View currPage = getPageAt(leftScreen);
+            while (leftScreen != endIndex &&
+                    currPage.getX() + currPage.getWidth() -
+                    currPage.getPaddingRight() < getScrollX()) {
+                leftScreen += delta;
+                currPage = getPageAt(leftScreen);
+            }
+            rightScreen = leftScreen;
+            currPage = getPageAt(rightScreen + delta);
+            while (rightScreen != endIndex &&
+                    currPage.getX() - currPage.getPaddingLeft() < getScrollX() + screenWidth) {
+                rightScreen += delta;
+                currPage = getPageAt(rightScreen + delta);
+            }
+            range[0] = Math.min(leftScreen, rightScreen);
+            range[1] = Math.max(leftScreen, rightScreen);
+        } else {
+            range[0] = -1;
+            range[1] = -1;
+        }
+    }
+
+    protected boolean shouldDrawChild(View child) {
+        return child.getAlpha() > 0;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        int halfScreenSize = getMeasuredWidth() / 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;
+
+        if (screenCenter != mLastScreenCenter || mForceScreenScrolled) {
+            // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can
+            // set it for the next frame
+            mForceScreenScrolled = false;
+            screenScrolled(screenCenter);
+            mLastScreenCenter = screenCenter;
+        }
+
+        // Find out which screens are visible; as an optimization we only call draw on them
+        final int pageCount = getChildCount();
+        if (pageCount > 0) {
+            getVisiblePages(mTempVisiblePagesRange);
+            final int leftScreen = mTempVisiblePagesRange[0];
+            final int rightScreen = mTempVisiblePagesRange[1];
+            if (leftScreen != -1 && rightScreen != -1) {
+                final long drawingTime = getDrawingTime();
+                // Clip to the bounds
+                canvas.save();
+                canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(),
+                        getScrollY() + getBottom() - getTop());
+
+                for (int i = getChildCount() - 1; i >= 0; i--) {
+                    final View v = getPageAt(i);
+                    if (mForceDrawAllChildrenNextFrame ||
+                               (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) {
+                        drawChild(canvas, v, drawingTime);
+                    }
+                }
+                mForceDrawAllChildrenNextFrame = false;
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
+        int page = indexToPage(indexOfChild(child));
+        if (page != mCurrentPage || !mScroller.isFinished()) {
+            snapToPage(page);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+        int focusablePage;
+        if (mNextPage != INVALID_PAGE) {
+            focusablePage = mNextPage;
+        } else {
+            focusablePage = mCurrentPage;
+        }
+        View v = getPageAt(focusablePage);
+        if (v != null) {
+            return v.requestFocus(direction, previouslyFocusedRect);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        // XXX-RTL: This will be fixed in a future CL
+        if (direction == View.FOCUS_LEFT) {
+            if (getCurrentPage() > 0) {
+                snapToPage(getCurrentPage() - 1);
+                return true;
+            }
+        } else if (direction == View.FOCUS_RIGHT) {
+            if (getCurrentPage() < getPageCount() - 1) {
+                snapToPage(getCurrentPage() + 1);
+                return true;
+            }
+        }
+        return super.dispatchUnhandledMove(focused, direction);
+    }
+
+    @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        // XXX-RTL: This will be fixed in a future CL
+        if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
+            getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode);
+        }
+        if (direction == View.FOCUS_LEFT) {
+            if (mCurrentPage > 0) {
+                getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode);
+            }
+        } else if (direction == View.FOCUS_RIGHT){
+            if (mCurrentPage < getPageCount() - 1) {
+                getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode);
+            }
+        }
+    }
+
+    /**
+     * If one of our descendant views decides that it could be focused now, only
+     * pass that along if it's on the current page.
+     *
+     * This happens when live folders requery, and if they're off page, they
+     * end up calling requestFocus, which pulls it on page.
+     */
+    @Override
+    public void focusableViewAvailable(View focused) {
+        View current = getPageAt(mCurrentPage);
+        View v = focused;
+        while (true) {
+            if (v == current) {
+                super.focusableViewAvailable(focused);
+                return;
+            }
+            if (v == this) {
+                return;
+            }
+            ViewParent parent = v.getParent();
+            if (parent instanceof View) {
+                v = (View)v.getParent();
+            } else {
+                return;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (disallowIntercept) {
+            // We need to make sure to cancel our long press if
+            // a scrollable widget takes over touch events
+            final View currentPage = getPageAt(mCurrentPage);
+            currentPage.cancelLongPress();
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+    /**
+     * 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()) {
+            return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing));
+        } else {
+            return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing);
+        }
+    }
+
+    /**
+     * 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()) {
+            return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing);
+        } else {
+            return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing));
+        }
+
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onTouchEvent will be called and we do the actual
+         * scrolling there.
+         */
+        acquireVelocityTrackerAndAddMovement(ev);
+
+        // Skip touch handling if there are no pages to swipe
+        if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev);
+
+        /*
+         * Shortcut the most recurring case: the user is in the dragging
+         * state and he is moving his finger.  We want to intercept this
+         * motion.
+         */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) &&
+                (mTouchState == TOUCH_STATE_SCROLLING)) {
+            return true;
+        }
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+                if (mActivePointerId != INVALID_POINTER) {
+                    determineScrollingStart(ev);
+                    break;
+                }
+                // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
+                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
+                // i.e. fall through to the next case (don't break)
+                // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
+                // while it's small- this was causing a crash before we checked for INVALID_POINTER)
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                // Remember location of down touch
+                mDownMotionX = x;
+                mLastMotionX = x;
+                mLastMotionY = y;
+                mLastMotionXRemainder = 0;
+                mTotalMotionX = 0;
+                mActivePointerId = ev.getPointerId(0);
+                mAllowLongPress = true;
+
+                /*
+                 * If being flinged and user touches the screen, initiate drag;
+                 * otherwise don't.  mScroller.isFinished should be false when
+                 * being flinged.
+                 */
+                final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
+                final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
+                if (finishedScrolling) {
+                    mTouchState = TOUCH_STATE_REST;
+                    mScroller.abortAnimation();
+                } else {
+                    mTouchState = TOUCH_STATE_SCROLLING;
+                }
+
+                // check if this can be the beginning of a tap on the side of the pages
+                // to scroll the current page
+                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;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mTouchState = TOUCH_STATE_REST;
+                mAllowLongPress = false;
+                mActivePointerId = INVALID_POINTER;
+                releaseVelocityTracker();
+                break;
+
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                releaseVelocityTracker();
+                break;
+        }
+
+        /*
+         * The only time we want to intercept motion events is if we are in the
+         * drag mode.
+         */
+        return mTouchState != TOUCH_STATE_REST;
+    }
+
+    protected void determineScrollingStart(MotionEvent ev) {
+        determineScrollingStart(ev, 1.0f);
+    }
+
+    /*
+     * 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, float touchSlopScale) {
+        /*
+         * Locally do absolute value. mLastMotionX is set to the y value
+         * of the down event.
+         */
+        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+        if (pointerIndex == -1) {
+            return;
+        }
+        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 = 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 = getScrollX();
+                mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
+                pageBeginMoving();
+            }
+            // Either way, cancel any pending longpress
+            cancelCurrentPageLongPress();
+        }
+    }
+
+    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();
+            }
+        }
+    }
+
+    protected float getScrollProgress(int screenCenter, View v, int page) {
+        final int halfScreenSize = getMeasuredWidth() / 2;
+
+        int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing;
+        int delta = screenCenter - (getChildOffset(page) -
+                getRelativeChildOffset(page) + halfScreenSize);
+
+        float scrollProgress = delta / (totalDistance * 1.0f);
+        scrollProgress = Math.min(scrollProgress, 1.0f);
+        scrollProgress = Math.max(scrollProgress, -1.0f);
+        return scrollProgress;
+    }
+
+    // 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 void acceleratedOverScroll(float amount) {
+        int screenSize = getMeasuredWidth();
+
+        // 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;
+
+        // Clamp this factor, f, to -1 < f < 1
+        if (Math.abs(f) >= 1) {
+            f /= Math.abs(f);
+        }
+
+        int overScrollAmount = (int) Math.round(f * screenSize);
+        if (amount < 0) {
+            mOverScrollX = overScrollAmount;
+            super.scrollTo(0, getScrollY());
+        } else {
+            mOverScrollX = mMaxScrollX + overScrollAmount;
+            super.scrollTo(mMaxScrollX, getScrollY());
+        }
+        invalidate();
+    }
+
+    protected void dampedOverScroll(float amount) {
+        int screenSize = getMeasuredWidth();
+
+        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(0, getScrollY());
+        } else {
+            mOverScrollX = mMaxScrollX + overScrollAmount;
+            super.scrollTo(mMaxScrollX, getScrollY());
+        }
+        invalidate();
+    }
+
+    protected void overScroll(float amount) {
+        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;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        // Skip touch handling if there are no pages to swipe
+        if (getChildCount() <= 0) return super.onTouchEvent(ev);
+
+        acquireVelocityTrackerAndAddMovement(ev);
+
+        final int action = ev.getAction();
+
+        switch (action & MotionEvent.ACTION_MASK) {
+        case MotionEvent.ACTION_DOWN:
+            /*
+             * If being flinged and user touches, stop the fling. isFinished
+             * will be false if being flinged.
+             */
+            if (!mScroller.isFinished()) {
+                mScroller.abortAnimation();
+            }
+
+            // Remember where the motion event started
+            mDownMotionX = mLastMotionX = ev.getX();
+            mLastMotionXRemainder = 0;
+            mTotalMotionX = 0;
+            mActivePointerId = ev.getPointerId(0);
+            if (mTouchState == TOUCH_STATE_SCROLLING) {
+                pageBeginMoving();
+            }
+            break;
+
+        case MotionEvent.ACTION_MOVE:
+            if (mTouchState == TOUCH_STATE_SCROLLING) {
+                // Scroll to follow the motion event
+                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                final float x = ev.getX(pointerIndex);
+                final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
+
+                mTotalMotionX += Math.abs(deltaX);
+
+                // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
+                // keep the remainder because we are actually testing if we've moved from the last
+                // scrolled position (which is discrete).
+                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();
+                    }
+                    mLastMotionX = x;
+                    mLastMotionXRemainder = deltaX - (int) deltaX;
+                } else {
+                    awakenScrollBars();
+                }
+            } else {
+                determineScrollingStart(ev);
+            }
+            break;
+
+        case MotionEvent.ACTION_UP:
+            if (mTouchState == TOUCH_STATE_SCROLLING) {
+                final int activePointerId = mActivePointerId;
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                final float x = ev.getX(pointerIndex);
+                final VelocityTracker velocityTracker = mVelocityTracker;
+                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
+                final int deltaX = (int) (x - mDownMotionX);
+                final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage));
+                boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
+                        SIGNIFICANT_MOVE_THRESHOLD;
+
+                mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
+
+                boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
+                        Math.abs(velocityX) > mFlingThresholdVelocity;
+
+                // In the case that the page is moved far to one direction and then is flung
+                // in the opposite direction, we use a threshold to determine whether we should
+                // just return to the starting page, or if we should skip one further.
+                boolean returnToOriginalPage = false;
+                if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
+                        Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
+                    returnToOriginalPage = true;
+                }
+
+                int finalPage;
+                // 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;
+                if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
+                        (isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
+                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
+                    snapToPageWithVelocity(finalPage, velocityX);
+                } else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
+                        (isFling && isVelocityXLeft)) &&
+                        mCurrentPage < getChildCount() - 1) {
+                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
+                    snapToPageWithVelocity(finalPage, velocityX);
+                } else {
+                    snapToDestination();
+                }
+            } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
+                // at this point we have not moved beyond the touch slop
+                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
+                // we can just page
+                int nextPage = Math.max(0, mCurrentPage - 1);
+                if (nextPage != mCurrentPage) {
+                    snapToPage(nextPage);
+                } else {
+                    snapToDestination();
+                }
+            } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
+                // at this point we have not moved beyond the touch slop
+                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
+                // we can just page
+                int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
+                if (nextPage != mCurrentPage) {
+                    snapToPage(nextPage);
+                } else {
+                    snapToDestination();
+                }
+            } else {
+                onUnhandledTap(ev);
+            }
+            mTouchState = TOUCH_STATE_REST;
+            mActivePointerId = INVALID_POINTER;
+            releaseVelocityTracker();
+            break;
+
+        case MotionEvent.ACTION_CANCEL:
+            if (mTouchState == TOUCH_STATE_SCROLLING) {
+                snapToDestination();
+            }
+            mTouchState = TOUCH_STATE_REST;
+            mActivePointerId = INVALID_POINTER;
+            releaseVelocityTracker();
+            break;
+
+        case MotionEvent.ACTION_POINTER_UP:
+            onSecondaryPointerUp(ev);
+            break;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    // Handle mouse (or ext. device) by shifting the page depending on the scroll
+                    final float vscroll;
+                    final float hscroll;
+                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+                        vscroll = 0;
+                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                    } else {
+                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                    }
+                    if (hscroll != 0 || vscroll != 0) {
+                        boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0)
+                                                         : (hscroll > 0 || vscroll > 0);
+                        if (isForwardScroll) {
+                            scrollRight();
+                        } else {
+                            scrollLeft();
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+    }
+
+    private void releaseVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
+            mLastMotionY = ev.getY(newPointerIndex);
+            mLastMotionXRemainder = 0;
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    protected void onUnhandledTap(MotionEvent ev) {}
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        super.requestChildFocus(child, focused);
+        int page = indexToPage(indexOfChild(child));
+        if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) {
+            snapToPage(page);
+        }
+    }
+
+    protected int getChildIndexForRelativeOffset(int relativeOffset) {
+        final boolean isRtl = isLayoutRtl();
+        final int childCount = getChildCount();
+        int left;
+        int right;
+        final int startIndex = isRtl ? childCount - 1 : 0;
+        final int endIndex = isRtl ? -1 : childCount;
+        final int delta = isRtl ? -1 : 1;
+        for (int i = startIndex; i != endIndex; i += delta) {
+            left = getRelativeChildOffset(i);
+            right = (left + getScaledMeasuredWidth(getPageAt(i)));
+            if (left <= relativeOffset && relativeOffset <= right) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    protected int getChildWidth(int index) {
+        // This functions are called enough times that it actually makes a difference in the
+        // profiler -- so just inline the max() here
+        final int measuredWidth = getPageAt(index).getMeasuredWidth();
+        final int minWidth = mMinimumWidth;
+        return (minWidth > measuredWidth) ? minWidth : measuredWidth;
+    }
+
+    int getPageNearestToCenterOfScreen() {
+        int minDistanceFromScreenCenter = Integer.MAX_VALUE;
+        int minDistanceFromScreenCenterIndex = -1;
+        int screenCenter = getScrollX() + (getMeasuredWidth() / 2);
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; ++i) {
+            View layout = (View) getPageAt(i);
+            int childWidth = getScaledMeasuredWidth(layout);
+            int halfChildWidth = (childWidth / 2);
+            int childCenter = getChildOffset(i) + halfChildWidth;
+            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+            if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
+                minDistanceFromScreenCenter = distanceFromScreenCenter;
+                minDistanceFromScreenCenterIndex = i;
+            }
+        }
+        return minDistanceFromScreenCenterIndex;
+    }
+
+    protected void snapToDestination() {
+        snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION);
+    }
+
+    private static class ScrollInterpolator implements Interpolator {
+        public ScrollInterpolator() {
+        }
+
+        public float getInterpolation(float t) {
+            t -= 1.0f;
+            return t*t*t*t*t + 1;
+        }
+    }
+
+    // We want the duration of the page snap animation to be influenced by the distance that
+    // 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) {
+        f -= 0.5f; // center the values about 0.
+        f *= 0.3f * Math.PI / 2.0f;
+        return (float) Math.sin(f);
+    }
+
+    protected void snapToPageWithVelocity(int whichPage, int velocity) {
+        whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1));
+        int halfScreenSize = getMeasuredWidth() / 2;
+
+        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
+        if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): "
+                + getMeasuredWidth() + ", " + getChildWidth(whichPage));
+        final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
+        int delta = newX - mUnboundedScrollX;
+        int duration = 0;
+
+        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, PAGE_SNAP_ANIMATION_DURATION);
+            return;
+        }
+
+        // Here we compute a "distance" that will be used in the computation of the overall
+        // snap duration. This is a function of the actual distance that needs to be traveled;
+        // we keep this value close to half screen size in order to reduce the variance in snap
+        // duration as a function of the distance the page needs to travel.
+        float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize));
+        float distance = halfScreenSize + halfScreenSize *
+                distanceInfluenceForSnapDuration(distanceRatio);
+
+        velocity = Math.abs(velocity);
+        velocity = Math.max(mMinSnapVelocity, velocity);
+
+        // we want the page's snap velocity to approximately match the velocity at which the
+        // user flings, so we scale the duration by a value near to the derivative of the scroll
+        // interpolator at zero, ie. 5. We use 4 to make it a little slower.
+        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+        duration = Math.min(duration, MAX_PAGE_SNAP_DURATION);
+
+        snapToPage(whichPage, delta, duration);
+    }
+
+    protected void snapToPage(int whichPage) {
+        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
+    }
+
+    protected void snapToPage(int whichPage, int duration) {
+        whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
+
+        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
+        if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMeasuredWidth() + ", "
+                + getChildWidth(whichPage));
+        int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
+        final int sX = mUnboundedScrollX;
+        final int delta = newX - sX;
+        snapToPage(whichPage, delta, duration);
+    }
+
+    protected void snapToPage(int whichPage, int delta, int duration) {
+        mNextPage = whichPage;
+
+        View focusedChild = getFocusedChild();
+        if (focusedChild != null && whichPage != mCurrentPage &&
+                focusedChild == getPageAt(mCurrentPage)) {
+            focusedChild.clearFocus();
+        }
+
+        pageBeginMoving();
+        awakenScrollBars(duration);
+        if (duration == 0) {
+            duration = Math.abs(delta);
+        }
+
+        if (!mScroller.isFinished()) mScroller.abortAnimation();
+        mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);
+
+        // Load associated pages immediately if someone else is handling the scroll, otherwise defer
+        // loading associated pages until the scroll settles
+        if (mDeferScrollUpdate) {
+            loadAssociatedPages(mNextPage);
+        } else {
+            mDeferLoadAssociatedPagesUntilScrollCompletes = true;
+        }
+        notifyPageSwitchListener();
+        invalidate();
+    }
+
+    public void scrollLeft() {
+        if (mScroller.isFinished()) {
+            if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
+        } else {
+            if (mNextPage > 0) snapToPage(mNextPage - 1);
+        }
+    }
+
+    public void scrollRight() {
+        if (mScroller.isFinished()) {
+            if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
+        } else {
+            if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
+        }
+    }
+
+    public int getPageForView(View v) {
+        int result = -1;
+        if (v != null) {
+            ViewParent vp = v.getParent();
+            int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                if (vp == getPageAt(i)) {
+                    return i;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @return True is long presses are still allowed for the current touch
+     */
+    public boolean allowLongPress() {
+        return mAllowLongPress;
+    }
+
+    /**
+     * 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;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            currentPage = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(currentPage);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    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
+            mScroller.forceFinished(true);
+            mNextPage = INVALID_PAGE;
+
+            // 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();
+        }
+    }
+
+    protected View getScrollingIndicator() {
+        // We use mHasScrollIndicator to prevent future lookups if there is no sibling indicator
+        // found
+        if (mHasScrollIndicator && mScrollIndicator == null) {
+            ViewGroup parent = (ViewGroup) getParent();
+            if (parent != null) {
+                mScrollIndicator = (View) (parent.findViewById(R.id.paged_view_indicator));
+                mHasScrollIndicator = mScrollIndicator != null;
+                if (mHasScrollIndicator) {
+                    mScrollIndicator.setVisibility(View.VISIBLE);
+                }
+            }
+        }
+        return mScrollIndicator;
+    }
+
+    protected boolean isScrollingIndicatorEnabled() {
+        return true;
+    }
+
+    Runnable hideScrollingIndicatorRunnable = new Runnable() {
+        @Override
+        public void run() {
+            hideScrollingIndicator(false);
+        }
+    };
+    protected void flashScrollingIndicator(boolean animated) {
+        removeCallbacks(hideScrollingIndicatorRunnable);
+        showScrollingIndicator(!animated);
+        postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration);
+    }
+
+    protected void showScrollingIndicator(boolean immediately) {
+        mShouldShowScrollIndicator = true;
+        mShouldShowScrollIndicatorImmediately = true;
+        if (getChildCount() <= 1) return;
+        if (!isScrollingIndicatorEnabled()) return;
+
+        mShouldShowScrollIndicator = false;
+        getScrollingIndicator();
+        if (mScrollIndicator != null) {
+            // Fade the indicator in
+            updateScrollingIndicatorPosition();
+            mScrollIndicator.setVisibility(View.VISIBLE);
+            cancelScrollingIndicatorAnimations();
+            if (immediately || mScrollingPaused) {
+                mScrollIndicator.setAlpha(1f);
+            } else {
+                mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 1f);
+                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration);
+                mScrollIndicatorAnimator.start();
+            }
+        }
+    }
+
+    protected void cancelScrollingIndicatorAnimations() {
+        if (mScrollIndicatorAnimator != null) {
+            mScrollIndicatorAnimator.cancel();
+        }
+    }
+
+    protected void hideScrollingIndicator(boolean immediately) {
+        if (getChildCount() <= 1) return;
+        if (!isScrollingIndicatorEnabled()) return;
+
+        getScrollingIndicator();
+        if (mScrollIndicator != null) {
+            // Fade the indicator out
+            updateScrollingIndicatorPosition();
+            cancelScrollingIndicatorAnimations();
+            if (immediately || mScrollingPaused) {
+                mScrollIndicator.setVisibility(View.INVISIBLE);
+                mScrollIndicator.setAlpha(0f);
+            } else {
+                mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 0f);
+                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration);
+                mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() {
+                    private boolean cancelled = false;
+                    @Override
+                    public void onAnimationCancel(android.animation.Animator animation) {
+                        cancelled = true;
+                    }
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        if (!cancelled) {
+                            mScrollIndicator.setVisibility(View.INVISIBLE);
+                        }
+                    }
+                });
+                mScrollIndicatorAnimator.start();
+            }
+        }
+    }
+
+    /**
+     * To be overridden by subclasses to determine whether the scroll indicator should stretch to
+     * fill its space on the track or not.
+     */
+    protected boolean hasElasticScrollIndicator() {
+        return true;
+    }
+
+    private void updateScrollingIndicator() {
+        if (getChildCount() <= 1) return;
+        if (!isScrollingIndicatorEnabled()) return;
+
+        getScrollingIndicator();
+        if (mScrollIndicator != null) {
+            updateScrollingIndicatorPosition();
+        }
+        if (mShouldShowScrollIndicator) {
+            showScrollingIndicator(mShouldShowScrollIndicatorImmediately);
+        }
+    }
+
+    private void updateScrollingIndicatorPosition() {
+        final boolean isRtl = isLayoutRtl();
+        if (!isScrollingIndicatorEnabled()) return;
+        if (mScrollIndicator == null) return;
+        int numPages = getChildCount();
+        int pageWidth = getMeasuredWidth();
+        int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight;
+        int indicatorWidth = mScrollIndicator.getMeasuredWidth() -
+                mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight();
+
+        float scrollPos = isRtl ? mMaxScrollX - getScrollX() : getScrollX();
+        float offset = Math.max(0f, Math.min(1f, (float) scrollPos / mMaxScrollX));
+        if (isRtl) {
+            offset = 1f - offset;
+        }
+        int indicatorSpace = trackWidth / numPages;
+        int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft;
+        if (hasElasticScrollIndicator()) {
+            if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) {
+                mScrollIndicator.getLayoutParams().width = indicatorSpace;
+                mScrollIndicator.requestLayout();
+            }
+        } else {
+            int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2;
+            indicatorPos += indicatorCenterOffset;
+        }
+        mScrollIndicator.setTranslationX(indicatorPos);
+    }
+
+    public void showScrollIndicatorTrack() {
+    }
+
+    public void hideScrollIndicatorTrack() {
+    }
+
+    /* Accessibility */
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setScrollable(getPageCount() > 1);
+        if (getCurrentPage() < getPageCount() - 1) {
+            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+        }
+        if (getCurrentPage() > 0) {
+            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setScrollable(true);
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            event.setFromIndex(mCurrentPage);
+            event.setToIndex(mCurrentPage);
+            event.setItemCount(getChildCount());
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (super.performAccessibilityAction(action, arguments)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+                if (getCurrentPage() < getPageCount() - 1) {
+                    scrollRight();
+                    return true;
+                }
+            } break;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+                if (getCurrentPage() > 0) {
+                    scrollLeft();
+                    return true;
+                }
+            } break;
+        }
+        return false;
+    }
+
+    protected String getCurrentPageDescription() {
+        return String.format(getContext().getString(R.string.default_scroll_format),
+                 getNextPage() + 1, getChildCount());
+    }
+
+    @Override
+    public boolean onHoverEvent(android.view.MotionEvent event) {
+        return true;
+    }
+}
diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java
new file mode 100644
index 0000000..177425a
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewCellLayout.java
@@ -0,0 +1,505 @@
+/*
+ * 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.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+
+/**
+ * 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;
+    private int mMaxGap;
+    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
+        Resources resources = context.getResources();
+        mOriginalCellWidth = mCellWidth =
+            resources.getDimensionPixelSize(R.dimen.apps_customize_cell_width);
+        mOriginalCellHeight = mCellHeight =
+            resources.getDimensionPixelSize(R.dimen.apps_customize_cell_height);
+        mCellCountX = LauncherModel.getCellCountX();
+        mCellCountY = LauncherModel.getCellCountY();
+        mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1;
+        mMaxGap = resources.getDimensionPixelSize(R.dimen.apps_customize_max_gap);
+
+        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 = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0);
+            mHeightGap = Math.min(mMaxGap,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(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 (LauncherApplication.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 + ")";
+        }
+    }
+}
+
+interface Page {
+    public int getPageChildCount();
+    public View getChildOnPageAt(int i);
+    public void removeAllViewsOnPage();
+    public void removeViewOnPageAt(int i);
+    public int indexOfChildOnPage(View v);
+}
diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java
new file mode 100644
index 0000000..c9e108d
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewCellLayoutChildren.java
@@ -0,0 +1,160 @@
+/*
+ * 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(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
new file mode 100644
index 0000000..b286861
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewGridLayout.java
@@ -0,0 +1,133 @@
+/*
+ * 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);
+        }
+    }
+
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // PagedView currently has issues with different-sized pages since it calculates the
+        // offset of each page to scroll to before it updates the actual size of each page
+        // (which can change depending on the content if the contents aren't a fixed size).
+        // We work around this by having a minimum size on each widget page).
+        int widthSpecSize = Math.min(getSuggestedMinimumWidth(),
+                MeasureSpec.getSize(widthMeasureSpec));
+        int widthSpecMode = MeasureSpec.EXACTLY;
+        super.onMeasure(MeasureSpec.makeMeasureSpec(widthSpecSize, widthSpecMode),
+                heightMeasureSpec);
+    }
+
+    @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/PagedViewIcon.java b/src/com/android/launcher3/PagedViewIcon.java
new file mode 100644
index 0000000..73f62d6
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewIcon.java
@@ -0,0 +1,92 @@
+/*
+ * 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.Bitmap;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * An icon on a PagedView, specifically for items in the launcher's paged view (with compound
+ * drawables on the top).
+ */
+public class PagedViewIcon extends TextView {
+    /** A simple callback interface to allow a PagedViewIcon to notify when it has been pressed */
+    public static interface PressedCallback {
+        void iconPressed(PagedViewIcon icon);
+    }
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "PagedViewIcon";
+    private static final float PRESS_ALPHA = 0.4f;
+
+    private PagedViewIcon.PressedCallback mPressedCallback;
+    private boolean mLockDrawableState = false;
+
+    private Bitmap mIcon;
+
+    public PagedViewIcon(Context context) {
+        this(context, null);
+    }
+
+    public PagedViewIcon(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void applyFromApplicationInfo(ApplicationInfo info, boolean scaleUp,
+            PagedViewIcon.PressedCallback cb) {
+        mIcon = info.iconBitmap;
+        mPressedCallback = cb;
+        setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null);
+        setText(info.title);
+        setTag(info);
+    }
+
+    public void lockDrawableState() {
+        mLockDrawableState = true;
+    }
+
+    public void resetDrawableState() {
+        mLockDrawableState = false;
+        post(new Runnable() {
+            @Override
+            public void run() {
+                refreshDrawableState();
+            }
+        });
+    }
+
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        // We keep in the pressed state until resetDrawableState() is called to reset the press
+        // feedback
+        if (isPressed()) {
+            setAlpha(PRESS_ALPHA);
+            if (mPressedCallback != null) {
+                mPressedCallback.iconPressed(this);
+            }
+        } else if (!mLockDrawableState) {
+            setAlpha(1f);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/PagedViewIconCache.java b/src/com/android/launcher3/PagedViewIconCache.java
new file mode 100644
index 0000000..0d03b5a
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewIconCache.java
@@ -0,0 +1,133 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.pm.ComponentInfo;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+
+/**
+ * Simple cache mechanism for PagedView outlines.
+ */
+public class PagedViewIconCache {
+    public static class Key {
+        public enum Type {
+            ApplicationInfoKey,
+            AppWidgetProviderInfoKey,
+            ResolveInfoKey
+        }
+        private final ComponentName mComponentName;
+        private final Type mType;
+
+        public Key(ApplicationInfo info) {
+            mComponentName = info.componentName;
+            mType = Type.ApplicationInfoKey;
+        }
+        public Key(ResolveInfo info) {
+            final ComponentInfo ci = info.activityInfo != null ? info.activityInfo :
+                info.serviceInfo;
+            mComponentName = new ComponentName(ci.packageName, ci.name);
+            mType = Type.ResolveInfoKey;
+        }
+        public Key(AppWidgetProviderInfo info) {
+            mComponentName = info.provider;
+            mType = Type.AppWidgetProviderInfoKey;
+        }
+
+        private ComponentName getComponentName() {
+            return mComponentName;
+        }
+        public boolean isKeyType(Type t) {
+            return (mType == t);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof Key) {
+                Key k = (Key) o;
+                return mComponentName.equals(k.mComponentName);
+            }
+            return super.equals(o);
+        }
+        @Override
+        public int hashCode() {
+            return getComponentName().hashCode();
+        }
+    }
+
+    private final HashMap<Key, Bitmap> mIconOutlineCache = new HashMap<Key, Bitmap>();
+
+    public void clear() {
+        for (Key key : mIconOutlineCache.keySet()) {
+            mIconOutlineCache.get(key).recycle();
+        }
+        mIconOutlineCache.clear();
+    }
+    private void retainAll(HashSet<Key> keysToKeep, Key.Type t) {
+        HashSet<Key> keysToRemove = new HashSet<Key>(mIconOutlineCache.keySet());
+        keysToRemove.removeAll(keysToKeep);
+        for (Key key : keysToRemove) {
+            if (key.isKeyType(t)) {
+                mIconOutlineCache.get(key).recycle();
+                mIconOutlineCache.remove(key);
+            }
+        }
+    }
+    /** Removes all the keys to applications that aren't in the passed in collection */
+    public void retainAllApps(ArrayList<ApplicationInfo> keys) {
+        HashSet<Key> keysSet = new HashSet<Key>();
+        for (ApplicationInfo info : keys) {
+            keysSet.add(new Key(info));
+        }
+        retainAll(keysSet, Key.Type.ApplicationInfoKey);
+    }
+    /** Removes all the keys to shortcuts that aren't in the passed in collection */
+    public void retainAllShortcuts(List<ResolveInfo> keys) {
+        HashSet<Key> keysSet = new HashSet<Key>();
+        for (ResolveInfo info : keys) {
+            keysSet.add(new Key(info));
+        }
+        retainAll(keysSet, Key.Type.ResolveInfoKey);
+    }
+    /** Removes all the keys to widgets that aren't in the passed in collection */
+    public void retainAllAppWidgets(List<AppWidgetProviderInfo> keys) {
+        HashSet<Key> keysSet = new HashSet<Key>();
+        for (AppWidgetProviderInfo info : keys) {
+            keysSet.add(new Key(info));
+        }
+        retainAll(keysSet, Key.Type.AppWidgetProviderInfoKey);
+    }
+    public void addOutline(Key key, Bitmap b) {
+        mIconOutlineCache.put(key, b);
+    }
+    public void removeOutline(Key key) {
+        if (mIconOutlineCache.containsKey(key)) {
+            mIconOutlineCache.get(key).recycle();
+            mIconOutlineCache.remove(key);
+        }
+    }
+    public Bitmap getOutline(Key key) {
+        return mIconOutlineCache.get(key);
+    }
+}
diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/PagedViewWidget.java
new file mode 100644
index 0000000..bd40c5c
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewWidget.java
@@ -0,0 +1,246 @@
+/*
+ * 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.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+/**
+ * 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();
+    }
+
+    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) {
+        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(info.label);
+        final TextView dims = (TextView) findViewById(R.id.widget_dims);
+        if (dims != null) {
+            int hSpan = Math.min(cellSpan[0], LauncherModel.getCellCountX());
+            int vSpan = Math.min(cellSpan[1], LauncherModel.getCellCountY());
+            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
new file mode 100644
index 0000000..71f5eea
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewWidgetImageView.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+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
new file mode 100644
index 0000000..8f10ecf
--- /dev/null
+++ b/src/com/android/launcher3/PagedViewWithDraggableItems.java
@@ -0,0 +1,178 @@
+/*
+ * 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);
+    }
+
+    @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();
+    }
+
+    /** Show the scrolling indicators when we move the page */
+    protected void onPageBeginMoving() {
+        showScrollingIndicator(false);
+    }
+    protected void onPageEndMoving() {
+        hideScrollingIndicator(false);
+    }
+}
diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java
new file mode 100644
index 0000000..967cc92
--- /dev/null
+++ b/src/com/android/launcher3/PendingAddItemInfo.java
@@ -0,0 +1,107 @@
+/*
+ * 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.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
+ */
+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();
+    }
+}
diff --git a/src/com/android/launcher3/PreloadReceiver.java b/src/com/android/launcher3/PreloadReceiver.java
new file mode 100644
index 0000000..ee34348
--- /dev/null
+++ b/src/com/android/launcher3/PreloadReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class PreloadReceiver extends BroadcastReceiver {
+    private static final String TAG = "Launcher.PreloadReceiver";
+    private static final boolean LOGD = false;
+
+    public static final String EXTRA_WORKSPACE_NAME =
+            "com.android.launcher3.action.EXTRA_WORKSPACE_NAME";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+        final LauncherProvider provider = app.getLauncherProvider();
+        if (provider != null) {
+            String name = intent.getStringExtra(EXTRA_WORKSPACE_NAME);
+            final int workspaceResId = !TextUtils.isEmpty(name)
+                    ? context.getResources().getIdentifier(name, "xml", "com.android.launcher3") : 0;
+            if (LOGD) {
+                Log.d(TAG, "workspace name: " + name + " id: " + workspaceResId);
+            }
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    provider.loadDefaultFavoritesIfNecessary(workspaceResId);
+                }
+            }).start();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java
new file mode 100644
index 0000000..32d094b
--- /dev/null
+++ b/src/com/android/launcher3/SearchDropTargetBar.java
@@ -0,0 +1,239 @@
+/*
+ * 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.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+
+/*
+ * Ths bar will manage the transition between the QSB search bar and the delete drop
+ * targets so that each of the individual IconDropTargets don't have to.
+ */
+public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener {
+
+    private static final int sTransitionInDuration = 200;
+    private static final int sTransitionOutDuration = 175;
+
+    private ObjectAnimator mDropTargetBarAnim;
+    private ObjectAnimator mQSBSearchBarAnim;
+    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 Drawable mPreviousBackground;
+    private boolean mEnableDropDownDropTargets;
+
+    public SearchDropTargetBar(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SearchDropTargetBar(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setup(Launcher launcher, DragController dragController) {
+        dragController.addDragListener(this);
+        dragController.addDragListener(mInfoDropTarget);
+        dragController.addDragListener(mDeleteDropTarget);
+        dragController.addDropTarget(mInfoDropTarget);
+        dragController.addDropTarget(mDeleteDropTarget);
+        dragController.setFlingToDeleteDropTarget(mDeleteDropTarget);
+        mInfoDropTarget.setLauncher(launcher);
+        mDeleteDropTarget.setLauncher(launcher);
+    }
+
+    private void prepareStartAnimation(View v) {
+        // Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd
+        // callback below)
+        v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+    }
+
+    private void setupAnimation(ObjectAnimator anim, final View v) {
+        anim.setInterpolator(sAccelerateInterpolator);
+        anim.setDuration(sTransitionInDuration);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setLayerType(View.LAYER_TYPE_NONE, null);
+            }
+        });
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        // Get the individual components
+        mQSBSearchBar = findViewById(R.id.qsb_search_bar);
+        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);
+        mBarHeight = getResources().getDimensionPixelSize(R.dimen.qsb_bar_height);
+
+        mInfoDropTarget.setSearchDropTargetBar(this);
+        mDeleteDropTarget.setSearchDropTargetBar(this);
+
+        mEnableDropDownDropTargets =
+            getResources().getBoolean(R.bool.config_useDropTargetDownTransition);
+
+        // Create the various fade animations
+        if (mEnableDropDownDropTargets) {
+            mDropTargetBar.setTranslationY(-mBarHeight);
+            mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "translationY",
+                    -mBarHeight, 0f);
+            mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0,
+                    -mBarHeight);
+        } else {
+            mDropTargetBar.setAlpha(0f);
+            mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f);
+            mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f);
+        }
+        setupAnimation(mDropTargetBarAnim, mDropTargetBar);
+        setupAnimation(mQSBSearchBarAnim, mQSBSearchBar);
+    }
+
+    public void finishAnimations() {
+        prepareStartAnimation(mDropTargetBar);
+        mDropTargetBarAnim.reverse();
+        prepareStartAnimation(mQSBSearchBar);
+        mQSBSearchBarAnim.reverse();
+    }
+
+    /*
+     * Shows and hides the search bar.
+     */
+    public void showSearchBar(boolean animated) {
+        if (!mIsSearchBarHidden) return;
+        if (animated) {
+            prepareStartAnimation(mQSBSearchBar);
+            mQSBSearchBarAnim.reverse();
+        } else {
+            mQSBSearchBarAnim.cancel();
+            if (mEnableDropDownDropTargets) {
+                mQSBSearchBar.setTranslationY(0);
+            } else {
+                mQSBSearchBar.setAlpha(1f);
+            }
+        }
+        mIsSearchBarHidden = false;
+    }
+    public void hideSearchBar(boolean animated) {
+        if (mIsSearchBarHidden) return;
+        if (animated) {
+            prepareStartAnimation(mQSBSearchBar);
+            mQSBSearchBarAnim.start();
+        } else {
+            mQSBSearchBarAnim.cancel();
+            if (mEnableDropDownDropTargets) {
+                mQSBSearchBar.setTranslationY(-mBarHeight);
+            } else {
+                mQSBSearchBar.setAlpha(0f);
+            }
+        }
+        mIsSearchBarHidden = true;
+    }
+
+    /*
+     * Gets various transition durations.
+     */
+    public int getTransitionInDuration() {
+        return sTransitionInDuration;
+    }
+    public int getTransitionOutDuration() {
+        return sTransitionOutDuration;
+    }
+
+    /*
+     * DragController.DragListener implementation
+     */
+    @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();
+        }
+    }
+
+    public void deferOnDragEnd() {
+        mDeferOnDragEnd = true;
+    }
+
+    @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();
+            }
+        } else {
+            mDeferOnDragEnd = false;
+        }
+    }
+
+    public void onSearchPackagesChanged(boolean searchVisible, boolean voiceVisible) {
+        if (mQSBSearchBar != null) {
+            Drawable bg = mQSBSearchBar.getBackground();
+            if (bg != null && (!searchVisible && !voiceVisible)) {
+                // Save the background and disable it
+                mPreviousBackground = bg;
+                mQSBSearchBar.setBackgroundResource(0);
+            } else if (mPreviousBackground != null && (searchVisible || voiceVisible)) {
+                // Restore the background
+                mQSBSearchBar.setBackground(mPreviousBackground);
+            }
+        }
+    }
+
+    public Rect getSearchBarBounds() {
+        if (mQSBSearchBar != null) {
+            final int[] pos = new int[2];
+            mQSBSearchBar.getLocationOnScreen(pos);
+
+            final Rect rect = new Rect();
+            rect.left = pos[0];
+            rect.top = pos[1];
+            rect.right = pos[0] + mQSBSearchBar.getWidth();
+            rect.bottom = pos[1] + mQSBSearchBar.getHeight();
+            return rect;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
new file mode 100644
index 0000000..18b9399
--- /dev/null
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -0,0 +1,204 @@
+/*
+ * 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.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class ShortcutAndWidgetContainer extends ViewGroup {
+    static final String TAG = "CellLayoutChildren";
+
+    // 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[] mTmpCellXY = new int[2];
+
+    private final WallpaperManager mWallpaperManager;
+
+    private int mCellWidth;
+    private int mCellHeight;
+
+    private int mWidthGap;
+    private int mHeightGap;
+
+    private int mCountX;
+
+    private boolean mInvertIfRtl = false;
+
+    public ShortcutAndWidgetContainer(Context context) {
+        super(context);
+        mWallpaperManager = WallpaperManager.getInstance(context);
+    }
+
+    public void setCellDimensions(int cellWidth, int cellHeight, int widthGap, int heightGap,
+            int countX) {
+        mCellWidth = cellWidth;
+        mCellHeight = cellHeight;
+        mWidthGap = widthGap;
+        mHeightGap = heightGap;
+        mCountX = countX;
+    }
+
+    public View getChildAt(int x, int y) {
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+
+            if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) &&
+                    (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        @SuppressWarnings("all") // suppress dead code warning
+        final boolean debug = false;
+        if (debug) {
+            // Debug drawing for hit space
+            Paint p = new Paint();
+            p.setColor(0x6600FF00);
+            for (int i = getChildCount() - 1; i >= 0; i--) {
+                final View child = getChildAt(i);
+                final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+
+                canvas.drawRect(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height, p);
+            }
+        }
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            measureChild(child);
+        }
+        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
+        setMeasuredDimension(widthSpecSize, heightSpecSize);
+    }
+
+    public void setupLp(CellLayout.LayoutParams lp) {
+        lp.setup(mCellWidth, mCellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(),
+                mCountX);
+    }
+
+    // Set whether or not to invert the layout horizontally if the layout is in RTL mode.
+    public void setInvertIfRtl(boolean invert) {
+        mInvertIfRtl = invert;
+    }
+
+    public void measureChild(View child) {
+        final int cellWidth = mCellWidth;
+        final int cellHeight = mCellHeight;
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+
+        lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(), mCountX);
+        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
+        int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height,
+                MeasureSpec.EXACTLY);
+        child.measure(childWidthMeasureSpec, childheightMeasureSpec);
+    }
+
+    private boolean invertLayoutHorizontally() {
+        return mInvertIfRtl && isLayoutRtl();
+    }
+
+    public boolean isLayoutRtl() {
+        return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+
+                int childLeft = lp.x;
+                int childTop = lp.y;
+                child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
+
+                if (lp.dropped) {
+                    lp.dropped = false;
+
+                    final int[] cellXY = mTmpCellXY;
+                    getLocationOnScreen(cellXY);
+                    mWallpaperManager.sendWallpaperCommand(getWindowToken(),
+                            WallpaperManager.COMMAND_DROP,
+                            cellXY[0] + childLeft + lp.width / 2,
+                            cellXY[1] + childTop + lp.height / 2, 0, null);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    @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
+    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();
+        }
+    }
+
+    @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() && enabled) {
+                view.buildDrawingCache(true);
+            }
+        }
+    }
+
+    @Override
+    protected void setChildrenDrawnWithCacheEnabled(boolean enabled) {
+        super.setChildrenDrawnWithCacheEnabled(enabled);
+    }
+}
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
new file mode 100644
index 0000000..5249fec
--- /dev/null
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -0,0 +1,162 @@
+/*
+ * 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 java.util.ArrayList;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+/**
+ * Represents a launchable icon on the workspaces and in folders.
+ */
+class ShortcutInfo extends ItemInfo {
+
+    /**
+     * The intent used to start the application.
+     */
+    Intent intent;
+
+    /**
+     * Indicates whether the icon comes from an application's resource (if false)
+     * or from a custom Bitmap (if true.)
+     */
+    boolean customIcon;
+
+    /**
+     * Indicates whether we're using the default fallback icon instead of something from the
+     * app.
+     */
+    boolean usingFallbackIcon;
+
+    /**
+     * If isShortcut=true and customIcon=false, this contains a reference to the
+     * shortcut icon as an application's resource.
+     */
+    Intent.ShortcutIconResource iconResource;
+
+    /**
+     * The application icon.
+     */
+    private Bitmap mIcon;
+
+    ShortcutInfo() {
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
+    }
+    
+    public ShortcutInfo(ShortcutInfo info) {
+        super(info);
+        title = info.title.toString();
+        intent = new Intent(info.intent);
+        if (info.iconResource != null) {
+            iconResource = new Intent.ShortcutIconResource();
+            iconResource.packageName = info.iconResource.packageName;
+            iconResource.resourceName = info.iconResource.resourceName;
+        }
+        mIcon = info.mIcon; // TODO: should make a copy here.  maybe we don't need this ctor at all
+        customIcon = info.customIcon;
+    }
+
+    /** TODO: Remove this.  It's only called by ApplicationInfo.makeShortcut. */
+    public ShortcutInfo(ApplicationInfo info) {
+        super(info);
+        title = info.title.toString();
+        intent = new Intent(info.intent);
+        customIcon = false;
+    }
+
+    public void setIcon(Bitmap b) {
+        mIcon = b;
+    }
+
+    public Bitmap getIcon(IconCache iconCache) {
+        if (mIcon == null) {
+            updateIcon(iconCache);
+        }
+        return mIcon;
+    }
+
+    public void updateIcon(IconCache iconCache) {
+        mIcon = iconCache.getIcon(intent);
+        usingFallbackIcon = iconCache.isDefaultIcon(mIcon);
+    }
+
+    /**
+     * Creates the application intent based on a component name and various launch flags.
+     * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}.
+     *
+     * @param className the class name of the component representing the intent
+     * @param launchFlags the launch flags
+     */
+    final void setActivity(ComponentName className, int launchFlags) {
+        intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setComponent(className);
+        intent.setFlags(launchFlags);
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION;
+    }
+
+    @Override
+    void onAddToDatabase(ContentValues values) {
+        super.onAddToDatabase(values);
+
+        String titleStr = title != null ? title.toString() : null;
+        values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr);
+
+        String uri = intent != null ? intent.toUri(0) : null;
+        values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri);
+
+        if (customIcon) {
+            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
+                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_BITMAP);
+            writeBitmap(values, mIcon);
+        } else {
+            if (!usingFallbackIcon) {
+                writeBitmap(values, mIcon);
+            }
+            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
+                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE);
+            if (iconResource != null) {
+                values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE,
+                        iconResource.packageName);
+                values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE,
+                        iconResource.resourceName);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ShortcutInfo(title=" + title.toString() + "intent=" + intent + "id=" + this.id
+                + " type=" + this.itemType + " container=" + this.container + " screen=" + screen
+                + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY
+                + " dropPos=" + dropPos + ")";
+    }
+
+    public static void dumpShortcutInfoList(String tag, String label,
+            ArrayList<ShortcutInfo> list) {
+        Log.d(tag, label + " size=" + list.size());
+        for (ShortcutInfo info: list) {
+            Log.d(tag, "   title=\"" + info.title + " icon=" + info.mIcon
+                    + " customIcon=" + info.customIcon);
+        }
+    }
+}
+
diff --git a/src/com/android/launcher3/SmoothPagedView.java b/src/com/android/launcher3/SmoothPagedView.java
new file mode 100644
index 0000000..1beffac
--- /dev/null
+++ b/src/com/android/launcher3/SmoothPagedView.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+import android.widget.Scroller;
+
+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) {
+            // _o(t) = t * t * ((tension + 1) * t + tension)
+            // o(t) = _o(t - 1) + 1
+            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();
+            mScroller = new Scroller(getContext(), 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 = getChildOffset(whichPage) - getRelativeChildOffset(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/SpringLoadedDragController.java b/src/com/android/launcher3/SpringLoadedDragController.java
new file mode 100644
index 0000000..45edaef
--- /dev/null
+++ b/src/com/android/launcher3/SpringLoadedDragController.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+public class SpringLoadedDragController implements OnAlarmListener {
+    // how long the user must hover over a mini-screen before it unshrinks
+    final long ENTER_SPRING_LOAD_HOVER_TIME = 500;
+    final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950;
+    final long EXIT_SPRING_LOAD_HOVER_TIME = 200;
+
+    Alarm mAlarm;
+
+    // the screen the user is currently hovering over, if any
+    private CellLayout mScreen;
+    private Launcher mLauncher;
+
+    public SpringLoadedDragController(Launcher launcher) {
+        mLauncher = launcher;
+        mAlarm = new Alarm();
+        mAlarm.setOnAlarmListener(this);
+    }
+
+    public void cancel() {
+        mAlarm.cancelAlarm();
+    }
+
+    // Set a new alarm to expire for the screen that we are hovering over now
+    public void setAlarm(CellLayout cl) {
+        mAlarm.cancelAlarm();
+        mAlarm.setAlarm((cl == null) ? ENTER_SPRING_LOAD_CANCEL_HOVER_TIME :
+            ENTER_SPRING_LOAD_HOVER_TIME);
+        mScreen = cl;
+    }
+
+    // this is called when our timer runs out
+    public void onAlarm(Alarm alarm) {
+        if (mScreen != null) {
+            // Snap to the screen that we are hovering over now
+            Workspace w = mLauncher.getWorkspace();
+            int page = w.indexOfChild(mScreen);
+            if (page != w.getCurrentPage()) {
+                w.snapToPage(page);
+            }
+        } else {
+            mLauncher.getDragController().cancelDrag();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java
new file mode 100644
index 0000000..6bc289a
--- /dev/null
+++ b/src/com/android/launcher3/UninstallShortcutReceiver.java
@@ -0,0 +1,166 @@
+/*
+ * 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.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+public class UninstallShortcutReceiver extends BroadcastReceiver {
+    private static final String ACTION_UNINSTALL_SHORTCUT =
+            "com.android.launcher3.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) {
+        String spKey = LauncherApplication.getSharedPreferencesKey();
+        SharedPreferences sharedPrefs = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+
+        final Intent data = pendingInfo.data;
+
+        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
+        synchronized (app) {
+            removeShortcut(context, data, sharedPrefs);
+        }
+    }
+
+    private static void removeShortcut(Context context, Intent data,
+            final SharedPreferences sharedPrefs) {
+        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();
+            }
+
+            // Remove any items due to be animated
+            boolean appRemoved;
+            Set<String> newApps = new HashSet<String>();
+            newApps = sharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps);
+            synchronized (newApps) {
+                do {
+                    appRemoved = newApps.remove(intent.toUri(0).toString());
+                } while (appRemoved);
+            }
+            if (appRemoved) {
+                final Set<String> savedNewApps = newApps;
+                new Thread("setNewAppsThread-remove") {
+                    public void run() {
+                        synchronized (savedNewApps) {
+                            SharedPreferences.Editor editor = sharedPrefs.edit();
+                            editor.putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY,
+                                    savedNewApps);
+                            if (savedNewApps.isEmpty()) {
+                                // Reset the page index if there are no more items
+                                editor.putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1);
+                            }
+                            editor.commit();
+                        }
+                    }
+                }.start();
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/UserInitializeReceiver.java
new file mode 100644
index 0000000..5cd5181
--- /dev/null
+++ b/src/com/android/launcher3/UserInitializeReceiver.java
@@ -0,0 +1,70 @@
+/*
+ * 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 java.io.IOException;
+import java.util.ArrayList;
+
+import com.android.launcher3.R;
+
+import android.app.WallpaperManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+
+/**
+ * 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) {
+        final Resources resources = context.getResources();
+        // Context.getPackageName() may return the "original" package name,
+        // com.android.launcher3; Resources needs the real package name,
+        // com.android.launcher3. So we ask Resources for what it thinks the
+        // package name should be.
+        final String packageName = resources.getResourcePackageName(R.array.wallpapers);
+        ArrayList<Integer> list = new ArrayList<Integer>();
+        addWallpapers(resources, packageName, R.array.wallpapers, list);
+        addWallpapers(resources, packageName, R.array.extra_wallpapers, list);
+        WallpaperManager wpm = (WallpaperManager) context.getSystemService(
+                Context.WALLPAPER_SERVICE);
+        for (int i=1; i<list.size(); i++) {
+            int resid = list.get(i);
+            if (!wpm.hasResourceWallpaper(resid)) {
+                try {
+                    wpm.setResource(resid);
+                } catch (IOException e) {
+                }
+                return;
+            }
+        }
+    }
+
+    private void addWallpapers(Resources resources, String packageName, int resid,
+            ArrayList<Integer> outList) {
+        final String[] extras = resources.getStringArray(resid);
+        for (String extra : extras) {
+            int res = resources.getIdentifier(extra, "drawable", packageName);
+            if (res != 0) {
+                outList.add(res);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
new file mode 100644
index 0000000..0cc29fa
--- /dev/null
+++ b/src/com/android/launcher3/Utilities.java
@@ -0,0 +1,275 @@
+/*
+ * 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 java.util.Random;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.util.DisplayMetrics;
+
+import com.android.launcher3.R;
+
+/**
+ * Various utilities shared amongst the Launcher's classes.
+ */
+final class Utilities {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Launcher.Utilities";
+
+    private static int sIconWidth = -1;
+    private static int sIconHeight = -1;
+    private static int sIconTextureWidth = -1;
+    private static int sIconTextureHeight = -1;
+
+    private static final Paint sBlurPaint = new Paint();
+    private static final Paint sGlowColorPressedPaint = new Paint();
+    private static final Paint sGlowColorFocusedPaint = new Paint();
+    private static final Paint sDisabledPaint = new Paint();
+    private static final Rect sOldBounds = new Rect();
+    private static final Canvas sCanvas = new Canvas();
+
+    static {
+        sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
+                Paint.FILTER_BITMAP_FLAG));
+    }
+    static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff };
+    static int sColorIndex = 0;
+
+    /**
+     * Returns a bitmap suitable for the all apps view. Used to convert pre-ICS
+     * icon bitmaps that are stored in the database (which were 74x74 pixels at hdpi size)
+     * to the proper size (48dp)
+     */
+    static Bitmap createIconBitmap(Bitmap icon, Context context) {
+        int textureWidth = sIconTextureWidth;
+        int textureHeight = sIconTextureHeight;
+        int sourceWidth = icon.getWidth();
+        int sourceHeight = icon.getHeight();
+        if (sourceWidth > textureWidth && sourceHeight > textureHeight) {
+            // Icon is bigger than it should be; clip it (solves the GB->ICS migration case)
+            return Bitmap.createBitmap(icon,
+                    (sourceWidth - textureWidth) / 2,
+                    (sourceHeight - textureHeight) / 2,
+                    textureWidth, textureHeight);
+        } else if (sourceWidth == textureWidth && sourceHeight == textureHeight) {
+            // Icon is the right size, no need to change it
+            return icon;
+        } else {
+            // Icon is too small, render to a larger bitmap
+            final Resources resources = context.getResources();
+            return createIconBitmap(new BitmapDrawable(resources, icon), context);
+        }
+    }
+
+    /**
+     * Returns a bitmap suitable for the all apps view.
+     */
+    static Bitmap createIconBitmap(Drawable icon, Context context) {
+        synchronized (sCanvas) { // we share the statics :-(
+            if (sIconWidth == -1) {
+                initStatics(context);
+            }
+
+            int width = sIconWidth;
+            int height = sIconHeight;
+
+            if (icon instanceof PaintDrawable) {
+                PaintDrawable painter = (PaintDrawable) icon;
+                painter.setIntrinsicWidth(width);
+                painter.setIntrinsicHeight(height);
+            } else if (icon instanceof BitmapDrawable) {
+                // Ensure the bitmap has a density.
+                BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
+                Bitmap bitmap = bitmapDrawable.getBitmap();
+                if (bitmap.getDensity() == Bitmap.DENSITY_NONE) {
+                    bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics());
+                }
+            }
+            int sourceWidth = icon.getIntrinsicWidth();
+            int sourceHeight = icon.getIntrinsicHeight();
+            if (sourceWidth > 0 && sourceHeight > 0) {
+                // There are intrinsic sizes.
+                if (width < sourceWidth || height < sourceHeight) {
+                    // It's too big, scale it down.
+                    final float ratio = (float) sourceWidth / sourceHeight;
+                    if (sourceWidth > sourceHeight) {
+                        height = (int) (width / ratio);
+                    } else if (sourceHeight > sourceWidth) {
+                        width = (int) (height * ratio);
+                    }
+                } else if (sourceWidth < width && sourceHeight < height) {
+                    // Don't scale up the icon
+                    width = sourceWidth;
+                    height = sourceHeight;
+                }
+            }
+
+            // no intrinsic size --> use default size
+            int textureWidth = sIconTextureWidth;
+            int textureHeight = sIconTextureHeight;
+
+            final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight,
+                    Bitmap.Config.ARGB_8888);
+            final Canvas canvas = sCanvas;
+            canvas.setBitmap(bitmap);
+
+            final int left = (textureWidth-width) / 2;
+            final int top = (textureHeight-height) / 2;
+
+            @SuppressWarnings("all") // suppress dead code warning
+            final boolean debug = false;
+            if (debug) {
+                // draw a big box for the icon for debugging
+                canvas.drawColor(sColors[sColorIndex]);
+                if (++sColorIndex >= sColors.length) sColorIndex = 0;
+                Paint debugPaint = new Paint();
+                debugPaint.setColor(0xffcccc00);
+                canvas.drawRect(left, top, left+width, top+height, debugPaint);
+            }
+
+            sOldBounds.set(icon.getBounds());
+            icon.setBounds(left, top, left+width, top+height);
+            icon.draw(canvas);
+            icon.setBounds(sOldBounds);
+            canvas.setBitmap(null);
+
+            return bitmap;
+        }
+    }
+
+    static void drawSelectedAllAppsBitmap(Canvas dest, int destWidth, int destHeight,
+            boolean pressed, Bitmap src) {
+        synchronized (sCanvas) { // we share the statics :-(
+            if (sIconWidth == -1) {
+                // We can't have gotten to here without src being initialized, which
+                // comes from this file already.  So just assert.
+                //initStatics(context);
+                throw new RuntimeException("Assertion failed: Utilities not initialized");
+            }
+
+            dest.drawColor(0, PorterDuff.Mode.CLEAR);
+
+            int[] xy = new int[2];
+            Bitmap mask = src.extractAlpha(sBlurPaint, xy);
+
+            float px = (destWidth - src.getWidth()) / 2;
+            float py = (destHeight - src.getHeight()) / 2;
+            dest.drawBitmap(mask, px + xy[0], py + xy[1],
+                    pressed ? sGlowColorPressedPaint : sGlowColorFocusedPaint);
+
+            mask.recycle();
+        }
+    }
+
+    /**
+     * Returns a Bitmap representing the thumbnail of the specified Bitmap.
+     * The size of the thumbnail is defined by the dimension
+     * android.R.dimen.launcher_application_icon_size.
+     *
+     * @param bitmap The bitmap to get a thumbnail of.
+     * @param context The application's context.
+     *
+     * @return A thumbnail for the specified bitmap or the bitmap itself if the
+     *         thumbnail could not be created.
+     */
+    static Bitmap resampleIconBitmap(Bitmap bitmap, Context context) {
+        synchronized (sCanvas) { // we share the statics :-(
+            if (sIconWidth == -1) {
+                initStatics(context);
+            }
+
+            if (bitmap.getWidth() == sIconWidth && bitmap.getHeight() == sIconHeight) {
+                return bitmap;
+            } else {
+                final Resources resources = context.getResources();
+                return createIconBitmap(new BitmapDrawable(resources, bitmap), context);
+            }
+        }
+    }
+
+    static Bitmap drawDisabledBitmap(Bitmap bitmap, Context context) {
+        synchronized (sCanvas) { // we share the statics :-(
+            if (sIconWidth == -1) {
+                initStatics(context);
+            }
+            final Bitmap disabled = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+                    Bitmap.Config.ARGB_8888);
+            final Canvas canvas = sCanvas;
+            canvas.setBitmap(disabled);
+            
+            canvas.drawBitmap(bitmap, 0.0f, 0.0f, sDisabledPaint);
+
+            canvas.setBitmap(null);
+
+            return disabled;
+        }
+    }
+
+    private static void initStatics(Context context) {
+        final Resources resources = context.getResources();
+        final DisplayMetrics metrics = resources.getDisplayMetrics();
+        final float density = metrics.density;
+
+        sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size);
+        sIconTextureWidth = sIconTextureHeight = sIconWidth;
+
+        sBlurPaint.setMaskFilter(new BlurMaskFilter(5 * density, BlurMaskFilter.Blur.NORMAL));
+        sGlowColorPressedPaint.setColor(0xffffc300);
+        sGlowColorFocusedPaint.setColor(0xffff8e00);
+
+        ColorMatrix cm = new ColorMatrix();
+        cm.setSaturation(0.2f);
+        sDisabledPaint.setColorFilter(new ColorMatrixColorFilter(cm));
+        sDisabledPaint.setAlpha(0x88);
+    }
+
+    /** Only works for positive numbers. */
+    static int roundToPow2(int n) {
+        int orig = n;
+        n >>= 1;
+        int mask = 0x8000000;
+        while (mask != 0 && (n & mask) == 0) {
+            mask >>= 1;
+        }
+        while (mask != 0) {
+            n |= mask;
+            mask >>= 1;
+        }
+        n += 1;
+        if (n != orig) {
+            n <<= 1;
+        }
+        return n;
+    }
+
+    static int generateRandomId() {
+        return new Random(System.currentTimeMillis()).nextInt(1 << 24);
+    }
+}
diff --git a/src/com/android/launcher3/WallpaperChooser.java b/src/com/android/launcher3/WallpaperChooser.java
new file mode 100644
index 0000000..fe81ccb
--- /dev/null
+++ b/src/com/android/launcher3/WallpaperChooser.java
@@ -0,0 +1,47 @@
+/*
+ * 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 com.android.launcher3.R;
+
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.os.Bundle;
+
+public class WallpaperChooser extends Activity {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Launcher.WallpaperChooser";
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.wallpaper_chooser_base);
+
+        Fragment fragmentView =
+                getFragmentManager().findFragmentById(R.id.wallpaper_chooser_fragment);
+        // TODO: The following code is currently not exercised. Leaving it here in case it
+        // needs to be revived again.
+        if (fragmentView == null) {
+            /* When the screen is XLarge, the fragment is not included in the layout, so show it
+             * as a dialog
+             */
+            DialogFragment fragment = WallpaperChooserDialogFragment.newInstance();
+            fragment.show(getFragmentManager(), "dialog");
+        }
+    }
+}
diff --git a/src/com/android/launcher3/WallpaperChooserDialogFragment.java b/src/com/android/launcher3/WallpaperChooserDialogFragment.java
new file mode 100644
index 0000000..99de815
--- /dev/null
+++ b/src/com/android/launcher3/WallpaperChooserDialogFragment.java
@@ -0,0 +1,360 @@
+/*
+ * 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.app.Activity;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Gallery;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.SpinnerAdapter;
+
+import com.android.launcher3.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class WallpaperChooserDialogFragment extends DialogFragment implements
+        AdapterView.OnItemSelectedListener, AdapterView.OnItemClickListener {
+
+    private static final String TAG = "Launcher.WallpaperChooserDialogFragment";
+    private static final String EMBEDDED_KEY = "com.android.launcher3."
+            + "WallpaperChooserDialogFragment.EMBEDDED_KEY";
+
+    private boolean mEmbedded;
+    private Bitmap mBitmap = null;
+
+    private ArrayList<Integer> mThumbs;
+    private ArrayList<Integer> mImages;
+    private WallpaperLoader mLoader;
+    private WallpaperDrawable mWallpaperDrawable = new WallpaperDrawable();
+
+    public static WallpaperChooserDialogFragment newInstance() {
+        WallpaperChooserDialogFragment fragment = new WallpaperChooserDialogFragment();
+        fragment.setCancelable(true);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null && savedInstanceState.containsKey(EMBEDDED_KEY)) {
+            mEmbedded = savedInstanceState.getBoolean(EMBEDDED_KEY);
+        } else {
+            mEmbedded = isInLayout();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putBoolean(EMBEDDED_KEY, mEmbedded);
+    }
+
+    private void cancelLoader() {
+        if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) {
+            mLoader.cancel(true);
+            mLoader = null;
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        cancelLoader();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        cancelLoader();
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        super.onDismiss(dialog);
+        /* On orientation changes, the dialog is effectively "dismissed" so this is called
+         * when the activity is no longer associated with this dying dialog fragment. We
+         * should just safely ignore this case by checking if getActivity() returns null
+         */
+        Activity activity = getActivity();
+        if (activity != null) {
+            activity.finish();
+        }
+    }
+
+    /* This will only be called when in XLarge mode, since this Fragment is invoked like
+     * a dialog in that mode
+     */
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        findWallpapers();
+
+        return null;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        findWallpapers();
+
+        /* If this fragment is embedded in the layout of this activity, then we should
+         * generate a view to display. Otherwise, a dialog will be created in
+         * onCreateDialog()
+         */
+        if (mEmbedded) {
+            View view = inflater.inflate(R.layout.wallpaper_chooser, container, false);
+            view.setBackground(mWallpaperDrawable);
+
+            final Gallery gallery = (Gallery) view.findViewById(R.id.gallery);
+            gallery.setCallbackDuringFling(false);
+            gallery.setOnItemSelectedListener(this);
+            gallery.setAdapter(new ImageAdapter(getActivity()));
+
+            View setButton = view.findViewById(R.id.set);
+            setButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    selectWallpaper(gallery.getSelectedItemPosition());
+                }
+            });
+            return view;
+        }
+        return null;
+    }
+
+    private void selectWallpaper(int position) {
+        try {
+            WallpaperManager wpm = (WallpaperManager) getActivity().getSystemService(
+                    Context.WALLPAPER_SERVICE);
+            wpm.setResource(mImages.get(position));
+            Activity activity = getActivity();
+            activity.setResult(Activity.RESULT_OK);
+            activity.finish();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to set wallpaper: " + e);
+        }
+    }
+
+    // Click handler for the Dialog's GridView
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        selectWallpaper(position);
+    }
+
+    // Selection handler for the embedded Gallery view
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) {
+            mLoader.cancel();
+        }
+        mLoader = (WallpaperLoader) new WallpaperLoader().execute(position);
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+    }
+
+    private void findWallpapers() {
+        mThumbs = new ArrayList<Integer>(24);
+        mImages = new ArrayList<Integer>(24);
+
+        final Resources resources = getResources();
+        // Context.getPackageName() may return the "original" package name,
+        // com.android.launcher3; Resources needs the real package name,
+        // com.android.launcher3. So we ask Resources for what it thinks the
+        // package name should be.
+        final String packageName = resources.getResourcePackageName(R.array.wallpapers);
+
+        addWallpapers(resources, packageName, R.array.wallpapers);
+        addWallpapers(resources, packageName, R.array.extra_wallpapers);
+    }
+
+    private void addWallpapers(Resources resources, String packageName, int list) {
+        final String[] extras = resources.getStringArray(list);
+        for (String extra : extras) {
+            int res = resources.getIdentifier(extra, "drawable", packageName);
+            if (res != 0) {
+                final int thumbRes = resources.getIdentifier(extra + "_small",
+                        "drawable", packageName);
+
+                if (thumbRes != 0) {
+                    mThumbs.add(thumbRes);
+                    mImages.add(res);
+                    // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")");
+                }
+            }
+        }
+    }
+
+    private class ImageAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter {
+        private LayoutInflater mLayoutInflater;
+
+        ImageAdapter(Activity activity) {
+            mLayoutInflater = activity.getLayoutInflater();
+        }
+
+        public int getCount() {
+            return mThumbs.size();
+        }
+
+        public Object getItem(int position) {
+            return position;
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view;
+
+            if (convertView == null) {
+                view = mLayoutInflater.inflate(R.layout.wallpaper_item, parent, false);
+            } else {
+                view = convertView;
+            }
+
+            ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
+
+            int thumbRes = mThumbs.get(position);
+            image.setImageResource(thumbRes);
+            Drawable thumbDrawable = image.getDrawable();
+            if (thumbDrawable != null) {
+                thumbDrawable.setDither(true);
+            } else {
+                Log.e(TAG, "Error decoding thumbnail resId=" + thumbRes + " for wallpaper #"
+                        + position);
+            }
+
+            return view;
+        }
+    }
+
+    class WallpaperLoader extends AsyncTask<Integer, Void, Bitmap> {
+        BitmapFactory.Options mOptions;
+
+        WallpaperLoader() {
+            mOptions = new BitmapFactory.Options();
+            mOptions.inDither = false;
+            mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        }
+
+        @Override
+        protected Bitmap doInBackground(Integer... params) {
+            if (isCancelled()) return null;
+            try {
+                return BitmapFactory.decodeResource(getResources(),
+                        mImages.get(params[0]), mOptions);
+            } catch (OutOfMemoryError e) {
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap b) {
+            if (b == null) return;
+
+            if (!isCancelled() && !mOptions.mCancel) {
+                // Help the GC
+                if (mBitmap != null) {
+                    mBitmap.recycle();
+                }
+
+                View v = getView();
+                if (v != null) {
+                    mBitmap = b;
+                    mWallpaperDrawable.setBitmap(b);
+                    v.postInvalidate();
+                } else {
+                    mBitmap = null;
+                    mWallpaperDrawable.setBitmap(null);
+                }
+                mLoader = null;
+            } else {
+               b.recycle();
+            }
+        }
+
+        void cancel() {
+            mOptions.requestCancelDecode();
+            super.cancel(true);
+        }
+    }
+
+    /**
+     * Custom drawable that centers the bitmap fed to it.
+     */
+    static class WallpaperDrawable extends Drawable {
+
+        Bitmap mBitmap;
+        int mIntrinsicWidth;
+        int mIntrinsicHeight;
+
+        /* package */void setBitmap(Bitmap bitmap) {
+            mBitmap = bitmap;
+            if (mBitmap == null)
+                return;
+            mIntrinsicWidth = mBitmap.getWidth();
+            mIntrinsicHeight = mBitmap.getHeight();
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            if (mBitmap == null) return;
+            int width = canvas.getWidth();
+            int height = canvas.getHeight();
+            int x = (width - mIntrinsicWidth) / 2;
+            int y = (height - mIntrinsicHeight) / 2;
+            canvas.drawBitmap(mBitmap, x, y, null);
+        }
+
+        @Override
+        public int getOpacity() {
+            return android.graphics.PixelFormat.OPAQUE;
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // Ignore
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter cf) {
+            // Ignore
+        }
+    }
+}
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
new file mode 100644
index 0000000..ddc478a
--- /dev/null
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -0,0 +1,610 @@
+package com.android.launcher3;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+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.Bitmap.Config;
+import android.graphics.BitmapFactory;
+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.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.android.launcher3.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+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;
+        }
+    }
+}
+
+class CanvasCache extends SoftReferenceThreadLocal<Canvas> {
+    @Override
+    protected Canvas initialValue() {
+        return new Canvas();
+    }
+}
+
+class PaintCache extends SoftReferenceThreadLocal<Paint> {
+    @Override
+    protected Paint initialValue() {
+        return null;
+    }
+}
+
+class BitmapCache extends SoftReferenceThreadLocal<Bitmap> {
+    @Override
+    protected Bitmap initialValue() {
+        return null;
+    }
+}
+
+class RectCache extends SoftReferenceThreadLocal<Rect> {
+    @Override
+    protected Rect initialValue() {
+        return new Rect();
+    }
+}
+
+class BitmapFactoryOptionsCache extends SoftReferenceThreadLocal<BitmapFactory.Options> {
+    @Override
+    protected BitmapFactory.Options initialValue() {
+        return new BitmapFactory.Options();
+    }
+}
+
+public class WidgetPreviewLoader {
+    static final String TAG = "WidgetPreviewLoader";
+
+    private int mPreviewBitmapWidth;
+    private int mPreviewBitmapHeight;
+    private String mSize;
+    private Context mContext;
+    private Launcher mLauncher;
+    private PackageManager mPackageManager;
+    private PagedViewCellLayout mWidgetSpacingLayout;
+
+    // Used for drawing shortcut previews
+    private BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache();
+    private PaintCache mCachedShortcutPreviewPaint = new PaintCache();
+    private CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache();
+
+    // Used for drawing widget previews
+    private CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache();
+    private RectCache mCachedAppWidgetPreviewSrcRect = new RectCache();
+    private RectCache mCachedAppWidgetPreviewDestRect = new RectCache();
+    private PaintCache mCachedAppWidgetPreviewPaint = new PaintCache();
+    private String mCachedSelectQuery;
+    private BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache();
+
+    private int mAppIconSize;
+    private IconCache mIconCache;
+
+    private final float sWidgetPreviewIconPaddingPercentage = 0.25f;
+
+    private CacheDb mDb;
+
+    private HashMap<String, WeakReference<Bitmap>> mLoadedPreviews;
+    private ArrayList<SoftReference<Bitmap>> mUnusedBitmaps;
+    private static HashSet<String> sInvalidPackages;
+
+    static {
+        sInvalidPackages = new HashSet<String>();
+    }
+
+    public WidgetPreviewLoader(Launcher launcher) {
+        mContext = mLauncher = launcher;
+        mPackageManager = mContext.getPackageManager();
+        mAppIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.app_icon_size);
+        LauncherApplication app = (LauncherApplication) launcher.getApplicationContext();
+        mIconCache = app.getIconCache();
+        mDb = app.getWidgetPreviewCacheDb();
+        mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>();
+        mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>();
+    }
+
+    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) {
+        String name = getObjectName(o);
+        // check if the package is valid
+        boolean packageValid = true;
+        synchronized(sInvalidPackages) {
+            packageValid = !sInvalidPackages.contains(getObjectPackage(o));
+        }
+        if (!packageValid) {
+            return null;
+        }
+        if (packageValid) {
+            synchronized(mLoadedPreviews) {
+                // check if it exists in our existing cache
+                if (mLoadedPreviews.containsKey(name) && mLoadedPreviews.get(name).get() != null) {
+                    return mLoadedPreviews.get(name).get();
+                }
+            }
+        }
+
+        Bitmap unusedBitmap = null;
+        synchronized(mUnusedBitmaps) {
+            // not in cache; we need to load it from the db
+            while ((unusedBitmap == null || !unusedBitmap.isMutable() ||
+                    unusedBitmap.getWidth() != mPreviewBitmapWidth ||
+                    unusedBitmap.getHeight() != mPreviewBitmapHeight)
+                    && mUnusedBitmaps.size() > 0) {
+                unusedBitmap = mUnusedBitmaps.remove(0).get();
+            }
+            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 = null;
+
+        if (packageValid) {
+            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");
+                }
+            }
+        }
+    }
+
+    static class CacheDb extends SQLiteOpenHelper {
+        final static int DB_VERSION = 2;
+        final static String DB_NAME = "widgetpreviews.db";
+        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;
+
+        public CacheDb(Context context) {
+            super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION);
+            // Store the context for later use
+            mContext = context;
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase database) {
+            database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+                    COLUMN_NAME + " TEXT NOT NULL, " +
+                    COLUMN_SIZE + " TEXT NOT NULL, " +
+                    COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " +
+                    "PRIMARY KEY (" + COLUMN_NAME + ", " + 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);
+            }
+        }
+    }
+
+    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).provider.flattenToString());
+            output = sb.toString();
+            sb.setLength(0);
+        } else {
+            sb.append(SHORTCUT_PREFIX);
+
+            ResolveInfo info = (ResolveInfo) o;
+            sb.append(new ComponentName(info.activityInfo.packageName,
+                    info.activityInfo.name).flattenToString());
+            output = sb.toString();
+            sb.setLength(0);
+        }
+        return output;
+    }
+
+    private String getObjectPackage(Object o) {
+        if (o instanceof AppWidgetProviderInfo) {
+            return ((AppWidgetProviderInfo) o).provider.getPackageName();
+        } else {
+            ResolveInfo info = (ResolveInfo) o;
+            return info.activityInfo.packageName;
+        }
+    }
+
+    private void writeToDb(Object o, Bitmap preview) {
+        String name = getObjectName(o);
+        SQLiteDatabase db = mDb.getWritableDatabase();
+        ContentValues values = new ContentValues();
+
+        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);
+        db.insert(CacheDb.TABLE_NAME, null, values);
+    }
+
+    public static void removeFromDb(final CacheDb cacheDb, final String packageName) {
+        synchronized(sInvalidPackages) {
+            sInvalidPackages.add(packageName);
+        }
+        new AsyncTask<Void, Void, Void>() {
+            public Void doInBackground(Void ... args) {
+                SQLiteDatabase db = cacheDb.getWritableDatabase();
+                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
+                            );
+                synchronized(sInvalidPackages) {
+                    sInvalidPackages.remove(packageName);
+                }
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+    }
+
+    private Bitmap readFromDb(String name, Bitmap b) {
+        if (mCachedSelectQuery == null) {
+            mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " +
+                    CacheDb.COLUMN_SIZE + " = ?";
+        }
+        SQLiteDatabase db = mDb.getReadableDatabase();
+        Cursor 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);
+        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;
+            Bitmap out = BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
+            return out;
+        } else {
+            result.close();
+            return null;
+        }
+    }
+
+    public 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);
+        } else {
+            return generateShortcutPreview(
+                    (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview);
+        }
+    }
+
+    public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) {
+        int[] cellSpans = Launcher.getSpanForWidget(mLauncher, info);
+        int maxWidth = maxWidthForWidgetPreview(cellSpans[0]);
+        int maxHeight = maxHeightForWidgetPreview(cellSpans[1]);
+        return generateWidgetPreview(info.provider, info.previewImage, info.icon,
+                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(ComponentName provider, int previewImage,
+            int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight,
+            Bitmap preview, int[] preScaledWidthOut) {
+        // Load the preview image if possible
+        String packageName = provider.getPackageName();
+        if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
+        if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE;
+
+        Drawable drawable = null;
+        if (previewImage != 0) {
+            drawable = mPackageManager.getDrawable(packageName, previewImage, null);
+            if (drawable == null) {
+                Log.w(TAG, "Can't load widget preview drawable 0x" +
+                        Integer.toHexString(previewImage) + " for provider: " + provider);
+            }
+        }
+
+        int previewWidth;
+        int previewHeight;
+        Bitmap defaultPreview = null;
+        boolean widgetPreviewExists = (drawable != 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;
+
+            BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources()
+                    .getDrawable(R.drawable.widget_preview_tile);
+            final int previewDrawableWidth = previewDrawable
+                    .getIntrinsicWidth();
+            final int previewDrawableHeight = previewDrawable
+                    .getIntrinsicHeight();
+            previewWidth = previewDrawableWidth * cellHSpan; // subtract 2 dips
+            previewHeight = previewDrawableHeight * cellVSpan;
+
+            defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight,
+                    Config.ARGB_8888);
+            final Canvas c = mCachedAppWidgetPreviewCanvas.get();
+            c.setBitmap(defaultPreview);
+            previewDrawable.setBounds(0, 0, previewWidth, previewHeight);
+            previewDrawable.setTileModeXY(Shader.TileMode.REPEAT,
+                    Shader.TileMode.REPEAT);
+            previewDrawable.draw(c);
+            c.setBitmap(null);
+
+            // Draw the icon in the top left corner
+            int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage);
+            int smallestSide = Math.min(previewWidth, previewHeight);
+            float iconScale = Math.min((float) smallestSide
+                    / (mAppIconSize + 2 * minOffset), 1f);
+
+            try {
+                Drawable icon = null;
+                int hoffset =
+                        (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2);
+                int yoffset =
+                        (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2);
+                if (iconId > 0)
+                    icon = mIconCache.getFullResIcon(packageName, iconId);
+                if (icon != null) {
+                    renderDrawableToBitmap(icon, defaultPreview, hoffset,
+                            yoffset, (int) (mAppIconSize * iconScale),
+                            (int) (mAppIconSize * iconScale));
+                }
+            } catch (Resources.NotFoundException e) {
+            }
+        }
+
+        // Scale to fit width only - let the widget preview be clipped in the
+        // vertical dimension
+        float scale = 1f;
+        if (preScaledWidthOut != null) {
+            preScaledWidthOut[0] = previewWidth;
+        }
+        if (previewWidth > maxPreviewWidth) {
+            scale = maxPreviewWidth / (float) previewWidth;
+        }
+        if (scale != 1f) {
+            previewWidth = (int) (scale * previewWidth);
+            previewHeight = (int) (scale * previewHeight);
+        }
+
+        // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size
+        if (preview == null) {
+            preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
+        }
+
+        // Draw the scaled preview into the final bitmap
+        int x = (preview.getWidth() - previewWidth) / 2;
+        if (widgetPreviewExists) {
+            renderDrawableToBitmap(drawable, preview, x, 0, previewWidth,
+                    previewHeight);
+        } 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);
+            }
+            c.drawBitmap(defaultPreview, src, dest, p);
+            c.setBitmap(null);
+        }
+        return preview;
+    }
+
+    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);
+        } else {
+            c.setBitmap(tempBitmap);
+            c.drawColor(0, PorterDuff.Mode.CLEAR);
+            c.setBitmap(null);
+        }
+        // Render the icon
+        Drawable icon = mIconCache.getFullResIcon(info);
+
+        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);
+
+        int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
+
+        renderDrawableToBitmap(
+                icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth);
+
+        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);
+        }
+
+        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;
+    }
+
+
+    public static void renderDrawableToBitmap(
+            Drawable d, Bitmap bitmap, int x, int y, int w, int h) {
+        renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f);
+    }
+
+    private static void renderDrawableToBitmap(
+            Drawable d, Bitmap bitmap, int x, int y, int w, int h,
+            float scale) {
+        if (bitmap != null) {
+            Canvas c = new Canvas(bitmap);
+            c.scale(scale, scale);
+            Rect oldBounds = d.copyBounds();
+            d.setBounds(x, y, x + w, y + h);
+            d.draw(c);
+            d.setBounds(oldBounds); // Restore the bounds
+            c.setBitmap(null);
+        }
+    }
+
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
new file mode 100644
index 0000000..4f1fb08
--- /dev/null
+++ b/src/com/android/launcher3/Workspace.java
@@ -0,0 +1,3894 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+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.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.drawable.Drawable;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.FolderIcon.FolderRingAnimator;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * The workspace is a wide area with a wallpaper and a finite number of pages.
+ * 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
+        implements DropTarget, DragSource, DragScroller, View.OnTouchListener,
+        DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener {
+    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 final int BACKGROUND_FADE_OUT_DURATION = 350;
+    private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
+    private static final int FLING_THRESHOLD_VELOCITY = 500;
+
+    // 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 Drawable mBackground;
+    boolean mDrawBackground = true;
+    private float mBackgroundAlpha = 0;
+
+    private float mWallpaperScrollRatio = 1.0f;
+    private int mOriginalPageSpacing;
+
+    private final WallpaperManager mWallpaperManager;
+    private IBinder mWindowToken;
+    private static final float WALLPAPER_SCREENS_SPAN = 2f;
+
+    private int mDefaultPage;
+
+    /**
+     * CellInfo for the cell that is currently being dragged
+     */
+    private CellLayout.CellInfo mDragInfo;
+
+    /**
+     * Target drop area calculated during last acceptDrop call.
+     */
+    private int[] mTargetCell = new int[2];
+    private int mDragOverX = -1;
+    private int mDragOverY = -1;
+
+    static Rect mLandscapeCellLayoutMetrics = null;
+    static Rect mPortraitCellLayoutMetrics = null;
+
+    /**
+     * The CellLayout that is currently being dragged over
+     */
+    private CellLayout mDragTargetLayout = null;
+    /**
+     * The CellLayout that we will show as glowing
+     */
+    private CellLayout mDragOverlappingLayout = null;
+
+    /**
+     * The CellLayout which will be dropped to
+     */
+    private CellLayout mDropToLayout = null;
+
+    private Launcher mLauncher;
+    private IconCache mIconCache;
+    private 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[] mTempEstimate = new int[2];
+    private float[] mDragViewVisualCenter = new float[2];
+    private float[] mTempDragCoordinates = new float[2];
+    private float[] mTempCellLayoutCenterCoordinates = new float[2];
+    private float[] mTempDragBottomRightCoordinates = new float[2];
+    private Matrix mTempInverseMatrix = new Matrix();
+
+    private SpringLoadedDragController mSpringLoadedDragController;
+    private float mSpringLoadedShrinkFactor;
+
+    private static final int DEFAULT_CELL_COUNT_X = 4;
+    private static final int DEFAULT_CELL_COUNT_Y = 4;
+
+    // State variable that indicates whether the pages are small (ie when you're
+    // in all apps or customize mode)
+
+    enum State { NORMAL, SPRING_LOADED, SMALL };
+    private State mState = State.NORMAL;
+    private boolean mIsSwitchingState = false;
+
+    boolean mAnimatingViewIntoPlace = false;
+    boolean mIsDragOccuring = false;
+    boolean mChildrenLayersEnabled = true;
+
+    /** Is the user is dragging an item near the edge of a page? */
+    private boolean mInScrollArea = false;
+
+    private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper();
+    private Bitmap mDragOutline = null;
+    private final Rect mTempRect = new Rect();
+    private final int[] mTempXY = new int[2];
+    private int[] mTempVisiblePagesRange = new int[2];
+    private float mOverscrollFade = 0;
+    private boolean mOverscrollTransformsSet;
+    public static final int DRAG_BITMAP_PADDING = 2;
+    private boolean mWorkspaceFadeInAdjacentScreens;
+
+    enum WallpaperVerticalOffset { TOP, MIDDLE, BOTTOM };
+    int mWallpaperWidth;
+    int mWallpaperHeight;
+    WallpaperOffsetInterpolator mWallpaperOffset;
+    boolean mUpdateWallpaperOffsetImmediately = false;
+    private Runnable mDelayedResizeRunnable;
+    private Runnable mDelayedSnapToPageRunnable;
+    private Point mDisplaySize = new Point();
+    private boolean mIsStaticWallpaper;
+    private int mWallpaperTravelWidth;
+    private int mSpringLoadedPageSpacing;
+    private int mCameraDistance;
+
+    // Variables relating to the creation of user folders by hovering shortcuts over shortcuts
+    private static final int FOLDER_CREATION_TIMEOUT = 0;
+    private static final int REORDER_TIMEOUT = 250;
+    private final Alarm mFolderCreationAlarm = new Alarm();
+    private final Alarm mReorderAlarm = new Alarm();
+    private FolderRingAnimator mDragFolderRingAnimator = null;
+    private FolderIcon mDragOverFolderIcon = null;
+    private boolean mCreateUserFolderOnDrop = false;
+    private boolean mAddToExistingFolderOnDrop = false;
+    private DropTarget.DragEnforcer mDragEnforcer;
+    private float mMaxDistanceForFolderCreation;
+
+    // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget)
+    private float mXDown;
+    private float mYDown;
+    final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
+    final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
+    final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
+
+    // Relating to the animation of items being dropped externally
+    public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0;
+    public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1;
+    public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2;
+    public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3;
+    public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4;
+
+    // Related to dragging, folder creation and reordering
+    private static final int DRAG_MODE_NONE = 0;
+    private static final int DRAG_MODE_CREATE_FOLDER = 1;
+    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;
+
+    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 mCurrentScaleX;
+    private float mCurrentScaleY;
+    private float mCurrentRotationY;
+    private float mCurrentTranslationX;
+    private float mCurrentTranslationY;
+    private float[] mOldTranslationXs;
+    private float[] mOldTranslationYs;
+    private float[] mOldScaleXs;
+    private float[] mOldScaleYs;
+    private float[] mOldBackgroundAlphas;
+    private float[] mOldAlphas;
+    private float[] mNewTranslationXs;
+    private float[] mNewTranslationYs;
+    private float[] mNewScaleXs;
+    private float[] mNewScaleYs;
+    private float[] mNewBackgroundAlphas;
+    private float[] mNewAlphas;
+    private float[] mNewRotationYs;
+    private float mTransitionProgress;
+
+    private final Runnable mBindPages = new Runnable() {
+        @Override
+        public void run() {
+            mLauncher.getModel().bindRemainingSynchronousPages();
+        }
+    };
+
+    /**
+     * 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 Workspace(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 Workspace(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mContentIsRefreshable = false;
+        mOriginalPageSpacing = mPageSpacing;
+
+        mDragEnforcer = new DropTarget.DragEnforcer(context);
+        // With workspace, data is available straight from the get-go
+        setDataIsReady();
+
+        mLauncher = (Launcher) context;
+        final Resources res = getResources();
+        mWorkspaceFadeInAdjacentScreens = res.getBoolean(R.bool.config_workspaceFadeAdjacentScreens);
+        mFadeInAdjacentScreens = false;
+        mWallpaperManager = WallpaperManager.getInstance(context);
+
+        int cellCountX = DEFAULT_CELL_COUNT_X;
+        int cellCountY = DEFAULT_CELL_COUNT_Y;
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.Workspace, defStyle, 0);
+
+        if (LauncherApplication.isScreenLarge()) {
+            // Determine number of rows/columns dynamically
+            // TODO: This code currently fails on tablets with an aspect ratio < 1.3.
+            // Around that ratio we should make cells the same size in portrait and
+            // landscape
+            TypedArray actionBarSizeTypedArray =
+                context.obtainStyledAttributes(new int[] { android.R.attr.actionBarSize });
+            final float actionBarHeight = actionBarSizeTypedArray.getDimension(0, 0f);
+
+            Point minDims = new Point();
+            Point maxDims = new Point();
+            mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
+
+            cellCountX = 1;
+            while (CellLayout.widthInPortrait(res, cellCountX + 1) <= minDims.x) {
+                cellCountX++;
+            }
+
+            cellCountY = 1;
+            while (actionBarHeight + CellLayout.heightInLandscape(res, cellCountY + 1)
+                <= minDims.y) {
+                cellCountY++;
+            }
+        }
+
+        mSpringLoadedShrinkFactor =
+            res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f;
+        mSpringLoadedPageSpacing =
+                res.getDimensionPixelSize(R.dimen.workspace_spring_loaded_page_spacing);
+        mCameraDistance = res.getInteger(R.integer.config_cameraDistance);
+
+        // if the value is manually specified, use that instead
+        cellCountX = a.getInt(R.styleable.Workspace_cellCountX, cellCountX);
+        cellCountY = a.getInt(R.styleable.Workspace_cellCountY, cellCountY);
+        mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1);
+        a.recycle();
+
+        setOnHierarchyChangeListener(this);
+
+        LauncherModel.updateWorkspaceLayoutCells(cellCountX, cellCountY);
+        setHapticFeedbackEnabled(false);
+
+        initWorkspace();
+
+        // Disable multitouch across the workspace/all apps/customize tray
+        setMotionEventSplittingEnabled(true);
+
+        // Unless otherwise specified this view is important for accessibility.
+        if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    // 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) {
+        int[] size = new int[2];
+        if (getChildCount() > 0) {
+            CellLayout cl = (CellLayout) mLauncher.getWorkspace().getChildAt(0);
+            Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan);
+            size[0] = r.width();
+            size[1] = r.height();
+            if (springLoaded) {
+                size[0] *= mSpringLoadedShrinkFactor;
+                size[1] *= mSpringLoadedShrinkFactor;
+            }
+            return size;
+        } else {
+            size[0] = Integer.MAX_VALUE;
+            size[1] = Integer.MAX_VALUE;
+            return size;
+        }
+    }
+    public Rect estimateItemPosition(CellLayout cl, ItemInfo pendingInfo,
+            int hCell, int vCell, int hSpan, int vSpan) {
+        Rect r = new Rect();
+        cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
+        return r;
+    }
+
+    public void onDragStart(DragSource source, Object info, int dragAction) {
+        mIsDragOccuring = true;
+        updateChildrenLayersEnabled(false);
+        mLauncher.lockScreenOrientation();
+        setChildrenBackgroundAlphaMultipliers(1f);
+        // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging
+        InstallShortcutReceiver.enableInstallQueue();
+        UninstallShortcutReceiver.enableUninstallQueue();
+    }
+
+    public void onDragEnd() {
+        mIsDragOccuring = false;
+        updateChildrenLayersEnabled(false);
+        mLauncher.unlockScreenOrientation(false);
+
+        // Re-enable any Un/InstallShortcutReceiver and now process any queued items
+        InstallShortcutReceiver.disableAndFlushInstallQueue(getContext());
+        UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext());
+    }
+
+    /**
+     * Initializes various states for this workspace.
+     */
+    protected void initWorkspace() {
+        Context context = getContext();
+        mCurrentPage = mDefaultPage;
+        Launcher.setScreen(mCurrentPage);
+        LauncherApplication app = (LauncherApplication)context.getApplicationContext();
+        mIconCache = app.getIconCache();
+        setWillNotDraw(false);
+        setClipChildren(false);
+        setClipToPadding(false);
+        setChildrenDrawnWithCacheEnabled(true);
+
+        final Resources res = getResources();
+        try {
+            mBackground = res.getDrawable(R.drawable.apps_customize_bg);
+        } catch (Resources.NotFoundException e) {
+            // In this case, we will skip drawing background protection
+        }
+
+        mWallpaperOffset = new WallpaperOffsetInterpolator();
+        Display display = mLauncher.getWindowManager().getDefaultDisplay();
+        display.getSize(mDisplaySize);
+        mWallpaperTravelWidth = (int) (mDisplaySize.x *
+                wallpaperTravelToScreenWidthRatio(mDisplaySize.x, mDisplaySize.y));
+
+        mMaxDistanceForFolderCreation = (0.55f * res.getDimensionPixelSize(R.dimen.app_icon_size));
+        mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
+    }
+
+    @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.");
+        }
+        CellLayout cl = ((CellLayout) child);
+        cl.setOnInterceptTouchListener(this);
+        cl.setClickable(true);
+        cl.setContentDescription(getContext().getString(
+                R.string.workspace_description_format, getChildCount()));
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+    }
+
+    protected boolean shouldDrawChild(View child) {
+        final CellLayout cl = (CellLayout) child;
+        return super.shouldDrawChild(child) &&
+            (cl.getShortcutsAndWidgets().getAlpha() > 0 ||
+             cl.getBackgroundAlpha() > 0);
+    }
+
+    /**
+     * @return The open folder on the current screen, or null if there is none
+     */
+    Folder getOpenFolder() {
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        int count = dragLayer.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = dragLayer.getChildAt(i);
+            if (child instanceof Folder) {
+                Folder folder = (Folder) child;
+                if (folder.getInfo().opened)
+                    return folder;
+            }
+        }
+        return null;
+    }
+
+    boolean isTouchActive() {
+        return mTouchState != TOUCH_STATE_REST;
+    }
+
+    /**
+     * Adds the specified child in the specified screen. The position and dimension of
+     * the child are defined by x, y, spanX and spanY.
+     *
+     * @param child The child to add in one of the workspace's screens.
+     * @param screen The screen in which to add the child.
+     * @param x The X position of the child in the screen's grid.
+     * @param y The Y position of the child in the screen's grid.
+     * @param spanX The number of cells spanned horizontally by the child.
+     * @param spanY The number of cells spanned vertically by the child.
+     */
+    void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY) {
+        addInScreen(child, container, screen, x, y, spanX, spanY, false);
+    }
+
+    /**
+     * Adds the specified child in the specified screen. The position and dimension of
+     * the child are defined by x, y, spanX and spanY.
+     *
+     * @param child The child to add in one of the workspace's screens.
+     * @param screen The screen in which to add the child.
+     * @param x The X position of the child in the screen's grid.
+     * @param y The Y position of the child in the screen's grid.
+     * @param spanX The number of cells spanned horizontally by the child.
+     * @param spanY The number of cells spanned vertically by the child.
+     * @param insert When true, the child is inserted at the beginning of the children list.
+     */
+    void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY,
+            boolean insert) {
+        if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+            if (screen < 0 || screen >= getChildCount()) {
+                Log.e(TAG, "The screen must be >= 0 and < " + getChildCount()
+                    + " (was " + screen + "); skipping child");
+                return;
+            }
+        }
+
+        final CellLayout layout;
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            layout = mLauncher.getHotseat().getLayout();
+            child.setOnKeyListener(null);
+
+            // Hide folder title in the hotseat
+            if (child instanceof FolderIcon) {
+                ((FolderIcon) child).setTextVisible(false);
+            }
+
+            if (screen < 0) {
+                screen = mLauncher.getHotseat().getOrderInHotseat(x, y);
+            } else {
+                // 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
+                x = mLauncher.getHotseat().getCellXFromOrder(screen);
+                y = mLauncher.getHotseat().getCellYFromOrder(screen);
+            }
+        } else {
+            // Show folder title if not in the hotseat
+            if (child instanceof FolderIcon) {
+                ((FolderIcon) child).setTextVisible(true);
+            }
+
+            layout = (CellLayout) getChildAt(screen);
+            child.setOnKeyListener(new IconKeyEventListener());
+        }
+
+        LayoutParams genericLp = child.getLayoutParams();
+        CellLayout.LayoutParams lp;
+        if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) {
+            lp = new CellLayout.LayoutParams(x, y, spanX, spanY);
+        } else {
+            lp = (CellLayout.LayoutParams) genericLp;
+            lp.cellX = x;
+            lp.cellY = y;
+            lp.cellHSpan = spanX;
+            lp.cellVSpan = spanY;
+        }
+
+        if (spanX < 0 && spanY < 0) {
+            lp.isLockedToGrid = false;
+        }
+
+        // Get the canonical child id to uniquely represent this view in this screen
+        int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY);
+        boolean markCellsAsOccupied = !(child instanceof Folder);
+        if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) {
+            // TODO: This branch occurs when the workspace is adding views
+            // outside of the defined grid
+            // maybe we should be deleting these items from the LauncherModel?
+            Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout");
+        }
+
+        if (!(child instanceof Folder)) {
+            child.setHapticFeedbackEnabled(false);
+            child.setOnLongClickListener(mLongClickListener);
+        }
+        if (child instanceof DropTarget) {
+            mDragController.addDropTarget((DropTarget) child);
+        }
+    }
+
+    /**
+     * Check if the point (x, y) hits a given page.
+     */
+    private boolean hitsPage(int index, float x, float y) {
+        final View page = getChildAt(index);
+        if (page != null) {
+            float[] localXY = { x, y };
+            mapPointFromSelfToChild(page, localXY);
+            return (localXY[0] >= 0 && localXY[0] < page.getWidth()
+                    && localXY[1] >= 0 && localXY[1] < page.getHeight());
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean hitsPreviousPage(float x, float y) {
+        // mNextPage is set to INVALID_PAGE whenever we are stationary.
+        // Calculating "next page" this way ensures that you scroll to whatever page you tap on
+        final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage;
+
+        // Only allow tap to next page on large devices, where there's significant margin outside
+        // the active workspace
+        return LauncherApplication.isScreenLarge() && hitsPage(current - 1, x, y);
+    }
+
+    @Override
+    protected boolean hitsNextPage(float x, float y) {
+        // mNextPage is set to INVALID_PAGE whenever we are stationary.
+        // Calculating "next page" this way ensures that you scroll to whatever page you tap on
+        final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage;
+
+        // Only allow tap to next page on large devices, where there's significant margin outside
+        // the active workspace
+        return LauncherApplication.isScreenLarge() && hitsPage(current + 1, x, y);
+    }
+
+    /**
+     * Called directly from a CellLayout (not by the framework), after we've been added as a
+     * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout
+     * that it should intercept touch events, which is not something that is normally supported.
+     */
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        return (isSmall() || !isFinishedSwitchingState());
+    }
+
+    public boolean isSwitchingState() {
+        return mIsSwitchingState;
+    }
+
+    /** This differs from isSwitchingState in that we take into account how far the transition
+     *  has completed. */
+    public boolean isFinishedSwitchingState() {
+        return !mIsSwitchingState || (mTransitionProgress > 0.5f);
+    }
+
+    protected void onWindowVisibilityChanged (int visibility) {
+        mLauncher.onWindowVisibilityChanged(visibility);
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        if (isSmall() || !isFinishedSwitchingState()) {
+            // when the home screens are shrunken, shouldn't allow side-scrolling
+            return false;
+        }
+        return super.dispatchUnhandledMove(focused, direction);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+        case MotionEvent.ACTION_DOWN:
+            mXDown = ev.getX();
+            mYDown = ev.getY();
+            break;
+        case MotionEvent.ACTION_POINTER_UP:
+        case MotionEvent.ACTION_UP:
+            if (mTouchState == TOUCH_STATE_REST) {
+                final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage);
+                if (!currentPage.lastDownOnOccupiedCell()) {
+                    onWallpaperTap(ev);
+                }
+            }
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    protected void reinflateWidgetsIfNecessary() {
+        final int clCount = getChildCount();
+        for (int i = 0; i < clCount; i++) {
+            CellLayout cl = (CellLayout) getChildAt(i);
+            ShortcutAndWidgetContainer swc = cl.getShortcutsAndWidgets();
+            final int itemCount = swc.getChildCount();
+            for (int j = 0; j < itemCount; j++) {
+                View v = swc.getChildAt(j);
+
+                if (v.getTag() instanceof LauncherAppWidgetInfo) {
+                    LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
+                    LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) info.hostView;
+                    if (lahv != null && lahv.orientationChangedSincedInflation()) {
+                        mLauncher.removeAppWidget(info);
+                        // Remove the current widget which is inflated with the wrong orientation
+                        cl.removeView(lahv);
+                        mLauncher.bindAppWidget(info);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void determineScrollingStart(MotionEvent ev) {
+        if (isSmall()) return;
+        if (!isFinishedSwitchingState()) return;
+
+        float deltaX = Math.abs(ev.getX() - mXDown);
+        float deltaY = Math.abs(ev.getY() - mYDown);
+
+        if (Float.compare(deltaX, 0f) == 0) return;
+
+        float slope = deltaY / deltaX;
+        float theta = (float) Math.atan(slope);
+
+        if (deltaX > mTouchSlop || deltaY > mTouchSlop) {
+            cancelCurrentPageLongPress();
+        }
+
+        if (theta > MAX_SWIPE_ANGLE) {
+            // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace
+            return;
+        } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
+            // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to
+            // increase the touch slop to make it harder to begin scrolling the workspace. This
+            // results in vertically scrolling widgets to more easily. The higher the angle, the
+            // more we increase touch slop.
+            theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
+            float extraRatio = (float)
+                    Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
+            super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
+        } else {
+            // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special
+            super.determineScrollingStart(ev);
+        }
+    }
+
+    protected void onPageBeginMoving() {
+        super.onPageBeginMoving();
+
+        if (isHardwareAccelerated()) {
+            updateChildrenLayersEnabled(false);
+        } else {
+            if (mNextPage != INVALID_PAGE) {
+                // we're snapping to a particular screen
+                enableChildrenCache(mCurrentPage, mNextPage);
+            } else {
+                // this is when user is actively dragging a particular screen, they might
+                // swipe it either left or right (but we won't advance by more than one screen)
+                enableChildrenCache(mCurrentPage - 1, mCurrentPage + 1);
+            }
+        }
+
+        // Only show page outlines as we pan if we are on large screen
+        if (LauncherApplication.isScreenLarge()) {
+            showOutlines();
+            mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null;
+        }
+
+        // If we are not fading in adjacent screens, we still need to restore the alpha in case the
+        // user scrolls while we are transitioning (should not affect dispatchDraw optimizations)
+        if (!mWorkspaceFadeInAdjacentScreens) {
+            for (int i = 0; i < getChildCount(); ++i) {
+                ((CellLayout) getPageAt(i)).setShortcutAndWidgetAlpha(1f);
+            }
+        }
+
+        // Show the scroll indicator as you pan the page
+        showScrollingIndicator(false);
+    }
+
+    protected void onPageEndMoving() {
+        super.onPageEndMoving();
+
+        if (isHardwareAccelerated()) {
+            updateChildrenLayersEnabled(false);
+        } else {
+            clearChildrenCache();
+        }
+
+
+        if (mDragController.isDragging()) {
+            if (isSmall()) {
+                // If we are in springloaded mode, then force an event to check if the current touch
+                // is under a new page (to scroll to)
+                mDragController.forceTouchMove();
+            }
+        } else {
+            // If we are not mid-dragging, hide the page outlines if we are on a large screen
+            if (LauncherApplication.isScreenLarge()) {
+                hideOutlines();
+            }
+
+            // Hide the scroll indicator as you pan the page
+            if (!mDragController.isDragging()) {
+                hideScrollingIndicator(false);
+            }
+        }
+
+        if (mDelayedResizeRunnable != null) {
+            mDelayedResizeRunnable.run();
+            mDelayedResizeRunnable = null;
+        }
+
+        if (mDelayedSnapToPageRunnable != null) {
+            mDelayedSnapToPageRunnable.run();
+            mDelayedSnapToPageRunnable = null;
+        }
+    }
+
+    @Override
+    protected void notifyPageSwitchListener() {
+        super.notifyPageSwitchListener();
+        Launcher.setScreen(mCurrentPage);
+    };
+
+    // As a ratio of screen height, the total distance we want the parallax effect to span
+    // horizontally
+    private 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;
+    }
+
+    // The range of scroll values for Workspace
+    private int getScrollRange() {
+        return getChildOffset(getChildCount() - 1) - getChildOffset(0);
+    }
+
+    protected void setWallpaperDimension() {
+        Point minDims = new Point();
+        Point maxDims = new Point();
+        mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
+
+        final int maxDim = Math.max(maxDims.x, maxDims.y);
+        final int minDim = Math.min(minDims.x, minDims.y);
+
+        // We need to ensure that there is enough extra space in the wallpaper for the intended
+        // parallax effects
+        if (LauncherApplication.isScreenLarge()) {
+            mWallpaperWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
+            mWallpaperHeight = maxDim;
+        } else {
+            mWallpaperWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
+            mWallpaperHeight = maxDim;
+        }
+        new Thread("setWallpaperDimension") {
+            public void run() {
+                mWallpaperManager.suggestDesiredDimensions(mWallpaperWidth, mWallpaperHeight);
+            }
+        }.start();
+    }
+
+    private float wallpaperOffsetForCurrentScroll() {
+        // Set wallpaper offset steps (1 / (number of screens - 1))
+        mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 1.0f);
+
+        // For the purposes of computing the scrollRange and overScrollOffset, we assume
+        // that mLayoutScale is 1. This means that when we're in spring-loaded mode,
+        // there's no discrepancy between the wallpaper offset for a given page.
+        float layoutScale = mLayoutScale;
+        mLayoutScale = 1f;
+        int scrollRange = getScrollRange();
+
+        // Again, we adjust the wallpaper offset to be consistent between values of mLayoutScale
+        float adjustedScrollX = Math.max(0, Math.min(getScrollX(), mMaxScrollX));
+        adjustedScrollX *= mWallpaperScrollRatio;
+        mLayoutScale = layoutScale;
+
+        float scrollProgress =
+            adjustedScrollX / (float) scrollRange;
+
+        if (LauncherApplication.isScreenLarge() && mIsStaticWallpaper) {
+            // The wallpaper travel width is how far, from left to right, the wallpaper will move
+            // at this orientation. On tablets in portrait mode we don't move all the way to the
+            // edges of the wallpaper, or otherwise the parallax effect would be too strong.
+            int wallpaperTravelWidth = Math.min(mWallpaperTravelWidth, mWallpaperWidth);
+
+            float offsetInDips = wallpaperTravelWidth * scrollProgress +
+                (mWallpaperWidth - wallpaperTravelWidth) / 2; // center it
+            float offset = offsetInDips / (float) mWallpaperWidth;
+            return offset;
+        } else {
+            return scrollProgress;
+        }
+    }
+
+    private void syncWallpaperOffsetWithScroll() {
+        final boolean enableWallpaperEffects = isHardwareAccelerated();
+        if (enableWallpaperEffects) {
+            mWallpaperOffset.setFinalX(wallpaperOffsetForCurrentScroll());
+        }
+    }
+
+    public void updateWallpaperOffsetImmediately() {
+        mUpdateWallpaperOffsetImmediately = true;
+    }
+
+    private void updateWallpaperOffsets() {
+        boolean updateNow = false;
+        boolean keepUpdating = true;
+        if (mUpdateWallpaperOffsetImmediately) {
+            updateNow = true;
+            keepUpdating = false;
+            mWallpaperOffset.jumpToFinal();
+            mUpdateWallpaperOffsetImmediately = false;
+        } else {
+            updateNow = keepUpdating = mWallpaperOffset.computeScrollOffset();
+        }
+        if (updateNow) {
+            if (mWindowToken != null) {
+                mWallpaperManager.setWallpaperOffsets(mWindowToken,
+                        mWallpaperOffset.getCurrX(), mWallpaperOffset.getCurrY());
+            }
+        }
+        if (keepUpdating) {
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void updateCurrentPageScroll() {
+        super.updateCurrentPageScroll();
+        computeWallpaperScrollRatio(mCurrentPage);
+    }
+
+    @Override
+    protected void snapToPage(int whichPage) {
+        super.snapToPage(whichPage);
+        computeWallpaperScrollRatio(whichPage);
+    }
+
+    @Override
+    protected void snapToPage(int whichPage, int duration) {
+        super.snapToPage(whichPage, duration);
+        computeWallpaperScrollRatio(whichPage);
+    }
+
+    protected void snapToPage(int whichPage, Runnable r) {
+        if (mDelayedSnapToPageRunnable != null) {
+            mDelayedSnapToPageRunnable.run();
+        }
+        mDelayedSnapToPageRunnable = r;
+        snapToPage(whichPage, SLOW_PAGE_SNAP_ANIMATION_DURATION);
+    }
+
+    private void computeWallpaperScrollRatio(int page) {
+        // Here, we determine what the desired scroll would be with and without a layout scale,
+        // and compute a ratio between the two. This allows us to adjust the wallpaper offset
+        // as though there is no layout scale.
+        float layoutScale = mLayoutScale;
+        int scaled = getChildOffset(page) - getRelativeChildOffset(page);
+        mLayoutScale = 1.0f;
+        float unscaled = getChildOffset(page) - getRelativeChildOffset(page);
+        mLayoutScale = layoutScale;
+        if (scaled > 0) {
+            mWallpaperScrollRatio = (1.0f * unscaled) / scaled;
+        } else {
+            mWallpaperScrollRatio = 1f;
+        }
+    }
+
+    class WallpaperOffsetInterpolator {
+        float mFinalHorizontalWallpaperOffset = 0.0f;
+        float mFinalVerticalWallpaperOffset = 0.5f;
+        float mHorizontalWallpaperOffset = 0.0f;
+        float mVerticalWallpaperOffset = 0.5f;
+        long mLastWallpaperOffsetUpdateTime;
+        boolean mIsMovingFast;
+        boolean mOverrideHorizontalCatchupConstant;
+        float mHorizontalCatchupConstant = 0.35f;
+        float mVerticalCatchupConstant = 0.35f;
+
+        public WallpaperOffsetInterpolator() {
+        }
+
+        public void setOverrideHorizontalCatchupConstant(boolean override) {
+            mOverrideHorizontalCatchupConstant = override;
+        }
+
+        public void setHorizontalCatchupConstant(float f) {
+            mHorizontalCatchupConstant = f;
+        }
+
+        public void setVerticalCatchupConstant(float f) {
+            mVerticalCatchupConstant = f;
+        }
+
+        public boolean computeScrollOffset() {
+            if (Float.compare(mHorizontalWallpaperOffset, mFinalHorizontalWallpaperOffset) == 0 &&
+                    Float.compare(mVerticalWallpaperOffset, mFinalVerticalWallpaperOffset) == 0) {
+                mIsMovingFast = false;
+                return false;
+            }
+            boolean isLandscape = mDisplaySize.x > mDisplaySize.y;
+
+            long currentTime = System.currentTimeMillis();
+            long timeSinceLastUpdate = currentTime - mLastWallpaperOffsetUpdateTime;
+            timeSinceLastUpdate = Math.min((long) (1000/30f), timeSinceLastUpdate);
+            timeSinceLastUpdate = Math.max(1L, timeSinceLastUpdate);
+
+            float xdiff = Math.abs(mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset);
+            if (!mIsMovingFast && xdiff > 0.07) {
+                mIsMovingFast = true;
+            }
+
+            float fractionToCatchUpIn1MsHorizontal;
+            if (mOverrideHorizontalCatchupConstant) {
+                fractionToCatchUpIn1MsHorizontal = mHorizontalCatchupConstant;
+            } else if (mIsMovingFast) {
+                fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.5f : 0.75f;
+            } else {
+                // slow
+                fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.27f : 0.5f;
+            }
+            float fractionToCatchUpIn1MsVertical = mVerticalCatchupConstant;
+
+            fractionToCatchUpIn1MsHorizontal /= 33f;
+            fractionToCatchUpIn1MsVertical /= 33f;
+
+            final float UPDATE_THRESHOLD = 0.00001f;
+            float hOffsetDelta = mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset;
+            float vOffsetDelta = mFinalVerticalWallpaperOffset - mVerticalWallpaperOffset;
+            boolean jumpToFinalValue = Math.abs(hOffsetDelta) < UPDATE_THRESHOLD &&
+                Math.abs(vOffsetDelta) < UPDATE_THRESHOLD;
+
+            // Don't have any lag between workspace and wallpaper on non-large devices
+            if (!LauncherApplication.isScreenLarge() || jumpToFinalValue) {
+                mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset;
+                mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset;
+            } else {
+                float percentToCatchUpVertical =
+                    Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsVertical);
+                float percentToCatchUpHorizontal =
+                    Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsHorizontal);
+                mHorizontalWallpaperOffset += percentToCatchUpHorizontal * hOffsetDelta;
+                mVerticalWallpaperOffset += percentToCatchUpVertical * vOffsetDelta;
+            }
+
+            mLastWallpaperOffsetUpdateTime = System.currentTimeMillis();
+            return true;
+        }
+
+        public float getCurrX() {
+            return mHorizontalWallpaperOffset;
+        }
+
+        public float getFinalX() {
+            return mFinalHorizontalWallpaperOffset;
+        }
+
+        public float getCurrY() {
+            return mVerticalWallpaperOffset;
+        }
+
+        public float getFinalY() {
+            return mFinalVerticalWallpaperOffset;
+        }
+
+        public void setFinalX(float x) {
+            mFinalHorizontalWallpaperOffset = Math.max(0f, Math.min(x, 1.0f));
+        }
+
+        public void setFinalY(float y) {
+            mFinalVerticalWallpaperOffset = Math.max(0f, Math.min(y, 1.0f));
+        }
+
+        public void jumpToFinal() {
+            mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset;
+            mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset;
+        }
+    }
+
+    @Override
+    public void computeScroll() {
+        super.computeScroll();
+        syncWallpaperOffsetWithScroll();
+    }
+
+    void showOutlines() {
+        if (!isSmall() && !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 (!isSmall() && !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;
+    }
+
+    void disableBackground() {
+        mDrawBackground = false;
+    }
+    void enableBackground() {
+        mDrawBackground = true;
+    }
+
+    private void animateBackgroundGradient(float finalAlpha, boolean animated) {
+        if (mBackground == null) return;
+        if (mBackgroundFadeInAnimation != null) {
+            mBackgroundFadeInAnimation.cancel();
+            mBackgroundFadeInAnimation = null;
+        }
+        if (mBackgroundFadeOutAnimation != null) {
+            mBackgroundFadeOutAnimation.cancel();
+            mBackgroundFadeOutAnimation = null;
+        }
+        float startAlpha = getBackgroundAlpha();
+        if (finalAlpha != startAlpha) {
+            if (animated) {
+                mBackgroundFadeOutAnimation =
+                        LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha);
+                mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() {
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        setBackgroundAlpha(((Float) animation.getAnimatedValue()).floatValue());
+                    }
+                });
+                mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f));
+                mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION);
+                mBackgroundFadeOutAnimation.start();
+            } else {
+                setBackgroundAlpha(finalAlpha);
+            }
+        }
+    }
+
+    public void setBackgroundAlpha(float alpha) {
+        if (alpha != mBackgroundAlpha) {
+            mBackgroundAlpha = alpha;
+            invalidate();
+        }
+    }
+
+    public float getBackgroundAlpha() {
+        return mBackgroundAlpha;
+    }
+
+    float backgroundAlphaInterpolator(float r) {
+        float pivotA = 0.1f;
+        float pivotB = 0.4f;
+        if (r < pivotA) {
+            return 0;
+        } else if (r > pivotB) {
+            return 1.0f;
+        } else {
+            return (r - pivotA)/(pivotB - pivotA);
+        }
+    }
+
+    private void updatePageAlphaValues(int screenCenter) {
+        boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX;
+        if (mWorkspaceFadeInAdjacentScreens &&
+                mState == State.NORMAL &&
+                !mIsSwitchingState &&
+                !isInOverscroll) {
+            for (int i = 0; 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);
+                    if (!mIsDragOccuring) {
+                        child.setBackgroundAlphaMultiplier(
+                                backgroundAlphaInterpolator(Math.abs(scrollProgress)));
+                    } else {
+                        child.setBackgroundAlphaMultiplier(1f);
+                    }
+                }
+            }
+        }
+    }
+
+    private void setChildrenBackgroundAlphaMultipliers(float a) {
+        for (int i = 0; i < getChildCount(); i++) {
+            CellLayout child = (CellLayout) getChildAt(i);
+            child.setBackgroundAlphaMultiplier(a);
+        }
+    }
+
+    @Override
+    protected void screenScrolled(int screenCenter) {
+        final boolean isRtl = isLayoutRtl();
+        super.screenScrolled(screenCenter);
+
+        updatePageAlphaValues(screenCenter);
+        enableHwLayersOnVisiblePages();
+
+        if (mOverScrollX < 0 || mOverScrollX > mMaxScrollX) {
+            int index = 0;
+            float pivotX = 0f;
+            final float leftBiasedPivot = 0.25f;
+            final float rightBiasedPivot = 0.75f;
+            final int lowerIndex = 0;
+            final int upperIndex = getChildCount() - 1;
+            if (isRtl) {
+                index = mOverScrollX < 0 ? upperIndex : lowerIndex;
+                pivotX = (index == 0 ? leftBiasedPivot : rightBiasedPivot);
+            } else {
+                index = mOverScrollX < 0 ? lowerIndex : upperIndex;
+                pivotX = (index == 0 ? rightBiasedPivot : leftBiasedPivot);
+            }
+
+            CellLayout cl = (CellLayout) getChildAt(index);
+            float scrollProgress = getScrollProgress(screenCenter, cl, index);
+            final boolean isLeftPage = (isRtl ? index > 0 : index == 0);
+            cl.setOverScrollAmount(Math.abs(scrollProgress), isLeftPage);
+            float rotation = -WORKSPACE_OVERSCROLL_ROTATION * scrollProgress;
+            cl.setRotationY(rotation);
+            setFadeForOverScroll(Math.abs(scrollProgress));
+            if (!mOverscrollTransformsSet) {
+                mOverscrollTransformsSet = true;
+                cl.setCameraDistance(mDensity * mCameraDistance);
+                cl.setPivotX(cl.getMeasuredWidth() * pivotX);
+                cl.setPivotY(cl.getMeasuredHeight() * 0.5f);
+                cl.setOverscrollTransformsDirty(true);
+            }
+        } else {
+            if (mOverscrollFade != 0) {
+                setFadeForOverScroll(0);
+            }
+            if (mOverscrollTransformsSet) {
+                mOverscrollTransformsSet = false;
+                ((CellLayout) getChildAt(0)).resetOverscrollTransforms();
+                ((CellLayout) getChildAt(getChildCount() - 1)).resetOverscrollTransforms();
+            }
+        }
+    }
+
+    @Override
+    protected void overScroll(float amount) {
+        acceleratedOverScroll(amount);
+    }
+
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mWindowToken = getWindowToken();
+        computeScroll();
+        mDragController.setWindowToken(mWindowToken);
+    }
+
+    protected void onDetachedFromWindow() {
+        mWindowToken = null;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
+            mUpdateWallpaperOffsetImmediately = true;
+        }
+        super.onLayout(changed, left, top, right, bottom);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        updateWallpaperOffsets();
+
+        // Draw the background gradient if necessary
+        if (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground) {
+            int alpha = (int) (mBackgroundAlpha * 255);
+            mBackground.setAlpha(alpha);
+            mBackground.setBounds(getScrollX(), 0, getScrollX() + getMeasuredWidth(),
+                    getMeasuredHeight());
+            mBackground.draw(canvas);
+        }
+
+        super.onDraw(canvas);
+
+        // Call back to LauncherModel to finish binding after the first draw
+        post(mBindPages);
+    }
+
+    boolean isDrawingBackgroundGradient() {
+        return (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground);
+    }
+
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+        if (!mLauncher.isAllAppsVisible()) {
+            final Folder openFolder = getOpenFolder();
+            if (openFolder != null) {
+                return openFolder.requestFocus(direction, previouslyFocusedRect);
+            } else {
+                return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getDescendantFocusability() {
+        if (isSmall()) {
+            return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
+        }
+        return super.getDescendantFocusability();
+    }
+
+    @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        if (!mLauncher.isAllAppsVisible()) {
+            final Folder openFolder = getOpenFolder();
+            if (openFolder != null) {
+                openFolder.addFocusables(views, direction);
+            } else {
+                super.addFocusables(views, direction, focusableMode);
+            }
+        }
+    }
+
+    public boolean isSmall() {
+        return mState == State.SMALL || mState == State.SPRING_LOADED;
+    }
+
+    void enableChildrenCache(int fromPage, int toPage) {
+        if (fromPage > toPage) {
+            final int temp = fromPage;
+            fromPage = toPage;
+            toPage = temp;
+        }
+
+        final int screenCount = getChildCount();
+
+        fromPage = Math.max(fromPage, 0);
+        toPage = Math.min(toPage, screenCount - 1);
+
+        for (int i = fromPage; i <= toPage; i++) {
+            final CellLayout layout = (CellLayout) getChildAt(i);
+            layout.setChildrenDrawnWithCacheEnabled(true);
+            layout.setChildrenDrawingCacheEnabled(true);
+        }
+    }
+
+    void clearChildrenCache() {
+        final int screenCount = getChildCount();
+        for (int i = 0; i < screenCount; i++) {
+            final CellLayout layout = (CellLayout) getChildAt(i);
+            layout.setChildrenDrawnWithCacheEnabled(false);
+            // In software mode, we don't want the items to continue to be drawn into bitmaps
+            if (!isHardwareAccelerated()) {
+                layout.setChildrenDrawingCacheEnabled(false);
+            }
+        }
+    }
+
+
+    private void updateChildrenLayersEnabled(boolean force) {
+        boolean small = mState == State.SMALL || mIsSwitchingState;
+        boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving();
+
+        if (enableChildrenLayers != mChildrenLayersEnabled) {
+            mChildrenLayersEnabled = enableChildrenLayers;
+            if (mChildrenLayersEnabled) {
+                enableHwLayersOnVisiblePages();
+            } else {
+                for (int i = 0; i < getPageCount(); i++) {
+                    final CellLayout cl = (CellLayout) getChildAt(i);
+                    cl.disableHardwareLayers();
+                }
+            }
+        }
+    }
+
+    private void enableHwLayersOnVisiblePages() {
+        if (mChildrenLayersEnabled) {
+            final int screenCount = getChildCount();
+            getVisiblePages(mTempVisiblePagesRange);
+            int leftScreen = mTempVisiblePagesRange[0];
+            int rightScreen = mTempVisiblePagesRange[1];
+            if (leftScreen == rightScreen) {
+                // make sure we're caching at least two pages always
+                if (rightScreen < screenCount - 1) {
+                    rightScreen++;
+                } else if (leftScreen > 0) {
+                    leftScreen--;
+                }
+            }
+            for (int i = 0; i < screenCount; i++) {
+                final CellLayout layout = (CellLayout) getPageAt(i);
+                if (!(leftScreen <= i && i <= rightScreen && shouldDrawChild(layout))) {
+                    layout.disableHardwareLayers();
+                }
+            }
+            for (int i = 0; i < screenCount; i++) {
+                final CellLayout layout = (CellLayout) getPageAt(i);
+                if (leftScreen <= i && i <= rightScreen && shouldDrawChild(layout)) {
+                    layout.enableHardwareLayers();
+                }
+            }
+        }
+    }
+
+    public void buildPageHardwareLayers() {
+        // force layers to be enabled just for the call to buildLayer
+        updateChildrenLayersEnabled(true);
+        if (getWindowToken() != null) {
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                CellLayout cl = (CellLayout) getChildAt(i);
+                cl.buildHardwareLayer();
+            }
+        }
+        updateChildrenLayersEnabled(false);
+    }
+
+    protected void onWallpaperTap(MotionEvent ev) {
+        final int[] position = mTempCell;
+        getLocationOnScreen(position);
+
+        int pointerIndex = ev.getActionIndex();
+        position[0] += (int) ev.getX(pointerIndex);
+        position[1] += (int) ev.getY(pointerIndex);
+
+        mWallpaperManager.sendWallpaperCommand(getWindowToken(),
+                ev.getAction() == MotionEvent.ACTION_UP
+                        ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP,
+                position[0], position[1], 0, null);
+    }
+
+    /*
+     * 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
+    *
+    * These methods mark the appropriate pages as accepting drops (which alters their visual
+    * appearance).
+    *
+    */
+    public void onDragStartedWithItem(View v) {
+        final Canvas canvas = new Canvas();
+
+        // The outline is used to visualize where the item will land if dropped
+        mDragOutline = createDragOutline(v, canvas, DRAG_BITMAP_PADDING);
+    }
+
+    public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) {
+        final Canvas canvas = new Canvas();
+
+        int[] size = estimateItemSize(info.spanX, info.spanY, info, false);
+
+        // The outline is used to visualize where the item will land if dropped
+        mDragOutline = createDragOutline(b, canvas, DRAG_BITMAP_PADDING, size[0],
+                size[1], clipAlpha);
+    }
+
+    public void exitWidgetResizeMode() {
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        dragLayer.clearAllResizeFrames();
+    }
+
+    private void initAnimationArrays() {
+        final int childCount = getChildCount();
+        if (mOldTranslationXs != null) return;
+        mOldTranslationXs = new float[childCount];
+        mOldTranslationYs = new float[childCount];
+        mOldScaleXs = new float[childCount];
+        mOldScaleYs = new float[childCount];
+        mOldBackgroundAlphas = new float[childCount];
+        mOldAlphas = new float[childCount];
+        mNewTranslationXs = new float[childCount];
+        mNewTranslationYs = new float[childCount];
+        mNewScaleXs = new float[childCount];
+        mNewScaleYs = new float[childCount];
+        mNewBackgroundAlphas = new float[childCount];
+        mNewAlphas = new float[childCount];
+        mNewRotationYs = new float[childCount];
+    }
+
+    Animator getChangeStateAnimation(final State state, boolean animated) {
+        return getChangeStateAnimation(state, animated, 0);
+    }
+
+    Animator getChangeStateAnimation(final State state, boolean animated, int delay) {
+        if (mState == state) {
+            return null;
+        }
+
+        // Initialize animation arrays for the first time if necessary
+        initAnimationArrays();
+
+        AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null;
+
+        // Stop any scrolling, move to the current page right away
+        setCurrentPage(getNextPage());
+
+        final State oldState = mState;
+        final boolean oldStateIsNormal = (oldState == State.NORMAL);
+        final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED);
+        final boolean oldStateIsSmall = (oldState == State.SMALL);
+        mState = state;
+        final boolean stateIsNormal = (state == State.NORMAL);
+        final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED);
+        final boolean stateIsSmall = (state == State.SMALL);
+        float finalScaleFactor = 1.0f;
+        float finalBackgroundAlpha = stateIsSpringLoaded ? 1.0f : 0f;
+        float translationX = 0;
+        float translationY = 0;
+        boolean zoomIn = true;
+
+        if (state != State.NORMAL) {
+            finalScaleFactor = mSpringLoadedShrinkFactor - (stateIsSmall ? 0.1f : 0);
+            setPageSpacing(mSpringLoadedPageSpacing);
+            if (oldStateIsNormal && stateIsSmall) {
+                zoomIn = false;
+                setLayoutScale(finalScaleFactor);
+                updateChildrenLayersEnabled(false);
+            } else {
+                finalBackgroundAlpha = 1.0f;
+                setLayoutScale(finalScaleFactor);
+            }
+        } else {
+            setPageSpacing(mOriginalPageSpacing);
+            setLayoutScale(1.0f);
+        }
+
+        final int duration = zoomIn ?
+                getResources().getInteger(R.integer.config_workspaceUnshrinkTime) :
+                getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime);
+        for (int i = 0; i < getChildCount(); i++) {
+            final CellLayout cl = (CellLayout) getChildAt(i);
+            float finalAlpha = (!mWorkspaceFadeInAdjacentScreens || stateIsSpringLoaded ||
+                    (i == mCurrentPage)) ? 1f : 0f;
+            float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();
+            float initialAlpha = currentAlpha;
+
+            // Determine the pages alpha during the state transition
+            if ((oldStateIsSmall && stateIsNormal) ||
+                (oldStateIsNormal && stateIsSmall)) {
+                // To/from workspace - only show the current page unless the transition is not
+                //                     animated and the animation end callback below doesn't run;
+                //                     or, if we're in spring-loaded mode
+                if (i == mCurrentPage || !animated || oldStateIsSpringLoaded) {
+                    finalAlpha = 1f;
+                } else {
+                    initialAlpha = 0f;
+                    finalAlpha = 0f;
+                }
+            }
+
+            mOldAlphas[i] = initialAlpha;
+            mNewAlphas[i] = finalAlpha;
+            if (animated) {
+                mOldTranslationXs[i] = cl.getTranslationX();
+                mOldTranslationYs[i] = cl.getTranslationY();
+                mOldScaleXs[i] = cl.getScaleX();
+                mOldScaleYs[i] = cl.getScaleY();
+                mOldBackgroundAlphas[i] = cl.getBackgroundAlpha();
+
+                mNewTranslationXs[i] = translationX;
+                mNewTranslationYs[i] = translationY;
+                mNewScaleXs[i] = finalScaleFactor;
+                mNewScaleYs[i] = finalScaleFactor;
+                mNewBackgroundAlphas[i] = finalBackgroundAlpha;
+            } else {
+                cl.setTranslationX(translationX);
+                cl.setTranslationY(translationY);
+                cl.setScaleX(finalScaleFactor);
+                cl.setScaleY(finalScaleFactor);
+                cl.setBackgroundAlpha(finalBackgroundAlpha);
+                cl.setShortcutAndWidgetAlpha(finalAlpha);
+            }
+        }
+
+        if (animated) {
+            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.setTranslationX(mNewTranslationXs[i]);
+                    cl.setTranslationY(mNewTranslationYs[i]);
+                    cl.setScaleX(mNewScaleXs[i]);
+                    cl.setScaleY(mNewScaleYs[i]);
+                    cl.setBackgroundAlpha(mNewBackgroundAlphas[i]);
+                    cl.setShortcutAndWidgetAlpha(mNewAlphas[i]);
+                    cl.setRotationY(mNewRotationYs[i]);
+                } else {
+                    LauncherViewPropertyAnimator a = new LauncherViewPropertyAnimator(cl);
+                    a.translationX(mNewTranslationXs[i])
+                        .translationY(mNewTranslationYs[i])
+                        .scaleX(mNewScaleXs[i])
+                        .scaleY(mNewScaleYs[i])
+                        .setDuration(duration)
+                        .setInterpolator(mZoomInInterpolator);
+                    anim.play(a);
+
+                    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).setDuration(duration);
+                        bgAnim.setInterpolator(mZoomInInterpolator);
+                        bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() {
+                                public void onAnimationUpdate(float a, float b) {
+                                    cl.setBackgroundAlpha(
+                                            a * mOldBackgroundAlphas[i] +
+                                            b * mNewBackgroundAlphas[i]);
+                                }
+                            });
+                        anim.play(bgAnim);
+                    }
+                }
+            }
+            anim.setStartDelay(delay);
+        }
+
+        if (stateIsSpringLoaded) {
+            // Right now we're covered by Apps Customize
+            // Show the background gradient immediately, so the gradient will
+            // be showing once AppsCustomize disappears
+            animateBackgroundGradient(getResources().getInteger(
+                    R.integer.config_appsCustomizeSpringLoadedBgAlpha) / 100f, false);
+        } else {
+            // Fade the background gradient away
+            animateBackgroundGradient(0f, true);
+        }
+        return anim;
+    }
+
+    @Override
+    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+        mIsSwitchingState = true;
+        updateChildrenLayersEnabled(false);
+        cancelScrollingIndicatorAnimations();
+    }
+
+    @Override
+    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
+    }
+
+    @Override
+    public void onLauncherTransitionStep(Launcher l, float t) {
+        mTransitionProgress = t;
+    }
+
+    @Override
+    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
+        mIsSwitchingState = false;
+        mWallpaperOffset.setOverrideHorizontalCatchupConstant(false);
+        updateChildrenLayersEnabled(false);
+        // The code in getChangeStateAnimation to determine initialAlpha and finalAlpha will ensure
+        // ensure that only the current page is visible during (and subsequently, after) the
+        // transition animation.  If fade adjacent pages is disabled, then re-enable the page
+        // visibility after the transition animation.
+        if (!mWorkspaceFadeInAdjacentScreens) {
+            for (int i = 0; i < getChildCount(); i++) {
+                final CellLayout cl = (CellLayout) getChildAt(i);
+                cl.setShortcutAndWidgetAlpha(1f);
+            }
+        }
+    }
+
+    @Override
+    public View getContent() {
+        return this;
+    }
+
+    /**
+     * Draw the View v into the given Canvas.
+     *
+     * @param v the view to draw
+     * @param destCanvas the canvas to draw on
+     * @param padding the horizontal and vertical padding to use when drawing
+     */
+    private void drawDragView(View v, Canvas destCanvas, int padding, boolean pruneToDrawable) {
+        final Rect clipRect = mTempRect;
+        v.getDrawingRect(clipRect);
+
+        boolean textVisible = false;
+
+        destCanvas.save();
+        if (v instanceof TextView && pruneToDrawable) {
+            Drawable d = ((TextView) v).getCompoundDrawables()[1];
+            clipRect.set(0, 0, d.getIntrinsicWidth() + padding, d.getIntrinsicHeight() + padding);
+            destCanvas.translate(padding / 2, padding / 2);
+            d.draw(destCanvas);
+        } else {
+            if (v instanceof FolderIcon) {
+                // For FolderIcons the text can bleed into the icon area, and so we need to
+                // hide the text completely (which can't be achieved by clipping).
+                if (((FolderIcon) v).getTextVisible()) {
+                    ((FolderIcon) v).setTextVisible(false);
+                    textVisible = true;
+                }
+            } else if (v instanceof BubbleTextView) {
+                final BubbleTextView tv = (BubbleTextView) v;
+                clipRect.bottom = tv.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V +
+                        tv.getLayout().getLineTop(0);
+            } else if (v instanceof TextView) {
+                final TextView tv = (TextView) v;
+                clipRect.bottom = tv.getExtendedPaddingTop() - tv.getCompoundDrawablePadding() +
+                        tv.getLayout().getLineTop(0);
+            }
+            destCanvas.translate(-v.getScrollX() + padding / 2, -v.getScrollY() + padding / 2);
+            destCanvas.clipRect(clipRect, Op.REPLACE);
+            v.draw(destCanvas);
+
+            // Restore text visibility of FolderIcon if necessary
+            if (textVisible) {
+                ((FolderIcon) v).setTextVisible(true);
+            }
+        }
+        destCanvas.restore();
+    }
+
+    /**
+     * Returns a new bitmap to show when the given View is being dragged around.
+     * Responsibility for the bitmap is transferred to the caller.
+     */
+    public Bitmap createDragBitmap(View v, Canvas canvas, int padding) {
+        Bitmap b;
+
+        if (v instanceof TextView) {
+            Drawable d = ((TextView) v).getCompoundDrawables()[1];
+            b = Bitmap.createBitmap(d.getIntrinsicWidth() + padding,
+                    d.getIntrinsicHeight() + padding, Bitmap.Config.ARGB_8888);
+        } else {
+            b = Bitmap.createBitmap(
+                    v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888);
+        }
+
+        canvas.setBitmap(b);
+        drawDragView(v, canvas, padding, true);
+        canvas.setBitmap(null);
+
+        return b;
+    }
+
+    /**
+     * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location.
+     * Responsibility for the bitmap is transferred to the caller.
+     */
+    private Bitmap createDragOutline(View v, Canvas canvas, int padding) {
+        final int outlineColor = getResources().getColor(android.R.color.holo_blue_light);
+        final Bitmap b = Bitmap.createBitmap(
+                v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888);
+
+        canvas.setBitmap(b);
+        drawDragView(v, canvas, padding, true);
+        mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor);
+        canvas.setBitmap(null);
+        return b;
+    }
+
+    /**
+     * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location.
+     * Responsibility for the bitmap is transferred to the caller.
+     */
+    private Bitmap createDragOutline(Bitmap orig, Canvas canvas, int padding, int w, int h,
+            boolean clipAlpha) {
+        final int outlineColor = getResources().getColor(android.R.color.holo_blue_light);
+        final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+        canvas.setBitmap(b);
+
+        Rect src = new Rect(0, 0, orig.getWidth(), orig.getHeight());
+        float scaleFactor = Math.min((w - padding) / (float) orig.getWidth(),
+                (h - padding) / (float) orig.getHeight());
+        int scaledWidth = (int) (scaleFactor * orig.getWidth());
+        int scaledHeight = (int) (scaleFactor * orig.getHeight());
+        Rect dst = new Rect(0, 0, scaledWidth, scaledHeight);
+
+        // center the image
+        dst.offset((w - scaledWidth) / 2, (h - scaledHeight) / 2);
+
+        canvas.drawBitmap(orig, src, dst, null);
+        mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor,
+                clipAlpha);
+        canvas.setBitmap(null);
+
+        return b;
+    }
+
+    void startDrag(CellLayout.CellInfo cellInfo) {
+        View child = cellInfo.cell;
+
+        // Make sure the drag was started by a long press as opposed to a long click.
+        if (!child.isInTouchMode()) {
+            return;
+        }
+
+        mDragInfo = cellInfo;
+        child.setVisibility(INVISIBLE);
+        CellLayout layout = (CellLayout) child.getParent().getParent();
+        layout.prepareChildForDrag(child);
+
+        child.clearFocus();
+        child.setPressed(false);
+
+        final Canvas canvas = new Canvas();
+
+        // The outline is used to visualize where the item will land if dropped
+        mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);
+        beginDragShared(child, this);
+    }
+
+    public void beginDragShared(View child, DragSource source) {
+        Resources r = getResources();
+
+        // The drag bitmap follows the touch point around on the screen
+        final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING);
+
+        final int bmpWidth = b.getWidth();
+        final int bmpHeight = b.getHeight();
+
+        float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);
+        int dragLayerX =
+                Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2);
+        int dragLayerY =
+                Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2
+                        - DRAG_BITMAP_PADDING / 2);
+
+        Point dragVisualizeOffset = null;
+        Rect dragRect = null;
+        if (child instanceof BubbleTextView || child instanceof PagedViewIcon) {
+            int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size);
+            int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top);
+            int top = child.getPaddingTop();
+            int left = (bmpWidth - iconSize) / 2;
+            int right = left + iconSize;
+            int bottom = top + iconSize;
+            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.
+            dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2,
+                    iconPaddingTop - DRAG_BITMAP_PADDING / 2);
+            dragRect = new Rect(left, top, right, bottom);
+        } else if (child instanceof FolderIcon) {
+            int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size);
+            dragRect = new Rect(0, 0, child.getWidth(), previewSize);
+        }
+
+        // Clear the pressed state if necessary
+        if (child instanceof BubbleTextView) {
+            BubbleTextView icon = (BubbleTextView) child;
+            icon.clearPressedOrFocusedBackground();
+        }
+
+        mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
+                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
+        b.recycle();
+
+        // Show the scrolling indicator when you pick up an item
+        showScrollingIndicator(false);
+    }
+
+    void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, int screen,
+            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, screen, cellXY[0], cellXY[1], 1, 1, insertAtFirst);
+        LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, cellXY[0],
+                cellXY[1]);
+    }
+
+    public boolean transitionStateShouldAllowDrop() {
+        return ((!isSwitchingState() || mTransitionProgress > 0.5f) && mState != State.SMALL);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean acceptDrop(DragObject d) {
+        // If it's an external drop (e.g. from All Apps), check if it should be accepted
+        CellLayout dropTargetLayout = mDropToLayout;
+        if (d.dragSource != this) {
+            // Don't accept the drop if we're not over a screen at time of drop
+            if (dropTargetLayout == null) {
+                return false;
+            }
+            if (!transitionStateShouldAllowDrop()) return false;
+
+            mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
+                    d.dragView, mDragViewVisualCenter);
+
+            // We want the point to be mapped to the dragTarget.
+            if (mLauncher.isHotseatLayout(dropTargetLayout)) {
+                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
+            } else {
+                mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null);
+            }
+
+            int spanX = 1;
+            int spanY = 1;
+            if (mDragInfo != null) {
+                final CellLayout.CellInfo dragCellInfo = mDragInfo;
+                spanX = dragCellInfo.spanX;
+                spanY = dragCellInfo.spanY;
+            } else {
+                final ItemInfo dragInfo = (ItemInfo) d.dragInfo;
+                spanX = dragInfo.spanX;
+                spanY = dragInfo.spanY;
+            }
+
+            int minSpanX = spanX;
+            int minSpanY = spanY;
+            if (d.dragInfo instanceof PendingAddWidgetInfo) {
+                minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
+                minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
+            }
+
+            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
+                    mTargetCell);
+            float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
+                    mDragViewVisualCenter[1], mTargetCell);
+            if (willCreateUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
+                    mTargetCell, distance, true)) {
+                return true;
+            }
+            if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
+                    mTargetCell, distance)) {
+                return true;
+            }
+
+            int[] resultSpan = new int[2];
+            mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
+                    null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
+            boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
+
+            // Don't accept the drop if there's no room for the item
+            if (!foundCell) {
+                // Don't show the message if we are dropping on the AllApps button and the hotseat
+                // is full
+                boolean isHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
+                if (mTargetCell != null && isHotseat) {
+                    Hotseat hotseat = mLauncher.getHotseat();
+                    if (hotseat.isAllAppsButtonRank(
+                            hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) {
+                        return false;
+                    }
+                }
+
+                mLauncher.showOutOfSpaceMessage(isHotseat);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float
+            distance, boolean considerTimeout) {
+        if (distance > mMaxDistanceForFolderCreation) return false;
+        View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
+
+        if (dropOverView != null) {
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
+            if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) {
+                return false;
+            }
+        }
+
+        boolean hasntMoved = false;
+        if (mDragInfo != null) {
+            hasntMoved = dropOverView == mDragInfo.cell;
+        }
+
+        if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) {
+            return false;
+        }
+
+        boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo);
+        boolean willBecomeShortcut =
+                (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
+                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
+
+        return (aboveShortcut && willBecomeShortcut);
+    }
+
+    boolean willAddToExistingUserFolder(Object dragInfo, CellLayout target, int[] targetCell,
+            float distance) {
+        if (distance > mMaxDistanceForFolderCreation) return false;
+        View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
+
+        if (dropOverView != null) {
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
+            if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) {
+                return false;
+            }
+        }
+
+        if (dropOverView instanceof FolderIcon) {
+            FolderIcon fi = (FolderIcon) dropOverView;
+            if (fi.acceptDrop(dragInfo)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    boolean createUserFolderIfNecessary(View newView, long container, CellLayout target,
+            int[] targetCell, float distance, boolean external, DragView dragView,
+            Runnable postAnimationRunnable) {
+        if (distance > mMaxDistanceForFolderCreation) return false;
+        View v = target.getChildAt(targetCell[0], targetCell[1]);
+
+        boolean hasntMoved = false;
+        if (mDragInfo != null) {
+            CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell);
+            hasntMoved = (mDragInfo.cellX == targetCell[0] &&
+                    mDragInfo.cellY == targetCell[1]) && (cellParent == target);
+        }
+
+        if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false;
+        mCreateUserFolderOnDrop = false;
+        final int screen = (targetCell == null) ? mDragInfo.screen : indexOfChild(target);
+
+        boolean aboveShortcut = (v.getTag() instanceof ShortcutInfo);
+        boolean willBecomeShortcut = (newView.getTag() instanceof ShortcutInfo);
+
+        if (aboveShortcut && willBecomeShortcut) {
+            ShortcutInfo sourceInfo = (ShortcutInfo) newView.getTag();
+            ShortcutInfo destInfo = (ShortcutInfo) v.getTag();
+            // if the drag started here, we need to remove it from the workspace
+            if (!external) {
+                getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
+            }
+
+            Rect folderLocation = new Rect();
+            float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
+            target.removeView(v);
+
+            FolderIcon fi =
+                mLauncher.addFolder(target, container, screen, targetCell[0], targetCell[1]);
+            destInfo.cellX = -1;
+            destInfo.cellY = -1;
+            sourceInfo.cellX = -1;
+            sourceInfo.cellY = -1;
+
+            // If the dragView is null, we can't animate
+            boolean animate = dragView != null;
+            if (animate) {
+                fi.performCreateAnimation(destInfo, v, sourceInfo, dragView, folderLocation, scale,
+                        postAnimationRunnable);
+            } else {
+                fi.addItem(destInfo);
+                fi.addItem(sourceInfo);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell,
+            float distance, DragObject d, boolean external) {
+        if (distance > mMaxDistanceForFolderCreation) return false;
+
+        View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
+        if (!mAddToExistingFolderOnDrop) return false;
+        mAddToExistingFolderOnDrop = false;
+
+        if (dropOverView instanceof FolderIcon) {
+            FolderIcon fi = (FolderIcon) dropOverView;
+            if (fi.acceptDrop(d.dragInfo)) {
+                fi.onDrop(d);
+
+                // if the drag started here, we need to remove it from the workspace
+                if (!external) {
+                    getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void onDrop(final DragObject d) {
+        mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView,
+                mDragViewVisualCenter);
+
+        CellLayout dropTargetLayout = mDropToLayout;
+
+        // We want the point to be mapped to the dragTarget.
+        if (dropTargetLayout != null) {
+            if (mLauncher.isHotseatLayout(dropTargetLayout)) {
+                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
+            } else {
+                mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null);
+            }
+        }
+
+        int snapScreen = -1;
+        boolean resizeOnDrop = false;
+        if (d.dragSource != this) {
+            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1] };
+            onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
+        } else if (mDragInfo != null) {
+            final View cell = mDragInfo.cell;
+
+            Runnable resizeRunnable = null;
+            if (dropTargetLayout != null) {
+                // Move internally
+                boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
+                boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
+                long container = hasMovedIntoHotseat ?
+                        LauncherSettings.Favorites.CONTAINER_HOTSEAT :
+                        LauncherSettings.Favorites.CONTAINER_DESKTOP;
+                int screen = (mTargetCell[0] < 0) ?
+                        mDragInfo.screen : indexOfChild(dropTargetLayout);
+                int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
+                int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
+                // First we find the cell nearest to point at which the item is
+                // dropped, without any consideration to whether there is an item there.
+
+                mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
+                        mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
+                float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
+                        mDragViewVisualCenter[1], mTargetCell);
+
+                // If the item being dropped is a shortcut and the nearest drop
+                // cell also contains a shortcut, then create a folder with the two shortcuts.
+                if (!mInScrollArea && createUserFolderIfNecessary(cell, container,
+                        dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) {
+                    return;
+                }
+
+                if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
+                        distance, d, false)) {
+                    return;
+                }
+
+                // Aside from the special case where we're dropping a shortcut onto a shortcut,
+                // we need to find the nearest cell location that is vacant
+                ItemInfo item = (ItemInfo) d.dragInfo;
+                int minSpanX = item.spanX;
+                int minSpanY = item.spanY;
+                if (item.minSpanX > 0 && item.minSpanY > 0) {
+                    minSpanX = item.minSpanX;
+                    minSpanY = item.minSpanY;
+                }
+
+                int[] resultSpan = new int[2];
+                mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0],
+                        (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
+                        mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
+
+                boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
+
+                // if the widget resizes on drop
+                if (foundCell && (cell instanceof AppWidgetHostView) &&
+                        (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
+                    resizeOnDrop = true;
+                    item.spanX = resultSpan[0];
+                    item.spanY = resultSpan[1];
+                    AppWidgetHostView awhv = (AppWidgetHostView) cell;
+                    AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
+                            resultSpan[1]);
+                }
+
+                if (mCurrentPage != screen && !hasMovedIntoHotseat) {
+                    snapScreen = screen;
+                    snapToPage(screen);
+                }
+
+                if (foundCell) {
+                    final ItemInfo info = (ItemInfo) cell.getTag();
+                    if (hasMovedLayouts) {
+                        // Reparent the view
+                        getParentCellLayoutForView(cell).removeView(cell);
+                        addInScreen(cell, container, screen, mTargetCell[0], mTargetCell[1],
+                                info.spanX, info.spanY);
+                    }
+
+                    // update the item's position after drop
+                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
+                    lp.cellX = lp.tmpCellX = mTargetCell[0];
+                    lp.cellY = lp.tmpCellY = mTargetCell[1];
+                    lp.cellHSpan = item.spanX;
+                    lp.cellVSpan = item.spanY;
+                    lp.isLockedToGrid = true;
+                    cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen,
+                            mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY));
+
+                    if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
+                            cell instanceof LauncherAppWidgetHostView) {
+                        final CellLayout cellLayout = dropTargetLayout;
+                        // We post this call so that the widget has a chance to be placed
+                        // in its final location
+
+                        final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
+                        AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo();
+                        if (pinfo != null &&
+                                pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
+                            final Runnable addResizeFrame = new Runnable() {
+                                public void run() {
+                                    DragLayer dragLayer = mLauncher.getDragLayer();
+                                    dragLayer.addResizeFrame(info, hostView, cellLayout);
+                                }
+                            };
+                            resizeRunnable = (new Runnable() {
+                                public void run() {
+                                    if (!isPageMoving()) {
+                                        addResizeFrame.run();
+                                    } else {
+                                        mDelayedResizeRunnable = addResizeFrame;
+                                    }
+                                }
+                            });
+                        }
+                    }
+
+                    LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX,
+                            lp.cellY);
+                } else {
+                    // If we can't find a drop location, we return the item to its original position
+                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
+                    mTargetCell[0] = lp.cellX;
+                    mTargetCell[1] = lp.cellY;
+                    CellLayout layout = (CellLayout) cell.getParent().getParent();
+                    layout.markCellsAsOccupiedForView(cell);
+                }
+            }
+
+            final CellLayout parent = (CellLayout) cell.getParent().getParent();
+            final Runnable finalResizeRunnable = resizeRunnable;
+            // Prepare it to be animated into its new position
+            // This must be called after the view has been re-parented
+            final Runnable onCompleteRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    mAnimatingViewIntoPlace = false;
+                    updateChildrenLayersEnabled(false);
+                    if (finalResizeRunnable != null) {
+                        finalResizeRunnable.run();
+                    }
+                }
+            };
+            mAnimatingViewIntoPlace = true;
+            if (d.dragView.hasDrawn()) {
+                final ItemInfo info = (ItemInfo) cell.getTag();
+                if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
+                    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;
+                    mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
+                            onCompleteRunnable, this);
+                }
+            } else {
+                d.deferDragViewCleanupPostAnimation = false;
+                cell.setVisibility(VISIBLE);
+            }
+            parent.onDropChild(cell);
+        }
+    }
+
+    public void setFinalScrollForPageChange(int screen) {
+        if (screen >= 0) {
+            mSavedScrollX = getScrollX();
+            CellLayout cl = (CellLayout) getChildAt(screen);
+            mSavedTranslationX = cl.getTranslationX();
+            mSavedRotationY = cl.getRotationY();
+            final int newX = getChildOffset(screen) - getRelativeChildOffset(screen);
+            setScrollX(newX);
+            cl.setTranslationX(0f);
+            cl.setRotationY(0f);
+        }
+    }
+
+    public void resetFinalScrollForPageChange(int screen) {
+        if (screen >= 0) {
+            CellLayout cl = (CellLayout) getChildAt(screen);
+            setScrollX(mSavedScrollX);
+            cl.setTranslationX(mSavedTranslationX);
+            cl.setRotationY(mSavedRotationY);
+        }
+    }
+
+    public void getViewLocationRelativeToSelf(View v, int[] location) {
+        getLocationInWindow(location);
+        int x = location[0];
+        int y = location[1];
+
+        v.getLocationInWindow(location);
+        int vX = location[0];
+        int vY = location[1];
+
+        location[0] = vX - x;
+        location[1] = vY - y;
+    }
+
+    public void onDragEnter(DragObject d) {
+        mDragEnforcer.onDragEnter();
+        mCreateUserFolderOnDrop = false;
+        mAddToExistingFolderOnDrop = false;
+
+        mDropToLayout = null;
+        CellLayout layout = getCurrentDropLayout();
+        setCurrentDropLayout(layout);
+        setCurrentDragOverlappingLayout(layout);
+
+        // Because we don't have space in the Phone UI (the CellLayouts run to the edge) we
+        // don't need to show the outlines
+        if (LauncherApplication.isScreenLarge()) {
+            showOutlines();
+        }
+    }
+
+    static Rect getCellLayoutMetrics(Launcher launcher, int orientation) {
+        Resources res = launcher.getResources();
+        Display display = launcher.getWindowManager().getDefaultDisplay();
+        Point smallestSize = new Point();
+        Point largestSize = new Point();
+        display.getCurrentSizeRange(smallestSize, largestSize);
+        if (orientation == CellLayout.LANDSCAPE) {
+            if (mLandscapeCellLayoutMetrics == null) {
+                int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land);
+                int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land);
+                int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land);
+                int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land);
+                int width = largestSize.x - paddingLeft - paddingRight;
+                int height = smallestSize.y - paddingTop - paddingBottom;
+                mLandscapeCellLayoutMetrics = new Rect();
+                CellLayout.getMetrics(mLandscapeCellLayoutMetrics, res,
+                        width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(),
+                        orientation);
+            }
+            return mLandscapeCellLayoutMetrics;
+        } else if (orientation == CellLayout.PORTRAIT) {
+            if (mPortraitCellLayoutMetrics == null) {
+                int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land);
+                int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land);
+                int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land);
+                int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land);
+                int width = smallestSize.x - paddingLeft - paddingRight;
+                int height = largestSize.y - paddingTop - paddingBottom;
+                mPortraitCellLayoutMetrics = new Rect();
+                CellLayout.getMetrics(mPortraitCellLayoutMetrics, res,
+                        width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(),
+                        orientation);
+            }
+            return mPortraitCellLayoutMetrics;
+        }
+        return null;
+    }
+
+    public void onDragExit(DragObject d) {
+        mDragEnforcer.onDragExit();
+
+        // Here we store the final page that will be dropped to, if the workspace in fact
+        // receives the drop
+        if (mInScrollArea) {
+            if (isPageMoving()) {
+                // If the user drops while the page is scrolling, we should use that page as the
+                // destination instead of the page that is being hovered over.
+                mDropToLayout = (CellLayout) getPageAt(getNextPage());
+            } else {
+                mDropToLayout = mDragOverlappingLayout;
+            }
+        } else {
+            mDropToLayout = mDragTargetLayout;
+        }
+
+        if (mDragMode == DRAG_MODE_CREATE_FOLDER) {
+            mCreateUserFolderOnDrop = true;
+        } else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) {
+            mAddToExistingFolderOnDrop = true;
+        }
+
+        // Reset the scroll area and previous drag target
+        onResetScrollArea();
+        setCurrentDropLayout(null);
+        setCurrentDragOverlappingLayout(null);
+
+        mSpringLoadedDragController.cancel();
+
+        if (!mIsPageMoving) {
+            hideOutlines();
+        }
+    }
+
+    void setCurrentDropLayout(CellLayout layout) {
+        if (mDragTargetLayout != null) {
+            mDragTargetLayout.revertTempState();
+            mDragTargetLayout.onDragExit();
+        }
+        mDragTargetLayout = layout;
+        if (mDragTargetLayout != null) {
+            mDragTargetLayout.onDragEnter();
+        }
+        cleanupReorder(true);
+        cleanupFolderCreation();
+        setCurrentDropOverCell(-1, -1);
+    }
+
+    void setCurrentDragOverlappingLayout(CellLayout layout) {
+        if (mDragOverlappingLayout != null) {
+            mDragOverlappingLayout.setIsDragOverlapping(false);
+        }
+        mDragOverlappingLayout = layout;
+        if (mDragOverlappingLayout != null) {
+            mDragOverlappingLayout.setIsDragOverlapping(true);
+        }
+        invalidate();
+    }
+
+    void setCurrentDropOverCell(int x, int y) {
+        if (x != mDragOverX || y != mDragOverY) {
+            mDragOverX = x;
+            mDragOverY = y;
+            setDragMode(DRAG_MODE_NONE);
+        }
+    }
+
+    void setDragMode(int dragMode) {
+        if (dragMode != mDragMode) {
+            if (dragMode == DRAG_MODE_NONE) {
+                cleanupAddToFolder();
+                // We don't want to cancel the re-order alarm every time the target cell changes
+                // as this feels to slow / unresponsive.
+                cleanupReorder(false);
+                cleanupFolderCreation();
+            } else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) {
+                cleanupReorder(true);
+                cleanupFolderCreation();
+            } else if (dragMode == DRAG_MODE_CREATE_FOLDER) {
+                cleanupAddToFolder();
+                cleanupReorder(true);
+            } else if (dragMode == DRAG_MODE_REORDER) {
+                cleanupAddToFolder();
+                cleanupFolderCreation();
+            }
+            mDragMode = dragMode;
+        }
+    }
+
+    private void cleanupFolderCreation() {
+        if (mDragFolderRingAnimator != null) {
+            mDragFolderRingAnimator.animateToNaturalState();
+        }
+        mFolderCreationAlarm.cancelAlarm();
+    }
+
+    private void cleanupAddToFolder() {
+        if (mDragOverFolderIcon != null) {
+            mDragOverFolderIcon.onDragExit(null);
+            mDragOverFolderIcon = null;
+        }
+    }
+
+    private void cleanupReorder(boolean cancelAlarm) {
+        // Any pending reorders are canceled
+        if (cancelAlarm) {
+            mReorderAlarm.cancelAlarm();
+        }
+        mLastReorderX = -1;
+        mLastReorderY = -1;
+    }
+
+    public DropTarget getDropTargetDelegate(DragObject d) {
+        return null;
+    }
+
+    /*
+    *
+    * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
+    * coordinate space. The argument xy is modified with the return result.
+    *
+    */
+   void mapPointFromSelfToChild(View v, float[] xy) {
+       mapPointFromSelfToChild(v, xy, null);
+   }
+
+   /*
+    *
+    * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
+    * coordinate space. The argument xy is modified with the return result.
+    *
+    * if cachedInverseMatrix is not null, this method will just use that matrix instead of
+    * computing it itself; we use this to avoid redundant matrix inversions in
+    * findMatchingPageForDragOver
+    *
+    */
+   void mapPointFromSelfToChild(View v, float[] xy, Matrix cachedInverseMatrix) {
+       if (cachedInverseMatrix == null) {
+           v.getMatrix().invert(mTempInverseMatrix);
+           cachedInverseMatrix = mTempInverseMatrix;
+       }
+       int scrollX = getScrollX();
+       if (mNextPage != INVALID_PAGE) {
+           scrollX = mScroller.getFinalX();
+       }
+       xy[0] = xy[0] + scrollX - v.getLeft();
+       xy[1] = xy[1] + getScrollY() - v.getTop();
+       cachedInverseMatrix.mapPoints(xy);
+   }
+
+
+   void mapPointFromSelfToHotseatLayout(Hotseat hotseat, float[] xy) {
+       hotseat.getLayout().getMatrix().invert(mTempInverseMatrix);
+       xy[0] = xy[0] - hotseat.getLeft() - hotseat.getLayout().getLeft();
+       xy[1] = xy[1] - hotseat.getTop() - hotseat.getLayout().getTop();
+       mTempInverseMatrix.mapPoints(xy);
+   }
+
+   /*
+    *
+    * Convert the 2D coordinate xy from this CellLayout's coordinate space to
+    * the parent View's coordinate space. The argument xy is modified with the return result.
+    *
+    */
+   void mapPointFromChildToSelf(View v, float[] xy) {
+       v.getMatrix().mapPoints(xy);
+       int scrollX = getScrollX();
+       if (mNextPage != INVALID_PAGE) {
+           scrollX = mScroller.getFinalX();
+       }
+       xy[0] -= (scrollX - v.getLeft());
+       xy[1] -= (getScrollY() - v.getTop());
+   }
+
+   static private float squaredDistance(float[] point1, float[] point2) {
+        float distanceX = point1[0] - point2[0];
+        float distanceY = point2[1] - point2[1];
+        return distanceX * distanceX + distanceY * distanceY;
+   }
+
+    /*
+     *
+     * Returns true if the passed CellLayout cl overlaps with dragView
+     *
+     */
+    boolean overlaps(CellLayout cl, DragView dragView,
+            int dragViewX, int dragViewY, Matrix cachedInverseMatrix) {
+        // Transform the coordinates of the item being dragged to the CellLayout's coordinates
+        final float[] draggedItemTopLeft = mTempDragCoordinates;
+        draggedItemTopLeft[0] = dragViewX;
+        draggedItemTopLeft[1] = dragViewY;
+        final float[] draggedItemBottomRight = mTempDragBottomRightCoordinates;
+        draggedItemBottomRight[0] = draggedItemTopLeft[0] + dragView.getDragRegionWidth();
+        draggedItemBottomRight[1] = draggedItemTopLeft[1] + dragView.getDragRegionHeight();
+
+        // Transform the dragged item's top left coordinates
+        // to the CellLayout's local coordinates
+        mapPointFromSelfToChild(cl, draggedItemTopLeft, cachedInverseMatrix);
+        float overlapRegionLeft = Math.max(0f, draggedItemTopLeft[0]);
+        float overlapRegionTop = Math.max(0f, draggedItemTopLeft[1]);
+
+        if (overlapRegionLeft <= cl.getWidth() && overlapRegionTop >= 0) {
+            // Transform the dragged item's bottom right coordinates
+            // to the CellLayout's local coordinates
+            mapPointFromSelfToChild(cl, draggedItemBottomRight, cachedInverseMatrix);
+            float overlapRegionRight = Math.min(cl.getWidth(), draggedItemBottomRight[0]);
+            float overlapRegionBottom = Math.min(cl.getHeight(), draggedItemBottomRight[1]);
+
+            if (overlapRegionRight >= 0 && overlapRegionBottom <= cl.getHeight()) {
+                float overlap = (overlapRegionRight - overlapRegionLeft) *
+                         (overlapRegionBottom - overlapRegionTop);
+                if (overlap > 0) {
+                    return true;
+                }
+             }
+        }
+        return false;
+    }
+
+    /*
+     *
+     * This method returns the CellLayout that is currently being dragged to. In order to drag
+     * to a CellLayout, either the touch point must be directly over the CellLayout, or as a second
+     * strategy, we see if the dragView is overlapping any CellLayout and choose the closest one
+     *
+     * Return null if no CellLayout is currently being dragged over
+     *
+     */
+    private CellLayout findMatchingPageForDragOver(
+            DragView dragView, float originX, float originY, boolean exact) {
+        // We loop through all the screens (ie CellLayouts) and see which ones overlap
+        // with the item being dragged and then choose the one that's closest to the touch point
+        final int screenCount = getChildCount();
+        CellLayout bestMatchingScreen = null;
+        float smallestDistSoFar = Float.MAX_VALUE;
+
+        for (int i = 0; i < screenCount; i++) {
+            CellLayout cl = (CellLayout) getChildAt(i);
+
+            final float[] touchXy = {originX, originY};
+            // Transform the touch coordinates to the CellLayout's local coordinates
+            // If the touch point is within the bounds of the cell layout, we can return immediately
+            cl.getMatrix().invert(mTempInverseMatrix);
+            mapPointFromSelfToChild(cl, touchXy, mTempInverseMatrix);
+
+            if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() &&
+                    touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) {
+                return cl;
+            }
+
+            if (!exact) {
+                // Get the center of the cell layout in screen coordinates
+                final float[] cellLayoutCenter = mTempCellLayoutCenterCoordinates;
+                cellLayoutCenter[0] = cl.getWidth()/2;
+                cellLayoutCenter[1] = cl.getHeight()/2;
+                mapPointFromChildToSelf(cl, cellLayoutCenter);
+
+                touchXy[0] = originX;
+                touchXy[1] = originY;
+
+                // Calculate the distance between the center of the CellLayout
+                // and the touch point
+                float dist = squaredDistance(touchXy, cellLayoutCenter);
+
+                if (dist < smallestDistSoFar) {
+                    smallestDistSoFar = dist;
+                    bestMatchingScreen = cl;
+                }
+            }
+        }
+        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);
+    }
+    private boolean isExternalDragWidget(DragObject d) {
+        return d.dragSource != this && isDragWidget(d);
+    }
+
+    public void onDragOver(DragObject d) {
+        // Skip drag over events while we are dragging over side pages
+        if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return;
+
+        Rect r = new Rect();
+        CellLayout layout = null;
+        ItemInfo item = (ItemInfo) d.dragInfo;
+
+        // 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);
+
+        final View child = (mDragInfo == null) ? null : mDragInfo.cell;
+        // Identify whether we have dragged over a side page
+        if (isSmall()) {
+            if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) {
+                mLauncher.getHotseat().getHitRect(r);
+                if (r.contains(d.x, d.y)) {
+                    layout = mLauncher.getHotseat().getLayout();
+                }
+            }
+            if (layout == null) {
+                layout = findMatchingPageForDragOver(d.dragView, d.x, d.y, false);
+            }
+            if (layout != mDragTargetLayout) {
+
+                setCurrentDropLayout(layout);
+                setCurrentDragOverlappingLayout(layout);
+
+                boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED);
+                if (isInSpringLoadedMode) {
+                    if (mLauncher.isHotseatLayout(layout)) {
+                        mSpringLoadedDragController.cancel();
+                    } else {
+                        mSpringLoadedDragController.setAlarm(mDragTargetLayout);
+                    }
+                }
+            }
+        } else {
+            // Test to see if we are over the hotseat otherwise just use the current page
+            if (mLauncher.getHotseat() != null && !isDragWidget(d)) {
+                mLauncher.getHotseat().getHitRect(r);
+                if (r.contains(d.x, d.y)) {
+                    layout = mLauncher.getHotseat().getLayout();
+                }
+            }
+            if (layout == null) {
+                layout = getCurrentDropLayout();
+            }
+            if (layout != mDragTargetLayout) {
+                setCurrentDropLayout(layout);
+                setCurrentDragOverlappingLayout(layout);
+            }
+        }
+
+        // Handle the drag over
+        if (mDragTargetLayout != null) {
+            // We want the point to be mapped to the dragTarget.
+            if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
+                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
+            } else {
+                mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null);
+            }
+
+            ItemInfo info = (ItemInfo) d.dragInfo;
+
+            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1], item.spanX, item.spanY,
+                    mDragTargetLayout, mTargetCell);
+
+            setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
+
+            float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
+                    mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
+
+            final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0],
+                    mTargetCell[1]);
+
+            manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
+                    targetCellDistance, dragOverView);
+
+            int minSpanX = item.spanX;
+            int minSpanY = item.spanY;
+            if (item.minSpanX > 0 && item.minSpanY > 0) {
+                minSpanX = item.minSpanX;
+                minSpanY = item.minSpanY;
+            }
+
+            boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
+                    mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
+                    item.spanY, child, mTargetCell);
+
+            if (!nearestDropOccupied) {
+                mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
+                        (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
+                        mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false,
+                        d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion());
+            } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
+                    && !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] ||
+                    mLastReorderY != mTargetCell[1])) {
+
+                // Otherwise, if we aren't adding to or creating a folder and there's no pending
+                // reorder, then we schedule a reorder
+                ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
+                        minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child);
+                mReorderAlarm.setOnAlarmListener(listener);
+                mReorderAlarm.setAlarm(REORDER_TIMEOUT);
+            }
+
+            if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
+                    !nearestDropOccupied) {
+                if (mDragTargetLayout != null) {
+                    mDragTargetLayout.revertTempState();
+                }
+            }
+        }
+    }
+
+    private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout,
+            int[] targetCell, float distance, View dragOverView) {
+        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);
+            return;
+        }
+
+        boolean willAddToFolder =
+                willAddToExistingUserFolder(info, targetLayout, targetCell, distance);
+
+        if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
+            mDragOverFolderIcon = ((FolderIcon) dragOverView);
+            mDragOverFolderIcon.onDragEnter(info);
+            if (targetLayout != null) {
+                targetLayout.clearDragOutlines();
+            }
+            setDragMode(DRAG_MODE_ADD_TO_FOLDER);
+            return;
+        }
+
+        if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) {
+            setDragMode(DRAG_MODE_NONE);
+        }
+        if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) {
+            setDragMode(DRAG_MODE_NONE);
+        }
+
+        return;
+    }
+
+    class FolderCreationAlarmListener implements OnAlarmListener {
+        CellLayout layout;
+        int cellX;
+        int cellY;
+
+        public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) {
+            this.layout = layout;
+            this.cellX = cellX;
+            this.cellY = cellY;
+        }
+
+        public void onAlarm(Alarm alarm) {
+            if (mDragFolderRingAnimator == null) {
+                mDragFolderRingAnimator = new FolderRingAnimator(mLauncher, null);
+            }
+            mDragFolderRingAnimator.setCell(cellX, cellY);
+            mDragFolderRingAnimator.setCellLayout(layout);
+            mDragFolderRingAnimator.animateToAcceptState();
+            layout.showFolderAccept(mDragFolderRingAnimator);
+            layout.clearDragOutlines();
+            setDragMode(DRAG_MODE_CREATE_FOLDER);
+        }
+    }
+
+    class ReorderAlarmListener implements OnAlarmListener {
+        float[] dragViewCenter;
+        int minSpanX, minSpanY, spanX, spanY;
+        DragView dragView;
+        View child;
+
+        public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
+                int spanY, DragView dragView, View child) {
+            this.dragViewCenter = dragViewCenter;
+            this.minSpanX = minSpanX;
+            this.minSpanY = minSpanY;
+            this.spanX = spanX;
+            this.spanY = spanY;
+            this.child = child;
+            this.dragView = dragView;
+        }
+
+        public void onAlarm(Alarm alarm) {
+            int[] resultSpan = new int[2];
+            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell);
+            mLastReorderX = mTargetCell[0];
+            mLastReorderY = mTargetCell[1];
+
+            mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0],
+                (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
+                child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
+
+            if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
+                mDragTargetLayout.revertTempState();
+            } else {
+                setDragMode(DRAG_MODE_REORDER);
+            }
+
+            boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
+            mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
+                (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
+                mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize,
+                dragView.getDragVisualizeOffset(), dragView.getDragRegion());
+        }
+    }
+
+    @Override
+    public void getHitRect(Rect outRect) {
+        // We want the workspace to have the whole area of the display (it will find the correct
+        // cell layout to drop to in the existing drag/drop logic.
+        outRect.set(0, 0, mDisplaySize.x, mDisplaySize.y);
+    }
+
+    /**
+     * Add the item specified by dragInfo to the given layout.
+     * @return true if successful
+     */
+    public boolean addExternalItemToScreen(ItemInfo dragInfo, CellLayout layout) {
+        if (layout.findCellForSpan(mTempEstimate, dragInfo.spanX, dragInfo.spanY)) {
+            onDropExternal(dragInfo.dropPos, (ItemInfo) dragInfo, (CellLayout) layout, false);
+            return true;
+        }
+        mLauncher.showOutOfSpaceMessage(mLauncher.isHotseatLayout(layout));
+        return false;
+    }
+
+    private void onDropExternal(int[] touchXY, Object dragInfo,
+            CellLayout cellLayout, boolean insertAtFirst) {
+        onDropExternal(touchXY, dragInfo, cellLayout, insertAtFirst, null);
+    }
+
+    /**
+     * Drop an item that didn't originate on one of the workspace screens.
+     * It may have come from Launcher (e.g. from all apps or customize), or it may have
+     * come from another app altogether.
+     *
+     * NOTE: This can also be called when we are outside of a drag event, when we want
+     * to add an item to one of the workspace screens.
+     */
+    private void onDropExternal(final int[] touchXY, final Object dragInfo,
+            final CellLayout cellLayout, boolean insertAtFirst, DragObject d) {
+        final Runnable exitSpringLoadedRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mLauncher.exitSpringLoadedDragModeDelayed(true, false, null);
+            }
+        };
+
+        ItemInfo info = (ItemInfo) dragInfo;
+        int spanX = info.spanX;
+        int spanY = info.spanY;
+        if (mDragInfo != null) {
+            spanX = mDragInfo.spanX;
+            spanY = mDragInfo.spanY;
+        }
+
+        final long container = mLauncher.isHotseatLayout(cellLayout) ?
+                LauncherSettings.Favorites.CONTAINER_HOTSEAT :
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP;
+        final int screen = indexOfChild(cellLayout);
+        if (!mLauncher.isHotseatLayout(cellLayout) && screen != mCurrentPage
+                && mState != State.SPRING_LOADED) {
+            snapToPage(screen);
+        }
+
+        if (info instanceof PendingAddItemInfo) {
+            final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) dragInfo;
+
+            boolean findNearestVacantCell = true;
+            if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
+                mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY,
+                        cellLayout, mTargetCell);
+                float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
+                        mDragViewVisualCenter[1], mTargetCell);
+                if (willCreateUserFolder((ItemInfo) d.dragInfo, cellLayout, mTargetCell,
+                        distance, true) || willAddToExistingUserFolder((ItemInfo) d.dragInfo,
+                                cellLayout, mTargetCell, distance)) {
+                    findNearestVacantCell = false;
+                }
+            }
+
+            final ItemInfo item = (ItemInfo) d.dragInfo;
+            boolean updateWidgetSize = false;
+            if (findNearestVacantCell) {
+                int minSpanX = item.spanX;
+                int minSpanY = item.spanY;
+                if (item.minSpanX > 0 && item.minSpanY > 0) {
+                    minSpanX = item.minSpanX;
+                    minSpanY = item.minSpanY;
+                }
+                int[] resultSpan = new int[2];
+                mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0],
+                        (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY,
+                        null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL);
+
+                if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) {
+                    updateWidgetSize = true;
+                }
+                item.spanX = resultSpan[0];
+                item.spanY = resultSpan[1];
+            }
+
+            Runnable onAnimationCompleteRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    // 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, screen, mTargetCell, span, null);
+                        break;
+                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        mLauncher.processShortcutFromDrop(pendingInfo.componentName,
+                                container, screen, mTargetCell, null);
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown item type: " +
+                                pendingInfo.itemType);
+                    }
+                }
+            };
+            View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+                    ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null;
+
+            if (finalView instanceof AppWidgetHostView && updateWidgetSize) {
+                AppWidgetHostView awhv = (AppWidgetHostView) finalView;
+                AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, item.spanX,
+                        item.spanY);
+            }
+
+            int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR;
+            if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
+                    ((PendingAddWidgetInfo) pendingInfo).info.configure != null) {
+                animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN;
+            }
+            animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable,
+                    animationStyle, finalView, true);
+        } else {
+            // This is for other drag/drop cases, like dragging from All Apps
+            View view = null;
+
+            switch (info.itemType) {
+            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                if (info.container == NO_ID && info instanceof ApplicationInfo) {
+                    // Came from all apps -- make a copy
+                    info = new ShortcutInfo((ApplicationInfo) info);
+                }
+                view = mLauncher.createShortcut(R.layout.application, cellLayout,
+                        (ShortcutInfo) info);
+                break;
+            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+                        (FolderInfo) info, mIconCache);
+                break;
+            default:
+                throw new IllegalStateException("Unknown item type: " + info.itemType);
+            }
+
+            // First we find the cell nearest to point at which the item is
+            // dropped, without any consideration to whether there is an item there.
+            if (touchXY != null) {
+                mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY,
+                        cellLayout, mTargetCell);
+                float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
+                        mDragViewVisualCenter[1], mTargetCell);
+                d.postAnimationRunnable = exitSpringLoadedRunnable;
+                if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance,
+                        true, d.dragView, d.postAnimationRunnable)) {
+                    return;
+                }
+                if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d,
+                        true)) {
+                    return;
+                }
+            }
+
+            if (touchXY != null) {
+                // when dragging and dropping, just find the closest free spot
+                mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0],
+                        (int) mDragViewVisualCenter[1], 1, 1, 1, 1,
+                        null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL);
+            } else {
+                cellLayout.findCellForSpan(mTargetCell, 1, 1);
+            }
+            addInScreen(view, container, screen, mTargetCell[0], mTargetCell[1], info.spanX,
+                    info.spanY, insertAtFirst);
+            cellLayout.onDropChild(view);
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
+            cellLayout.getShortcutsAndWidgets().measureChild(view);
+
+
+            LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen,
+                    lp.cellX, lp.cellY);
+
+            if (d.dragView != null) {
+                // We wrap the animation call in the temporary set and reset of the current
+                // cellLayout to its final transform -- this means we animate the drag view to
+                // the correct final location.
+                setFinalTransitionTransform(cellLayout);
+                mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view,
+                        exitSpringLoadedRunnable);
+                resetTransitionTransform(cellLayout);
+            }
+        }
+    }
+
+    public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
+        int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX,
+                widgetInfo.spanY, widgetInfo, false);
+        int visibility = layout.getVisibility();
+        layout.setVisibility(VISIBLE);
+
+        int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY);
+        int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY);
+        Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1],
+                Bitmap.Config.ARGB_8888);
+        Canvas c = new Canvas(b);
+
+        layout.measure(width, height);
+        layout.layout(0, 0, unScaledSize[0], unScaledSize[1]);
+        layout.draw(c);
+        c.setBitmap(null);
+        layout.setVisibility(visibility);
+        return b;
+    }
+
+    private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY,
+            DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell,
+            boolean external, boolean scale) {
+        // Now we animate the dragView, (ie. the widget or shortcut preview) into its final
+        // location and size on the home screen.
+        int spanX = info.spanX;
+        int spanY = info.spanY;
+
+        Rect r = estimateItemPosition(layout, info, targetCell[0], targetCell[1], spanX, spanY);
+        loc[0] = r.left;
+        loc[1] = r.top;
+
+        setFinalTransitionTransform(layout);
+        float cellLayoutScale =
+                mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, loc);
+        resetTransitionTransform(layout);
+
+        float dragViewScaleX;
+        float dragViewScaleY;
+        if (scale) {
+            dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth();
+            dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight();
+        } else {
+            dragViewScaleX = 1f;
+            dragViewScaleY = 1f;
+        }
+
+        // The animation will scale the dragView about its center, so we need to center about
+        // the final location.
+        loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2;
+        loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2;
+
+        scaleXY[0] = dragViewScaleX * cellLayoutScale;
+        scaleXY[1] = dragViewScaleY * cellLayoutScale;
+    }
+
+    public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, DragView dragView,
+            final Runnable onCompleteRunnable, int animationType, final View finalView,
+            boolean external) {
+        Rect from = new Rect();
+        mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from);
+
+        int[] finalPos = new int[2];
+        float scaleXY[] = new float[2];
+        boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
+        getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell,
+                external, scalePreview);
+
+        Resources res = mLauncher.getResources();
+        int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200;
+
+        // 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);
+        }
+        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) {
+            scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0],  scaleXY[1]);
+        }
+
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) {
+            mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f,
+                    DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
+        } else {
+            int endStyle;
+            if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) {
+                endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE;
+            } else {
+                endStyle = DragLayer.ANIMATION_END_DISAPPEAR;;
+            }
+
+            Runnable onComplete = new Runnable() {
+                @Override
+                public void run() {
+                    if (finalView != null) {
+                        finalView.setVisibility(VISIBLE);
+                    }
+                    if (onCompleteRunnable != null) {
+                        onCompleteRunnable.run();
+                    }
+                }
+            };
+            dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0],
+                    finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
+                    duration, this);
+        }
+    }
+
+    public void setFinalTransitionTransform(CellLayout layout) {
+        if (isSwitchingState()) {
+            int index = indexOfChild(layout);
+            mCurrentScaleX = layout.getScaleX();
+            mCurrentScaleY = layout.getScaleY();
+            mCurrentTranslationX = layout.getTranslationX();
+            mCurrentTranslationY = layout.getTranslationY();
+            mCurrentRotationY = layout.getRotationY();
+            layout.setScaleX(mNewScaleXs[index]);
+            layout.setScaleY(mNewScaleYs[index]);
+            layout.setTranslationX(mNewTranslationXs[index]);
+            layout.setTranslationY(mNewTranslationYs[index]);
+            layout.setRotationY(mNewRotationYs[index]);
+        }
+    }
+    public void resetTransitionTransform(CellLayout layout) {
+        if (isSwitchingState()) {
+            mCurrentScaleX = layout.getScaleX();
+            mCurrentScaleY = layout.getScaleY();
+            mCurrentTranslationX = layout.getTranslationX();
+            mCurrentTranslationY = layout.getTranslationY();
+            mCurrentRotationY = layout.getRotationY();
+            layout.setScaleX(mCurrentScaleX);
+            layout.setScaleY(mCurrentScaleY);
+            layout.setTranslationX(mCurrentTranslationX);
+            layout.setTranslationY(mCurrentTranslationY);
+            layout.setRotationY(mCurrentRotationY);
+        }
+    }
+
+    /**
+     * Return the current {@link CellLayout}, correctly picking the destination
+     * screen while a scroll is in progress.
+     */
+    public CellLayout getCurrentDropLayout() {
+        return (CellLayout) getChildAt(getNextPage());
+    }
+
+    /**
+     * Return the current CellInfo describing our current drag; this method exists
+     * so that Launcher can sync this object with the correct info when the activity is created/
+     * destroyed
+     *
+     */
+    public CellLayout.CellInfo getDragInfo() {
+        return mDragInfo;
+    }
+
+    /**
+     * Calculate the nearest cell where the given object would be dropped.
+     *
+     * pixelX and pixelY should be in the coordinate system of layout
+     */
+    private int[] findNearestArea(int pixelX, int pixelY,
+            int spanX, int spanY, CellLayout layout, int[] recycle) {
+        return layout.findNearestArea(
+                pixelX, pixelY, spanX, spanY, recycle);
+    }
+
+    void setup(DragController dragController) {
+        mSpringLoadedDragController = new SpringLoadedDragController(mLauncher);
+        mDragController = dragController;
+
+        // hardware layers on children are enabled on startup, but should be disabled until
+        // needed
+        updateChildrenLayersEnabled(false);
+        setWallpaperDimension();
+    }
+
+    /**
+     * Called at the end of a drag which originated on the workspace.
+     */
+    public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (success) {
+            if (target != this) {
+                if (mDragInfo != null) {
+                    getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
+                    if (mDragInfo.cell instanceof DropTarget) {
+                        mDragController.removeDropTarget((DropTarget) mDragInfo.cell);
+                    }
+                }
+            }
+        } else if (mDragInfo != null) {
+            CellLayout cellLayout;
+            if (mLauncher.isHotseatLayout(target)) {
+                cellLayout = mLauncher.getHotseat().getLayout();
+            } else {
+                cellLayout = (CellLayout) getChildAt(mDragInfo.screen);
+            }
+            cellLayout.onDropChild(mDragInfo.cell);
+        }
+        if (d.cancelled &&  mDragInfo.cell != null) {
+                mDragInfo.cell.setVisibility(VISIBLE);
+        }
+        mDragOutline = null;
+        mDragInfo = null;
+
+        // Hide the scrolling indicator after you pick up an item
+        hideScrollingIndicator(false);
+    }
+
+    void updateItemLocationsInDatabase(CellLayout cl) {
+        int count = cl.getShortcutsAndWidgets().getChildCount();
+
+        int screen = indexOfChild(cl);
+        int container = Favorites.CONTAINER_DESKTOP;
+
+        if (mLauncher.isHotseatLayout(cl)) {
+            screen = -1;
+            container = Favorites.CONTAINER_HOTSEAT;
+        }
+
+        for (int i = 0; i < count; i++) {
+            View v = cl.getShortcutsAndWidgets().getChildAt(i);
+            ItemInfo info = (ItemInfo) v.getTag();
+            // Null check required as the AllApps button doesn't have an item info
+            if (info != null && info.requiresDbUpdate) {
+                info.requiresDbUpdate = false;
+                LauncherModel.modifyItemInDatabase(mLauncher, info, container, screen, info.cellX,
+                        info.cellY, info.spanX, info.spanY);
+            }
+        }
+    }
+
+    @Override
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
+    @Override
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
+    @Override
+    public void onFlingToDeleteCompleted() {
+        // Do nothing
+    }
+
+    public boolean isDropEnabled() {
+        return true;
+    }
+
+    @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 
+        // others we will need to wait until after their items are bound.
+        mSavedStates = container;
+    }
+
+    public void restoreInstanceStateForChild(int child) {
+        if (mSavedStates != null) {
+            mRestoredPages.add(child);
+            CellLayout cl = (CellLayout) getChildAt(child);
+            cl.restoreInstanceState(mSavedStates);
+        }
+    }
+
+    public void restoreInstanceStateForRemainingPages() {
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            if (!mRestoredPages.contains(i)) {
+                restoreInstanceStateForChild(i);
+            }
+        }
+        mRestoredPages.clear();
+    }
+
+    @Override
+    public void scrollLeft() {
+        if (!isSmall() && !mIsSwitchingState) {
+            super.scrollLeft();
+        }
+        Folder openFolder = getOpenFolder();
+        if (openFolder != null) {
+            openFolder.completeDragExit();
+        }
+    }
+
+    @Override
+    public void scrollRight() {
+        if (!isSmall() && !mIsSwitchingState) {
+            super.scrollRight();
+        }
+        Folder openFolder = getOpenFolder();
+        if (openFolder != null) {
+            openFolder.completeDragExit();
+        }
+    }
+
+    @Override
+    public boolean onEnterScrollArea(int x, int y, int direction) {
+        // Ignore the scroll area if we are dragging over the hot seat
+        boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext());
+        if (mLauncher.getHotseat() != null && isPortrait) {
+            Rect r = new Rect();
+            mLauncher.getHotseat().getHitRect(r);
+            if (r.contains(x, y)) {
+                return false;
+            }
+        }
+
+        boolean result = false;
+        if (!isSmall() && !mIsSwitchingState) {
+            mInScrollArea = true;
+
+            final int page = getNextPage() +
+                       (direction == DragController.SCROLL_LEFT ? -1 : 1);
+
+            // We always want to exit the current layout to ensure parity of enter / exit
+            setCurrentDropLayout(null);
+
+            if (0 <= page && page < getChildCount()) {
+                CellLayout layout = (CellLayout) getChildAt(page);
+                setCurrentDragOverlappingLayout(layout);
+
+                // Workspace is responsible for drawing the edge glow on adjacent pages,
+                // so we need to redraw the workspace when this may have changed.
+                invalidate();
+                result = true;
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean onExitScrollArea() {
+        boolean result = false;
+        if (mInScrollArea) {
+            invalidate();
+            CellLayout layout = getCurrentDropLayout();
+            setCurrentDropLayout(layout);
+            setCurrentDragOverlappingLayout(layout);
+
+            result = true;
+            mInScrollArea = false;
+        }
+        return result;
+    }
+
+    private void onResetScrollArea() {
+        setCurrentDragOverlappingLayout(null);
+        mInScrollArea = false;
+    }
+
+    /**
+     * Returns a specific CellLayout
+     */
+    CellLayout getParentCellLayoutForView(View v) {
+        ArrayList<CellLayout> layouts = getWorkspaceAndHotseatCellLayouts();
+        for (CellLayout layout : layouts) {
+            if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) {
+                return layout;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a list of all the CellLayouts in the workspace.
+     */
+    ArrayList<CellLayout> getWorkspaceAndHotseatCellLayouts() {
+        ArrayList<CellLayout> layouts = new ArrayList<CellLayout>();
+        int screenCount = getChildCount();
+        for (int screen = 0; screen < screenCount; screen++) {
+            layouts.add(((CellLayout) getChildAt(screen)));
+        }
+        if (mLauncher.getHotseat() != null) {
+            layouts.add(mLauncher.getHotseat().getLayout());
+        }
+        return layouts;
+    }
+
+    /**
+     * We should only use this to search for specific children.  Do not use this method to modify
+     * ShortcutsAndWidgetsContainer directly. Includes ShortcutAndWidgetContainers from
+     * the hotseat and workspace pages
+     */
+    ArrayList<ShortcutAndWidgetContainer> getAllShortcutAndWidgetContainers() {
+        ArrayList<ShortcutAndWidgetContainer> childrenLayouts =
+                new ArrayList<ShortcutAndWidgetContainer>();
+        int screenCount = getChildCount();
+        for (int screen = 0; screen < screenCount; screen++) {
+            childrenLayouts.add(((CellLayout) getChildAt(screen)).getShortcutsAndWidgets());
+        }
+        if (mLauncher.getHotseat() != null) {
+            childrenLayouts.add(mLauncher.getHotseat().getLayout().getShortcutsAndWidgets());
+        }
+        return childrenLayouts;
+    }
+
+    public Folder getFolderForTag(Object tag) {
+        ArrayList<ShortcutAndWidgetContainer> childrenLayouts =
+                getAllShortcutAndWidgetContainers();
+        for (ShortcutAndWidgetContainer layout: childrenLayouts) {
+            int count = layout.getChildCount();
+            for (int i = 0; i < count; i++) {
+                View child = layout.getChildAt(i);
+                if (child instanceof Folder) {
+                    Folder f = (Folder) child;
+                    if (f.getInfo() == tag && f.getInfo().opened) {
+                        return f;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    public View getViewForTag(Object tag) {
+        ArrayList<ShortcutAndWidgetContainer> childrenLayouts =
+                getAllShortcutAndWidgetContainers();
+        for (ShortcutAndWidgetContainer layout: childrenLayouts) {
+            int count = layout.getChildCount();
+            for (int i = 0; i < count; i++) {
+                View child = layout.getChildAt(i);
+                if (child.getTag() == tag) {
+                    return child;
+                }
+            }
+        }
+        return null;
+    }
+
+    void clearDropTargets() {
+        ArrayList<ShortcutAndWidgetContainer> childrenLayouts =
+                getAllShortcutAndWidgetContainers();
+        for (ShortcutAndWidgetContainer layout: childrenLayouts) {
+            int childCount = layout.getChildCount();
+            for (int j = 0; j < childCount; j++) {
+                View v = layout.getChildAt(j);
+                if (v instanceof DropTarget) {
+                    mDragController.removeDropTarget((DropTarget) v);
+                }
+            }
+        }
+    }
+
+    // Removes ALL items that match a given package name, this is usually called when a package
+    // has been removed and we want to remove all components (widgets, shortcuts, apps) that
+    // belong to that package.
+    void removeItemsByPackageName(final ArrayList<String> packages) {
+        HashSet<String> packageNames = new HashSet<String>();
+        packageNames.addAll(packages);
+
+        // Just create a hash table of all the specific components that this will affect
+        HashSet<ComponentName> cns = new HashSet<ComponentName>();
+        ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts();
+        for (CellLayout layoutParent : cellLayouts) {
+            ViewGroup layout = layoutParent.getShortcutsAndWidgets();
+            int childCount = layout.getChildCount();
+            for (int i = 0; i < childCount; ++i) {
+                View view = layout.getChildAt(i);
+                Object tag = view.getTag();
+
+                if (tag instanceof ShortcutInfo) {
+                    ShortcutInfo info = (ShortcutInfo) tag;
+                    ComponentName cn = info.intent.getComponent();
+                    if ((cn != null) && packageNames.contains(cn.getPackageName())) {
+                        cns.add(cn);
+                    }
+                } else if (tag instanceof FolderInfo) {
+                    FolderInfo info = (FolderInfo) tag;
+                    for (ShortcutInfo s : info.contents) {
+                        ComponentName cn = s.intent.getComponent();
+                        if ((cn != null) && packageNames.contains(cn.getPackageName())) {
+                            cns.add(cn);
+                        }
+                    }
+                } else if (tag instanceof LauncherAppWidgetInfo) {
+                    LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag;
+                    ComponentName cn = info.providerName;
+                    if ((cn != null) && packageNames.contains(cn.getPackageName())) {
+                        cns.add(cn);
+                    }
+                }
+            }
+        }
+
+        // Remove all the things
+        removeItemsByComponentName(cns);
+    }
+
+    // Removes items that match the application info specified, when applications are removed
+    // as a part of an update, this is called to ensure that other widgets and application
+    // shortcuts are not removed.
+    void removeItemsByApplicationInfo(final ArrayList<ApplicationInfo> appInfos) {
+        // Just create a hash table of all the specific components that this will affect
+        HashSet<ComponentName> cns = new HashSet<ComponentName>();
+        for (ApplicationInfo info : appInfos) {
+            cns.add(info.componentName);
+        }
+
+        // Remove all the things
+        removeItemsByComponentName(cns);
+    }
+
+    void removeItemsByComponentName(final HashSet<ComponentName> componentNames) {
+        ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts();
+        for (final CellLayout layoutParent: cellLayouts) {
+            final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
+
+            // Avoid ANRs by treating each screen separately
+            post(new Runnable() {
+                public void run() {
+                    final ArrayList<View> childrenToRemove = new ArrayList<View>();
+                    childrenToRemove.clear();
+
+                    int childCount = layout.getChildCount();
+                    for (int j = 0; j < childCount; j++) {
+                        final View view = layout.getChildAt(j);
+                        Object tag = view.getTag();
+
+                        if (tag instanceof ShortcutInfo) {
+                            final ShortcutInfo info = (ShortcutInfo) tag;
+                            final Intent intent = info.intent;
+                            final ComponentName name = intent.getComponent();
+
+                            if (name != null) {
+                                if (componentNames.contains(name)) {
+                                    LauncherModel.deleteItemFromDatabase(mLauncher, info);
+                                    childrenToRemove.add(view);
+                                }
+                            }
+                        } else if (tag instanceof FolderInfo) {
+                            final FolderInfo info = (FolderInfo) tag;
+                            final ArrayList<ShortcutInfo> contents = info.contents;
+                            final int contentsCount = contents.size();
+                            final ArrayList<ShortcutInfo> appsToRemoveFromFolder =
+                                    new ArrayList<ShortcutInfo>();
+
+                            for (int k = 0; k < contentsCount; k++) {
+                                final ShortcutInfo appInfo = contents.get(k);
+                                final Intent intent = appInfo.intent;
+                                final ComponentName name = intent.getComponent();
+
+                                if (name != null) {
+                                    if (componentNames.contains(name)) {
+                                        appsToRemoveFromFolder.add(appInfo);
+                                    }
+                                }
+                            }
+                            for (ShortcutInfo item: appsToRemoveFromFolder) {
+                                info.remove(item);
+                                LauncherModel.deleteItemFromDatabase(mLauncher, item);
+                            }
+                        } else if (tag instanceof LauncherAppWidgetInfo) {
+                            final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag;
+                            final ComponentName provider = info.providerName;
+                            if (provider != null) {
+                                if (componentNames.contains(provider)) {
+                                    LauncherModel.deleteItemFromDatabase(mLauncher, info);
+                                    childrenToRemove.add(view);
+                                }
+                            }
+                        }
+                    }
+
+                    childCount = childrenToRemove.size();
+                    for (int j = 0; j < childCount; j++) {
+                        View child = childrenToRemove.get(j);
+                        // Note: We can not remove the view directly from CellLayoutChildren as this
+                        // does not re-mark the spaces as unoccupied.
+                        layoutParent.removeViewInLayout(child);
+                        if (child instanceof DropTarget) {
+                            mDragController.removeDropTarget((DropTarget)child);
+                        }
+                    }
+
+                    if (childCount > 0) {
+                        layout.requestLayout();
+                        layout.invalidate();
+                    }
+                }
+            });
+        }
+
+        // Clean up new-apps animation list
+        final Context context = getContext();
+        post(new Runnable() {
+            @Override
+            public void run() {
+                String spKey = LauncherApplication.getSharedPreferencesKey();
+                SharedPreferences sp = context.getSharedPreferences(spKey,
+                        Context.MODE_PRIVATE);
+                Set<String> newApps = sp.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY,
+                        null);
+
+                // Remove all queued items that match the same package
+                if (newApps != null) {
+                    synchronized (newApps) {
+                        Iterator<String> iter = newApps.iterator();
+                        while (iter.hasNext()) {
+                            try {
+                                Intent intent = Intent.parseUri(iter.next(), 0);
+                                if (componentNames.contains(intent.getComponent())) {
+                                    iter.remove();
+                                }
+
+                                // It is possible that we've queued an item to be loaded, yet it has
+                                // not been added to the workspace, so remove those items as well.
+                                ArrayList<ItemInfo> shortcuts;
+                                shortcuts = LauncherModel.getWorkspaceShortcutItemInfosWithIntent(
+                                        intent);
+                                for (ItemInfo info : shortcuts) {
+                                    LauncherModel.deleteItemFromDatabase(context, info);
+                                }
+                            } catch (URISyntaxException e) {}
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    void updateShortcuts(ArrayList<ApplicationInfo> apps) {
+        ArrayList<ShortcutAndWidgetContainer> childrenLayouts = getAllShortcutAndWidgetContainers();
+        for (ShortcutAndWidgetContainer layout: childrenLayouts) {
+            int childCount = layout.getChildCount();
+            for (int j = 0; j < childCount; j++) {
+                final View view = layout.getChildAt(j);
+                Object tag = view.getTag();
+                if (tag instanceof ShortcutInfo) {
+                    ShortcutInfo info = (ShortcutInfo) tag;
+                    // We need to check for ACTION_MAIN otherwise getComponent() might
+                    // return null for some shortcuts (for instance, for shortcuts to
+                    // web pages.)
+                    final Intent intent = info.intent;
+                    final ComponentName name = intent.getComponent();
+                    if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
+                            Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) {
+                        final int appCount = apps.size();
+                        for (int k = 0; k < appCount; k++) {
+                            ApplicationInfo app = apps.get(k);
+                            if (app.componentName.equals(name)) {
+                                BubbleTextView shortcut = (BubbleTextView) view;
+                                info.updateIcon(mIconCache);
+                                info.title = app.title.toString();
+                                shortcut.applyFromShortcutInfo(info, mIconCache);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void moveToDefaultScreen(boolean animate) {
+        if (!isSmall()) {
+            if (animate) {
+                snapToPage(mDefaultPage);
+            } else {
+                setCurrentPage(mDefaultPage);
+            }
+        }
+        getChildAt(mDefaultPage).requestFocus();
+    }
+
+    @Override
+    public void syncPages() {
+    }
+
+    @Override
+    public void syncPageItems(int page, boolean immediate) {
+    }
+
+    @Override
+    protected String getCurrentPageDescription() {
+        int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
+        return String.format(getContext().getString(R.string.workspace_scroll_format),
+                page + 1, getChildCount());
+    }
+
+    public void getLocationInDragLayer(int[] loc) {
+        mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
+    }
+
+    void setFadeForOverScroll(float fade) {
+        if (!isScrollingIndicatorEnabled()) return;
+
+        mOverscrollFade = fade;
+        float reducedFade = 0.5f + 0.5f * (1 - fade);
+        final ViewGroup parent = (ViewGroup) getParent();
+        final ImageView qsbDivider = (ImageView) (parent.findViewById(R.id.qsb_divider));
+        final ImageView dockDivider = (ImageView) (parent.findViewById(R.id.dock_divider));
+        final View scrollIndicator = getScrollingIndicator();
+
+        cancelScrollingIndicatorAnimations();
+        if (qsbDivider != null) qsbDivider.setAlpha(reducedFade);
+        if (dockDivider != null) dockDivider.setAlpha(reducedFade);
+        scrollIndicator.setAlpha(1 - fade);
+    }
+}