Add gestures to Home.

Press the Home key while in Home to enable the gestures pad.
diff --git a/src/com/android/launcher/ApplicationInfo.java b/src/com/android/launcher/ApplicationInfo.java
index 9bc0950..c9c8c5c 100644
--- a/src/com/android/launcher/ApplicationInfo.java
+++ b/src/com/android/launcher/ApplicationInfo.java
@@ -61,7 +61,7 @@
     Intent.ShortcutIconResource iconResource;
 
     ApplicationInfo() {
-        itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
     }
     
     public ApplicationInfo(ApplicationInfo info) {
@@ -80,7 +80,7 @@
 
     /**
      * Creates the application intent based on a component name and various launch flags.
-     * Sets {@link #itemType} to {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}.
+     * 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
@@ -90,7 +90,7 @@
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
         intent.setComponent(className);
         intent.setFlags(launchFlags);
-        itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION;
     }
 
     @Override
@@ -98,22 +98,24 @@
         super.onAddToDatabase(values);
 
         String titleStr = title != null ? title.toString() : null;
-        values.put(LauncherSettings.Favorites.TITLE, titleStr);
+        values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr);
 
         String uri = intent != null ? intent.toURI() : null;
-        values.put(LauncherSettings.Favorites.INTENT, uri);
+        values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri);
 
         if (customIcon) {
-            values.put(LauncherSettings.Favorites.ICON_TYPE,
-                    LauncherSettings.Favorites.ICON_TYPE_BITMAP);
+            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
+                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_BITMAP);
             Bitmap bitmap = ((FastBitmapDrawable) icon).getBitmap();
             writeBitmap(values, bitmap);
         } else {
-            values.put(LauncherSettings.Favorites.ICON_TYPE,
-                    LauncherSettings.Favorites.ICON_TYPE_RESOURCE);
+            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
+                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE);
             if (iconResource != null) {
-                values.put(LauncherSettings.Favorites.ICON_PACKAGE, iconResource.packageName);
-                values.put(LauncherSettings.Favorites.ICON_RESOURCE, iconResource.resourceName);
+                values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE,
+                        iconResource.packageName);
+                values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE,
+                        iconResource.resourceName);
             }
         }
     }
diff --git a/src/com/android/launcher/GesturesActivity.java b/src/com/android/launcher/GesturesActivity.java
new file mode 100644
index 0000000..a112e1b
--- /dev/null
+++ b/src/com/android/launcher/GesturesActivity.java
@@ -0,0 +1,310 @@
+/*
+ * 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.launcher;
+
+import android.app.ListActivity;
+import android.app.Dialog;
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.os.AsyncTask;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.AdapterView;
+import android.widget.Toast;
+import android.widget.EditText;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.gesture.GestureLibrary;
+import android.gesture.Gesture;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.text.TextUtils;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Collections;
+import java.util.Map;
+
+public class GesturesActivity extends ListActivity {
+    private static final int MENU_ID_RENAME = 1;
+    private static final int MENU_ID_REMOVE = 2;
+
+    private static final int DIALOG_RENAME_GESTURE = 1;
+
+    private final Comparator<ApplicationInfo> mSorter =
+            new LauncherModel.ApplicationInfoComparator();
+
+    private GesturesAdapter mAdapter;
+    private GestureLibrary mStore;
+    private GesturesLoadTask mTask;
+    private TextView mEmpty;
+
+    private Dialog mRenameDialog;
+    private EditText mInput;
+    private ApplicationInfo mCurrentRenameInfo;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.gestures_settings);
+
+        mAdapter = new GesturesAdapter(this);
+        setListAdapter(mAdapter);
+
+        mStore = Launcher.getGestureLibrary();
+        mEmpty = (TextView) findViewById(android.R.id.empty);
+        mTask = (GesturesLoadTask) new GesturesLoadTask().execute();
+
+        registerForContextMenu(getListView());
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        if (mTask != null && mTask.getStatus() != GesturesLoadTask.Status.FINISHED) {
+            mTask.cancel(true);
+            mTask = null;
+        }
+
+        cleanupRenameDialog();
+    }
+
+    private void checkForEmpty() {
+        if (mAdapter.getCount() == 0) {
+            mEmpty.setText(R.string.gestures_empty);
+        }
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenu.ContextMenuInfo menuInfo) {
+
+        super.onCreateContextMenu(menu, v, menuInfo);
+
+        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+        menu.setHeaderTitle(((TextView) info.targetView).getText());
+
+        menu.add(0, MENU_ID_RENAME, 0, R.string.gestures_rename);
+        menu.add(0, MENU_ID_REMOVE, 0, R.string.gestures_delete);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        final AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)
+                item.getMenuInfo();
+        final ApplicationInfo info = (ApplicationInfo) menuInfo.targetView.getTag();
+
+        switch (item.getItemId()) {
+            case MENU_ID_RENAME:
+                renameGesture(info);
+                return true;
+            case MENU_ID_REMOVE:
+                deleteGesture(info);
+                return true;
+        }
+
+        return super.onContextItemSelected(item);
+    }
+
+    private void renameGesture(ApplicationInfo info) {
+        mCurrentRenameInfo = info;
+        showDialog(DIALOG_RENAME_GESTURE);
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        if (id == DIALOG_RENAME_GESTURE) {
+            return createRenameDialog();
+        }
+        return super.onCreateDialog(id);
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog) {
+        super.onPrepareDialog(id, dialog);
+        if (id == DIALOG_RENAME_GESTURE) {
+            mInput.setText(mCurrentRenameInfo.title);
+        }
+    }
+
+    private Dialog createRenameDialog() {
+        final View layout = View.inflate(this, R.layout.rename_folder, null);
+        mInput = (EditText) layout.findViewById(R.id.folder_name);
+        ((TextView) layout.findViewById(R.id.label)).setText(R.string.gestures_rename_label);
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setIcon(0);
+        builder.setTitle(getString(R.string.gestures_rename_title));
+        builder.setCancelable(true);
+        builder.setOnCancelListener(new Dialog.OnCancelListener() {
+            public void onCancel(DialogInterface dialog) {
+                cleanupRenameDialog();
+            }
+        });
+        builder.setNegativeButton(getString(R.string.cancel_action),
+            new Dialog.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    cleanupRenameDialog();
+                }
+            }
+        );
+        builder.setPositiveButton(getString(R.string.rename_action),
+            new Dialog.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    changeGestureName();
+                }
+            }
+        );
+        builder.setView(layout);
+        return builder.create();
+    }
+
+    private void changeGestureName() {
+        final String name = mInput.getText().toString();
+        if (!TextUtils.isEmpty(name)) {
+            mCurrentRenameInfo.title = mInput.getText();
+            LauncherModel.updateGestureInDatabase(this, mCurrentRenameInfo);
+        }
+    }
+
+    private void cleanupRenameDialog() {
+        if (mRenameDialog != null) {
+            mRenameDialog.dismiss();
+            mRenameDialog = null;
+            mInput = null;
+        }
+    }
+
+    private void deleteGesture(ApplicationInfo info) {
+        mStore.removeEntry(String.valueOf(info.id));
+        // TODO: On a thread?
+        mStore.save();
+
+        final GesturesActivity.GesturesAdapter adapter = mAdapter;
+        adapter.setNotifyOnChange(false);
+        adapter.remove(info);
+        adapter.sort(mSorter);
+        checkForEmpty();
+        adapter.notifyDataSetChanged();
+        
+        LauncherModel.deleteGestureFromDatabase(this, info);
+
+        Toast.makeText(this, R.string.gestures_delete_success, Toast.LENGTH_SHORT).show();
+    }
+
+    private class GesturesLoadTask extends AsyncTask<Void, ApplicationInfo, Boolean> {
+        private int mThumbnailSize;
+        private int mThumbnailInset;
+        private int mPathColor;
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+
+            final Resources resources = getResources();
+            mPathColor = resources.getColor(R.color.gesture_color);
+            mThumbnailInset = (int) resources.getDimension(R.dimen.gesture_thumbnail_inset);
+            mThumbnailSize = (int) resources.getDimension(R.dimen.gesture_thumbnail_size);
+        }
+
+        protected Boolean doInBackground(Void... params) {
+            if (isCancelled()) return Boolean.FALSE;
+
+            final GestureLibrary store = mStore;
+
+            if (store.load()) {
+                final LauncherModel model = Launcher.getModel();
+
+                for (String name : store.getGestureEntries()) {
+                    final Gesture gesture = store.getGestures(name).get(0);
+                    final Bitmap bitmap = gesture.toBitmap(mThumbnailSize, mThumbnailSize,
+                            mThumbnailInset, mPathColor);
+                    final ApplicationInfo info = model.queryGesture(GesturesActivity.this, name);
+
+                    mAdapter.addBitmap(info.id, bitmap);
+                    publishProgress(info);
+                }
+
+                return Boolean.TRUE;
+            }
+
+            return Boolean.FALSE;
+        }
+
+        @Override
+        protected void onProgressUpdate(ApplicationInfo... values) {
+            super.onProgressUpdate(values);
+
+            final GesturesActivity.GesturesAdapter adapter = mAdapter;
+            adapter.setNotifyOnChange(false);
+
+            for (ApplicationInfo info : values) {
+                adapter.add(info);
+            }
+
+            adapter.sort(mSorter);
+            adapter.notifyDataSetChanged();
+        }
+
+        @Override
+        protected void onPostExecute(Boolean aBoolean) {
+            super.onPostExecute(aBoolean);
+            checkForEmpty();
+        }
+    }
+
+    private class GesturesAdapter extends ArrayAdapter<ApplicationInfo> {
+        private final LayoutInflater mInflater;
+        private final Map<Long, Drawable> mThumbnails = Collections.synchronizedMap(
+                new HashMap<Long, Drawable>());
+
+        public GesturesAdapter(Context context) {
+            super(context, 0);
+            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        }
+
+        void addBitmap(Long id, Bitmap bitmap) {
+            mThumbnails.put(id, new BitmapDrawable(bitmap));
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.gestures_settings_item, parent, false);
+            }
+
+            final ApplicationInfo info = getItem(position);
+            final TextView label = (TextView) convertView;
+
+            label.setTag(info);
+            label.setText(info.title);
+            label.setCompoundDrawablesWithIntrinsicBounds(info.icon, null,
+                    mThumbnails.get(info.id), null);
+
+            return convertView;
+        }
+    }
+}
diff --git a/src/com/android/launcher/GesturesConstants.java b/src/com/android/launcher/GesturesConstants.java
new file mode 100644
index 0000000..3151ea3
--- /dev/null
+++ b/src/com/android/launcher/GesturesConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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.launcher;
+
+interface GesturesConstants {
+    final double PREDICTION_THRESHOLD = 1.0;
+    final String STORE_NAME = "gestures";
+    final long MATCH_DELAY = 370;
+    final float LENGTH_THRESHOLD = 120.0f;
+    int PATH_SAMPLE_COUNT = 10;
+}
diff --git a/src/com/android/launcher/GesturesPanel.java b/src/com/android/launcher/GesturesPanel.java
new file mode 100644
index 0000000..ee39613
--- /dev/null
+++ b/src/com/android/launcher/GesturesPanel.java
@@ -0,0 +1,47 @@
+/*
+ * 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.launcher;
+
+import android.widget.RelativeLayout;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+
+public class GesturesPanel extends RelativeLayout {
+    public GesturesPanel(Context context) {
+        super(context);
+    }
+
+    public GesturesPanel(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return true;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+            ((Launcher) mContext).hideGesturesPanel();
+            return true;
+        }
+
+        return super.dispatchKeyEvent(event);
+    }
+}
diff --git a/src/com/android/launcher/ItemInfo.java b/src/com/android/launcher/ItemInfo.java
index 51449a7..71cee18 100644
--- a/src/com/android/launcher/ItemInfo.java
+++ b/src/com/android/launcher/ItemInfo.java
@@ -76,6 +76,11 @@
      */
     int spanY = 1;
 
+    /**
+     * Indicates whether the item is a gesture.
+     */
+    boolean isGesture = false;
+
     ItemInfo() {
     }
 
@@ -96,13 +101,15 @@
      * @param values
      */
     void onAddToDatabase(ContentValues values) { 
-        values.put(LauncherSettings.Favorites.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);
+        values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType);
+        if (!isGesture) {
+            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);
+        }
     }
 
     static void writeBitmap(ContentValues values, Bitmap bitmap) {
diff --git a/src/com/android/launcher/Launcher.java b/src/com/android/launcher/Launcher.java
index b4437d4..0c54382 100644
--- a/src/com/android/launcher/Launcher.java
+++ b/src/com/android/launcher/Launcher.java
@@ -41,6 +41,8 @@
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.PorterDuff;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.TransitionDrawable;
@@ -58,7 +60,6 @@
 import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
 import android.text.method.TextKeyListener;
-import android.util.Log;
 import static android.util.Log.*;
 import android.view.Display;
 import android.view.KeyEvent;
@@ -67,6 +68,8 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.MotionEvent;
+import android.view.Gravity;
 import android.view.View.OnLongClickListener;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
@@ -74,8 +77,16 @@
 import android.widget.SlidingDrawer;
 import android.widget.TextView;
 import android.widget.Toast;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.ViewSwitcher;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
+import android.gesture.GestureOverlayView;
+import android.gesture.GestureLibraries;
+import android.gesture.GestureLibrary;
+import android.gesture.Gesture;
+import android.gesture.Prediction;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -92,6 +103,7 @@
     private static final boolean PROFILE_DRAWER = false;
     private static final boolean PROFILE_ROTATE = false;
     private static final boolean DEBUG_USER_INTERFACE = false;
+    private static final boolean DEBUG_GESTURES = false;
 
     private static final int WALLPAPER_SCREENS_SPAN = 2;
 
@@ -100,7 +112,8 @@
     private static final int MENU_WALLPAPER_SETTINGS = MENU_ADD + 1;
     private static final int MENU_SEARCH = MENU_WALLPAPER_SETTINGS + 1;
     private static final int MENU_NOTIFICATIONS = MENU_SEARCH + 1;
-    private static final int MENU_SETTINGS = MENU_NOTIFICATIONS + 1;
+    private static final int MENU_GESTURES = MENU_NOTIFICATIONS + 1;
+    private static final int MENU_SETTINGS = MENU_GESTURES + 1;
 
     private static final int REQUEST_CREATE_SHORTCUT = 1;
     private static final int REQUEST_CREATE_LIVE_FOLDER = 4;
@@ -109,6 +122,9 @@
     private static final int REQUEST_PICK_SHORTCUT = 7;
     private static final int REQUEST_PICK_LIVE_FOLDER = 8;
     private static final int REQUEST_PICK_APPWIDGET = 9;
+    private static final int REQUEST_PICK_GESTURE_ACTION = 10;
+    private static final int REQUEST_CREATE_GESTURE_ACTION = 11;
+    private static final int REQUEST_CREATE_GESTURE_APPLICATION_ACTION = 12;
 
     static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate";
 
@@ -154,6 +170,8 @@
     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: Gesture (Parcelable)
+    private static final String RUNTIME_STATE_PENDING_GESTURE = "launcher.gesture";
 
     private static final LauncherModel sModel = new LauncherModel();
 
@@ -164,6 +182,8 @@
 
     private static WallpaperIntentReceiver sWallpaperReceiver;
 
+    private static GestureLibrary sLibrary;
+
     private final BroadcastReceiver mApplicationsReceiver = new ApplicationsIntentReceiver();
     private final ContentObserver mObserver = new FavoritesChangeObserver();
 
@@ -202,11 +222,26 @@
 
     private DesktopBinder mBinder;
 
+    private View mGesturesPanel;
+    private GestureOverlayView mGesturesOverlay;
+    private ViewSwitcher mGesturesPrompt;
+    private ImageView mGesturesAdd;
+    private PopupWindow mGesturesWindow;
+    private Launcher.GesturesProcessor mGesturesProcessor;
+    private Gesture mCurrentGesture;
+    private GesturesAction mGesturesAction;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mInflater = getLayoutInflater();
 
+        if (sLibrary == null) {
+            // The context is not kept by the library so it's safe to do this
+            sLibrary = GestureLibraries.fromPrivateFile(Launcher.this,
+                    GesturesConstants.STORE_NAME);
+        }
+
         mAppWidgetManager = AppWidgetManager.getInstance(this);
 
         mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
@@ -308,13 +343,17 @@
         // 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 && mAddItemCellInfo != null) {
+        if (resultCode == RESULT_OK && (mAddItemCellInfo != null ||
+                ((requestCode == REQUEST_PICK_GESTURE_ACTION ||
+                requestCode == REQUEST_CREATE_GESTURE_ACTION ||
+                requestCode == REQUEST_CREATE_GESTURE_APPLICATION_ACTION) && mCurrentGesture != null))) {
+
             switch (requestCode) {
                 case REQUEST_PICK_APPLICATION:
                     completeAddApplication(this, data, mAddItemCellInfo, !mDesktopLocked);
                     break;
                 case REQUEST_PICK_SHORTCUT:
-                    addShortcut(data);
+                    processShortcut(data, REQUEST_PICK_APPLICATION, REQUEST_CREATE_SHORTCUT);
                     break;
                 case REQUEST_CREATE_SHORTCUT:
                     completeAddShortcut(data, mAddItemCellInfo, !mDesktopLocked);
@@ -331,6 +370,16 @@
                 case REQUEST_CREATE_APPWIDGET:
                     completeAddAppWidget(data, mAddItemCellInfo, !mDesktopLocked);
                     break;
+                case REQUEST_PICK_GESTURE_ACTION:
+                    processShortcut(data, REQUEST_CREATE_GESTURE_APPLICATION_ACTION,
+                            REQUEST_CREATE_GESTURE_ACTION);
+                    break;
+                case REQUEST_CREATE_GESTURE_ACTION:
+                    completeCreateGesture(data, true);
+                    break;
+                case REQUEST_CREATE_GESTURE_APPLICATION_ACTION:
+                    completeCreateGesture(data, false);
+                    break;
             }
         } else if (requestCode == REQUEST_PICK_APPWIDGET &&
                 resultCode == RESULT_CANCELED && data != null) {
@@ -358,10 +407,20 @@
     @Override
     protected void onPause() {
         super.onPause();
+        if (mGesturesWindow != null) {
+            mGesturesWindow.setAnimationStyle(0);
+            mGesturesWindow.update();
+        }
         closeDrawer(false);
     }
 
     @Override
+    protected void onStop() {
+        super.onStop();
+        hideGesturesPanel();
+    }
+
+    @Override
     public Object onRetainNonConfigurationInstance() {
         // Flag any binder to stop early before switching
         if (mBinder != null) {
@@ -448,6 +507,8 @@
             mFolderInfo = sModel.getFolderById(this, id);
             mRestoring = true;
         }
+
+        mCurrentGesture = (Gesture) savedState.get(RUNTIME_STATE_PENDING_GESTURE);
     }
 
     /**
@@ -495,6 +556,68 @@
         dragLayer.setIgnoredDropTarget(grid);
         dragLayer.setDragScoller(workspace);
         dragLayer.setDragListener(deleteZone);
+
+        mGesturesPanel = mInflater.inflate(R.layout.gestures, mDragLayer, false);
+        final View gesturesPanel = mGesturesPanel;
+
+        mGesturesPrompt = (ViewSwitcher) gesturesPanel.findViewById(R.id.gestures_actions);
+        mGesturesAction = new GesturesAction();
+
+        mGesturesPrompt.getChildAt(0).setOnClickListener(mGesturesAction);
+        mGesturesPrompt.getChildAt(1).setOnClickListener(mGesturesAction);
+
+        mGesturesAdd = (ImageView) gesturesPanel.findViewById(R.id.gestures_add);
+        final ImageView gesturesAdd = mGesturesAdd;
+        gesturesAdd.setAlpha(128);
+        gesturesAdd.setEnabled(false);
+        gesturesAdd.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                createGesture();
+            }
+        });
+
+        mGesturesOverlay = (GestureOverlayView) gesturesPanel.findViewById(R.id.gestures_overlay);
+        mGesturesProcessor = new GesturesProcessor();
+
+        final GestureOverlayView overlay = mGesturesOverlay;
+        overlay.setFadeOffset(GesturesConstants.MATCH_DELAY);
+        overlay.addOnGestureListener(mGesturesProcessor);
+        overlay.getGesturePaint().setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));        
+    }
+
+    private void createGesture() {
+        mCurrentGesture = mGesturesOverlay.getGesture();
+        mWaitingForResult = true;
+        pickShortcut(REQUEST_PICK_GESTURE_ACTION, R.string.title_select_shortcut);
+    }
+
+    private void completeCreateGesture(Intent data, boolean isShortcut) {
+        ApplicationInfo info;
+
+        if (isShortcut) {
+            info = infoFromShortcutIntent(this, data);
+        } else {
+            info = infoFromApplicationIntent(this, data);
+        }
+
+        boolean success = false;
+        if (info != null) {
+            info.isGesture = true;
+
+            if (LauncherModel.addGestureToDatabase(this, info, false)) {
+                mGesturesProcessor.addGesture(String.valueOf(info.id), mCurrentGesture);
+                mGesturesProcessor.update(info, mCurrentGesture);
+                Toast.makeText(this, getString(R.string.gestures_created, info.title),
+                        Toast.LENGTH_SHORT).show();
+                success = true;
+            }
+        }
+
+        if (!success) {
+            Toast.makeText(this, getString(R.string.gestures_failed), Toast.LENGTH_SHORT).show();
+        }
+
+        mCurrentGesture = null;
     }
 
     /**
@@ -545,14 +668,20 @@
         cellInfo.screen = mWorkspace.getCurrentScreen();
         if (!findSingleSlot(cellInfo)) return;
 
-        // Find details for this application
+        final ApplicationInfo info = infoFromApplicationIntent(context, data);
+        if (info != null) {
+            mWorkspace.addApplicationShortcut(info, cellInfo, insertAtFirst);
+        }
+    }
+
+    private static ApplicationInfo infoFromApplicationIntent(Context context, Intent data) {
         ComponentName component = data.getComponent();
         PackageManager packageManager = context.getPackageManager();
         ActivityInfo activityInfo = null;
         try {
             activityInfo = packageManager.getActivityInfo(component, 0 /* no flags */);
         } catch (NameNotFoundException e) {
-            Log.e(LOG_TAG, "Couldn't find ActivityInfo for selected application", e);
+            e(LOG_TAG, "Couldn't find ActivityInfo for selected application", e);
         }
 
         if (activityInfo != null) {
@@ -568,8 +697,10 @@
             itemInfo.icon = activityInfo.loadIcon(packageManager);
             itemInfo.container = ItemInfo.NO_ID;
 
-            mWorkspace.addApplicationShortcut(itemInfo, cellInfo, insertAtFirst);
+            return itemInfo;
         }
+
+        return null;
     }
 
     /**
@@ -653,6 +784,14 @@
     static ApplicationInfo addShortcut(Context context, Intent data,
             CellLayout.CellInfo cellInfo, boolean notify) {
 
+        final ApplicationInfo info = infoFromShortcutIntent(context, data);
+        LauncherModel.addItemToDatabase(context, info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                cellInfo.screen, cellInfo.cellX, cellInfo.cellY, notify);
+
+        return info;
+    }
+
+    private static ApplicationInfo infoFromShortcutIntent(Context context, Intent data) {
         Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
         String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
         Bitmap bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
@@ -660,7 +799,7 @@
         Drawable icon = null;
         boolean filtered = false;
         boolean customIcon = false;
-        Intent.ShortcutIconResource iconResource = null;
+        ShortcutIconResource iconResource = null;
 
         if (bitmap != null) {
             icon = new FastBitmapDrawable(Utilities.createBitmapThumbnail(bitmap, context));
@@ -668,9 +807,9 @@
             customIcon = true;
         } else {
             Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
-            if (extra != null && extra instanceof Intent.ShortcutIconResource) {
+            if (extra != null && extra instanceof ShortcutIconResource) {
                 try {
-                    iconResource = (Intent.ShortcutIconResource) extra;
+                    iconResource = (ShortcutIconResource) extra;
                     final PackageManager packageManager = context.getPackageManager();
                     Resources resources = packageManager.getResourcesForApplication(
                             iconResource.packageName);
@@ -694,8 +833,6 @@
         info.customIcon = customIcon;
         info.iconResource = iconResource;
 
-        LauncherModel.addItemToDatabase(context, info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
-                cellInfo.screen, cellInfo.cellX, cellInfo.cellY, notify);
         return info;
     }
 
@@ -723,15 +860,15 @@
                 // An exception is thrown if the dialog is not visible, which is fine
             }
 
-            // If we are already in front we go back to the default screen,
-            // otherwise we don't
             if ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) !=
                     Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) {
-                if (!mWorkspace.isDefaultScreenShowing()) {
-                    mWorkspace.moveToDefaultScreen();
+
+                if (mGesturesPanel != null && mDragLayer.getWindowVisibility() == View.VISIBLE) {
+                    onHomeKeyPressed();
                 }
                 closeDrawer();
-                View v = getWindow().peekDecorView();
+
+                final View v = getWindow().peekDecorView();
                 if (v != null && v.getWindowToken() != null) {
                     InputMethodManager imm = (InputMethodManager)getSystemService(
                             INPUT_METHOD_SERVICE);
@@ -743,6 +880,74 @@
         }
     }
 
+    private void onHomeKeyPressed() {
+        if (mGesturesWindow == null || !mGesturesWindow.isShowing()) {
+            showGesturesPanel();
+        } else {
+            hideGesturesPanel();
+        }
+    }
+
+    private void showGesturesPanel() {
+        resetGesturesPrompt();
+
+        mGesturesAdd.setEnabled(false);
+        mGesturesAdd.setAlpha(128);
+
+        mGesturesOverlay.clear(false);
+
+        PopupWindow window;
+        if (mGesturesWindow == null) {
+            mGesturesWindow = new PopupWindow(this);
+            window = mGesturesWindow;
+            window.setFocusable(true);
+            window.setTouchable(true);
+            window.setBackgroundDrawable(null);
+            window.setContentView(mGesturesPanel);
+        } else {
+            window = mGesturesWindow;
+        }
+        window.setAnimationStyle(com.android.internal.R.style.Animation_SlidingCard);
+
+        final int[] xy = new int[2];
+        final DragLayer dragLayer = mDragLayer;
+        dragLayer.getLocationOnScreen(xy);
+
+        window.setWidth(dragLayer.getWidth());
+        window.setHeight(dragLayer.getHeight() - 1);
+        window.showAtLocation(dragLayer, Gravity.TOP | Gravity.LEFT, xy[0], xy[1] + 1);
+    }
+
+    private void resetGesturesPrompt() {
+        mGesturesAction.intent = null;
+        final TextView prompt = (TextView) mGesturesPrompt.getCurrentView();
+        prompt.setText(R.string.gestures_instructions);
+        prompt.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+        prompt.setClickable(false);
+    }
+
+    private void resetGesturesNextPrompt() {
+        mGesturesAction.intent = null;
+        setGesturesNextPrompt(null, getString(R.string.gestures_instructions));
+        mGesturesPrompt.getNextView().setClickable(false);
+    }
+
+    private void setGesturesNextPrompt(Drawable icon, CharSequence title) {
+        final TextView prompt = (TextView) mGesturesPrompt.getNextView();
+        prompt.setText(title);
+        prompt.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+        prompt.setClickable(true);
+        mGesturesPrompt.showNext();
+    }
+
+    void hideGesturesPanel() {
+        if (mGesturesWindow != null) {
+            mGesturesWindow.setAnimationStyle(com.android.internal.R.style.Animation_SlidingCard);
+            mGesturesWindow.update();
+            mGesturesWindow.dismiss();
+        }
+    }
+
     @Override
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
         // Do not call super here
@@ -791,6 +996,10 @@
             outState.putBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, true);
             outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id);
         }
+
+        if (mCurrentGesture != null && mWaitingForResult) {
+            outState.putParcelable(RUNTIME_STATE_PENDING_GESTURE, mCurrentGesture);
+        }
     }
 
     @Override
@@ -911,6 +1120,11 @@
                 .setIcon(com.android.internal.R.drawable.ic_menu_notifications)
                 .setAlphabeticShortcut('N');
 
+        final Intent gestures = new Intent(this, GesturesActivity.class);
+        menu.add(0, MENU_GESTURES, 0, R.string.menu_gestures)
+                .setIcon(com.android.internal.R.drawable.ic_menu_compose).setAlphabeticShortcut('G')
+                .setIntent(gestures);
+
         final Intent settings = new Intent(android.provider.Settings.ACTION_SETTINGS);
         settings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
@@ -1028,7 +1242,7 @@
         mWorkspace.addInCurrentScreen(view, xy[0], xy[1], info.spanX, spanY);
     }
 
-    void addShortcut(Intent intent) {
+    void processShortcut(Intent intent, int requestCodeApplication, int requestCodeShortcut) {
         // Handle case where user selected "Applications"
         String applicationName = getResources().getString(R.string.group_applications);
         String shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
@@ -1039,9 +1253,9 @@
 
             Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY);
             pickIntent.putExtra(Intent.EXTRA_INTENT, mainIntent);
-            startActivityForResult(pickIntent, REQUEST_PICK_APPLICATION);
+            startActivityForResult(pickIntent, requestCodeApplication);
         } else {
-            startActivityForResult(intent, REQUEST_CREATE_SHORTCUT);
+            startActivityForResult(intent, requestCodeShortcut);
         }
     }
 
@@ -1482,7 +1696,7 @@
             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(LOG_TAG, "Launcher does not have the permission to launch " + intent +
+            e(LOG_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);
         }
@@ -1601,6 +1815,10 @@
         return sModel;
     }
 
+    static GestureLibrary getGestureLibrary() {
+        return sLibrary;
+    }
+
     void closeAllApplications() {
         mDrawer.close();
     }
@@ -1669,6 +1887,26 @@
         showDialog(DIALOG_CREATE_SHORTCUT);
     }
 
+    private void pickShortcut(int requestCode, int title) {
+        Bundle bundle = new Bundle();
+
+        ArrayList<String> shortcutNames = new ArrayList<String>();
+        shortcutNames.add(getString(R.string.group_applications));
+        bundle.putStringArrayList(Intent.EXTRA_SHORTCUT_NAME, shortcutNames);
+
+        ArrayList<ShortcutIconResource> shortcutIcons = new ArrayList<ShortcutIconResource>();
+        shortcutIcons.add(ShortcutIconResource.fromContext(Launcher.this,
+                        R.drawable.ic_launcher_application));
+        bundle.putParcelableArrayList(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, shortcutIcons);
+
+        Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY);
+        pickIntent.putExtra(Intent.EXTRA_INTENT, new Intent(Intent.ACTION_CREATE_SHORTCUT));
+        pickIntent.putExtra(Intent.EXTRA_TITLE, getText(title));
+        pickIntent.putExtras(bundle);
+
+        startActivityForResult(pickIntent, requestCode);
+    }
+
     private class RenameFolder {
         private EditText mInput;
 
@@ -1789,26 +2027,7 @@
             switch (which) {
                 case AddAdapter.ITEM_SHORTCUT: {
                     // Insert extra item to handle picking application
-                    Bundle bundle = new Bundle();
-
-                    ArrayList<String> shortcutNames = new ArrayList<String>();
-                    shortcutNames.add(res.getString(R.string.group_applications));
-                    bundle.putStringArrayList(Intent.EXTRA_SHORTCUT_NAME, shortcutNames);
-
-                    ArrayList<ShortcutIconResource> shortcutIcons =
-                            new ArrayList<ShortcutIconResource>();
-                    shortcutIcons.add(ShortcutIconResource.fromContext(Launcher.this,
-                            R.drawable.ic_launcher_application));
-                    bundle.putParcelableArrayList(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, shortcutIcons);
-
-                    Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY);
-                    pickIntent.putExtra(Intent.EXTRA_INTENT,
-                            new Intent(Intent.ACTION_CREATE_SHORTCUT));
-                    pickIntent.putExtra(Intent.EXTRA_TITLE,
-                            getText(R.string.title_select_shortcut));
-                    pickIntent.putExtras(bundle);
-
-                    startActivityForResult(pickIntent, REQUEST_PICK_SHORTCUT);
+                    pickShortcut(REQUEST_PICK_SHORTCUT, R.string.title_select_shortcut);
                     break;
                 }
 
@@ -2109,5 +2328,113 @@
             }
         }
     }
+
+    private class GesturesProcessor implements GestureOverlayView.OnGestureListener,
+            GestureOverlayView.OnGesturePerformedListener {
+
+        private final GestureMatcher mMatcher = new GestureMatcher();
+
+        GesturesProcessor() {
+            // TODO: Maybe the load should happen on a background thread?
+            sLibrary.load();
+        }
+
+        public void onGestureStarted(GestureOverlayView overlay, MotionEvent event) {
+            overlay.removeCallbacks(mMatcher);
+            resetGesturesNextPrompt();
+
+            mGesturesAdd.setAlpha(128);
+            mGesturesAdd.setEnabled(false);
+        }
+
+        public void onGesture(GestureOverlayView overlay, MotionEvent event) {
+        }
+
+        public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {
+        }
+
+        public void onGestureEnded(GestureOverlayView overlay, MotionEvent event) {
+            overlay.removeCallbacks(mMatcher);
+
+            mMatcher.gesture = overlay.getGesture();
+            if (mMatcher.gesture.getLength() < GesturesConstants.LENGTH_THRESHOLD) {
+                overlay.clear(false);
+            } else {
+                overlay.postDelayed(mMatcher, GesturesConstants.MATCH_DELAY);
+            }
+        }
+
+        private void matchGesture(Gesture gesture) {
+            mGesturesAdd.setAlpha(255);
+            mGesturesAdd.setEnabled(true);
+
+            if (gesture != null) {
+                final ArrayList<Prediction> predictions = sLibrary.recognize(gesture);
+
+                if (DEBUG_GESTURES) {
+                    for (Prediction p : predictions) {
+                        d(LOG_TAG, String.format("name=%s, score=%f", p.name, p.score));
+                    }
+                }
+
+                boolean match = false;
+                if (predictions.size() > 0) {
+                    final Prediction prediction = predictions.get(0);
+                    if (prediction.score > GesturesConstants.PREDICTION_THRESHOLD) {
+                        match = true;
+
+                        ApplicationInfo info = sModel.queryGesture(Launcher.this, prediction.name);
+                        if (info != null) {
+                            updatePrompt(info);
+                        }
+                    }
+                }
+
+                if (!match){
+                    setGesturesNextPrompt(null, getString(R.string.gestures_unknown));
+                }
+            }
+        }
+
+        private void updatePrompt(ApplicationInfo info) {
+            setGesturesNextPrompt(info.icon, info.title);
+            mGesturesAction.intent = info.intent;
+        }
+
+        public void onGestureCancelled(GestureOverlayView overlay, MotionEvent event) {
+            overlay.removeCallbacks(mMatcher);
+        }
+
+        void addGesture(String name, Gesture gesture) {
+            sLibrary.addGesture(name, gesture);
+            // TODO: On a background thread?
+            sLibrary.save();
+        }
+
+        void update(ApplicationInfo info, Gesture gesture) {
+            mGesturesOverlay.setGesture(gesture);
+            updatePrompt(info);            
+        }
+
+        class GestureMatcher implements Runnable {
+            Gesture gesture;
+
+            public void run() {
+                if (gesture != null) {
+                    matchGesture(gesture);
+                }
+            }
+        }
+    }
+
+    private class GesturesAction implements View.OnClickListener {
+        Intent intent;
+
+        public void onClick(View v) {
+            if (intent != null) {
+                startActivitySafely(intent);
+            }
+        }
+    }
 }
 
diff --git a/src/com/android/launcher/LauncherModel.java b/src/com/android/launcher/LauncherModel.java
index 19f6e9b..271f9f4 100644
--- a/src/com/android/launcher/LauncherModel.java
+++ b/src/com/android/launcher/LauncherModel.java
@@ -560,7 +560,7 @@
         }
     }
 
-    private static class ApplicationInfoComparator implements Comparator<ApplicationInfo> {
+    static class ApplicationInfoComparator implements Comparator<ApplicationInfo> {
         public final int compare(ApplicationInfo a, ApplicationInfo b) {
             return sCollator.compare(a.title.toString(), b.title.toString());
         }
@@ -614,11 +614,11 @@
 
     private static void updateShortcutLabels(ContentResolver resolver, PackageManager manager) {
         final Cursor c = resolver.query(LauncherSettings.Favorites.CONTENT_URI,
-                new String[] { LauncherSettings.Favorites.ID, LauncherSettings.Favorites.TITLE,
+                new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.TITLE,
                         LauncherSettings.Favorites.INTENT, LauncherSettings.Favorites.ITEM_TYPE },
                 null, null, null);
 
-        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ID);
+        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
         final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
         final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
         final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
@@ -725,7 +725,7 @@
                     LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
 
             try {
-                final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ID);
+                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);
@@ -1143,7 +1143,7 @@
     /**
      * Make an ApplicationInfo object for a sortcut
      */
-    private ApplicationInfo getApplicationInfoShortcut(Cursor c, Launcher launcher,
+    private ApplicationInfo getApplicationInfoShortcut(Cursor c, Context context,
             int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex) {
 
         final ApplicationInfo info = new ApplicationInfo();
@@ -1154,11 +1154,11 @@
             case LauncherSettings.Favorites.ICON_TYPE_RESOURCE:
                 String packageName = c.getString(iconPackageIndex);
                 String resourceName = c.getString(iconResourceIndex);
-                PackageManager packageManager = launcher.getPackageManager();
+                PackageManager packageManager = context.getPackageManager();
                 try {
                     Resources resources = packageManager.getResourcesForApplication(packageName);
                     final int id = resources.getIdentifier(resourceName, null, null);
-                    info.icon = Utilities.createIconThumbnail(resources.getDrawable(id), launcher);
+                    info.icon = Utilities.createIconThumbnail(resources.getDrawable(id), context);
                 } catch (Exception e) {
                     info.icon = packageManager.getDefaultActivityIcon();
                 }
@@ -1172,16 +1172,16 @@
                 try {
                     Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                     info.icon = new FastBitmapDrawable(
-                            Utilities.createBitmapThumbnail(bitmap, launcher));
+                            Utilities.createBitmapThumbnail(bitmap, context));
                 } catch (Exception e) {
-                    packageManager = launcher.getPackageManager();
+                    packageManager = context.getPackageManager();
                     info.icon = packageManager.getDefaultActivityIcon();
                 }
                 info.filtered = true;
                 info.customIcon = true;
                 break;
             default:
-                info.icon = launcher.getPackageManager().getDefaultActivityIcon();
+                info.icon = context.getPackageManager().getDefaultActivityIcon();
                 info.customIcon = false;
                 break;
         }
@@ -1326,6 +1326,26 @@
     }
 
     /**
+     * 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 boolean addGestureToDatabase(Context context, ItemInfo item, boolean notify) {
+        final ContentValues values = new ContentValues();
+        final ContentResolver cr = context.getContentResolver();
+
+        item.onAddToDatabase(values);
+
+        Uri result = cr.insert(notify ? LauncherSettings.Gestures.CONTENT_URI :
+                LauncherSettings.Gestures.CONTENT_URI_NO_NOTIFICATION, values);
+
+        if (result != null) {
+            item.id = Integer.parseInt(result.getPathSegments().get(1));
+        }
+
+        return result != null;
+    }
+
+    /**
      * Update an item to the database in a specified container.
      */
     static void updateItemInDatabase(Context context, ItemInfo item) {
@@ -1359,4 +1379,84 @@
         cr.delete(LauncherSettings.Favorites.CONTENT_URI,
                 LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
     }
+
+    static void deleteGestureFromDatabase(Context context, ItemInfo item) {
+        final ContentResolver cr = context.getContentResolver();
+
+        cr.delete(LauncherSettings.Gestures.getContentUri(item.id, false), null, null);
+    }
+
+    static void updateGestureInDatabase(Context context, ItemInfo item) {
+        final ContentValues values = new ContentValues();
+        final ContentResolver cr = context.getContentResolver();
+
+        item.onAddToDatabase(values);
+
+        cr.update(LauncherSettings.Gestures.getContentUri(item.id, false), values, null, null);
+    }
+
+
+    ApplicationInfo queryGesture(Context context, String id) {
+        final ContentResolver contentResolver = context.getContentResolver();
+        final PackageManager manager = context.getPackageManager();
+        final Cursor c = contentResolver.query(
+                LauncherSettings.Gestures.CONTENT_URI, null, LauncherSettings.Gestures._ID + "=?",
+                new String[] { id }, null);
+
+        ApplicationInfo info = null;
+
+        try {
+            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures._ID);
+            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.INTENT);
+            final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.TITLE);
+            final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.ICON_TYPE);
+            final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.ICON);
+            final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.ICON_PACKAGE);
+            final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.ICON_RESOURCE);
+            final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Gestures.ITEM_TYPE);
+
+            String intentDescription;
+            Intent intent;
+
+            if (c.moveToNext()) {
+                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.getIntent(intentDescription);
+                        } catch (java.net.URISyntaxException e) {
+                            return null;
+                        }
+
+                        if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
+                            info = getApplicationInfo(manager, intent, context);
+                        } else {
+                            info = getApplicationInfoShortcut(c, context, iconTypeIndex,
+                                    iconPackageIndex, iconResourceIndex, iconIndex);
+                        }
+
+                        if (info == null) {
+                            info = new ApplicationInfo();
+                            info.icon = manager.getDefaultActivityIcon();
+                        }
+
+                        info.isGesture = true;
+                        info.title = c.getString(titleIndex);
+                        info.intent = intent;
+                        info.id = c.getLong(idIndex);
+
+                        break;
+                }
+            }
+        } catch (Exception e) {
+            w(LOG_TAG, "Could not load gesture with name " + id);
+        } finally {
+            c.close();
+        }
+
+        return info;
+    }
 }
diff --git a/src/com/android/launcher/LauncherProvider.java b/src/com/android/launcher/LauncherProvider.java
index a27b746..ba8ebda 100644
--- a/src/com/android/launcher/LauncherProvider.java
+++ b/src/com/android/launcher/LauncherProvider.java
@@ -55,7 +55,7 @@
 
     private static final String DATABASE_NAME = "launcher.db";
     
-    private static final int DATABASE_VERSION = 3;
+    private static final int DATABASE_VERSION = 4;
 
     static final String AUTHORITY = "com.android.launcher.settings";
     
@@ -63,10 +63,11 @@
     static final String EXTRA_BIND_TARGETS = "com.android.launcher.settings.bindtargets";
 
     static final String TABLE_FAVORITES = "favorites";
+    static final String TABLE_GESTURES = "gestures";
     static final String PARAMETER_NOTIFY = "notify";
 
     /**
-     * {@link Uri} triggered at any registered {@link ContentObserver} when
+     * {@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.
      */
@@ -99,7 +100,7 @@
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         qb.setTables(args.table);
 
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
         result.setNotificationUri(getContext().getContentResolver(), uri);
 
@@ -220,6 +221,17 @@
                     "displayMode INTEGER" +
                     ");");
 
+            db.execSQL("CREATE TABLE gestures (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    "title TEXT," +
+                    "intent TEXT," +
+                    "itemType INTEGER," +
+                    "iconType INTEGER," +
+                    "iconPackage TEXT," +
+                    "iconResource TEXT," +
+                    "icon BLOB" +
+                    ");");
+
             // Database was just created, so wipe any previous widgets
             if (mAppWidgetHost != null) {
                 mAppWidgetHost.deleteHost();
@@ -270,7 +282,7 @@
         }
 
         private int copyFromCursor(SQLiteDatabase db, Cursor c) {
-            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ID);
+            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);
@@ -289,7 +301,7 @@
             int i = 0;
             while (c.moveToNext()) {
                 ContentValues values = new ContentValues(c.getColumnCount());
-                values.put(LauncherSettings.Favorites.ID, c.getLong(idIndex));
+                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));
@@ -352,6 +364,29 @@
                     convertWidgets(db);
                 }
             }
+
+            if (version < 4) {
+                db.beginTransaction();
+                try {
+                    db.execSQL("CREATE TABLE gestures (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "title TEXT," +
+                        "intent TEXT," +
+                        "itemType INTEGER," +
+                        "iconType INTEGER," +
+                        "iconPackage TEXT," +
+                        "iconResource TEXT," +
+                        "icon BLOB" +
+                        ");");
+                    db.setTransactionSuccessful();
+                    version = 4;
+                } catch (SQLException ex) {
+                    // Old version remains, which means we wipe old data
+                    Log.e(LOG_TAG, ex.getMessage(), ex);
+                } finally {
+                    db.endTransaction();
+                }
+            }
             
             if (version != DATABASE_VERSION) {
                 Log.w(LOG_TAG, "Destroying all old data.");
diff --git a/src/com/android/launcher/LauncherSettings.java b/src/com/android/launcher/LauncherSettings.java
index 60ea0df..062c8a6 100644
--- a/src/com/android/launcher/LauncherSettings.java
+++ b/src/com/android/launcher/LauncherSettings.java
@@ -23,11 +23,109 @@
  * 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#getIntent} 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";
+    }
+
+    static final class Gestures implements BaseLauncherColumns {
+                /**
+         * The content:// style URL for this table
+         */
+        static final Uri CONTENT_URI = Uri.parse("content://" +
+                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_GESTURES +
+                "?" + 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_GESTURES +
+                "?" + 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_GESTURES + "/" + id + "?" +
+                    LauncherProvider.PARAMETER_NOTIFY + "=" + notify);
+        }
+    }
+
     /**
      * Favorites. When changing these values, be sure to update
      * {@link com.android.settings.LauncherAppWidgetBinder} as needed.
      */
-    static final class Favorites implements BaseColumns {
+    static final class Favorites implements BaseLauncherColumns {
         /**
          * The content:// style URL for this table
          */
@@ -58,26 +156,6 @@
         }
 
         /**
-         * The row ID.
-         * <p>Type: INTEGER</p>
-         */
-        static final String ID = "_id";
-
-        /**
-         * Descriptive name of the favorite that can be displayed to the user.
-         * <P>Type: TEXT</P>
-         */
-        static final String TITLE = "title";
-
-        /**
-         * The Intent URL of the favorite, describing what it points to.  This
-         * value is given to {@link android.content.Intent#getIntent} to create
-         * an Intent that can be launched.
-         * <P>Type: TEXT</P>
-         */
-        static final String INTENT = "intent";
-
-        /**
          * The container holding the favorite
          * <P>Type: INTEGER</P>
          */
@@ -121,23 +199,6 @@
         static final String SPANY = "spanY";
 
         /**
-         * The type of the favorite
-         *
-         * <P>Type: INTEGER</P>
-         */
-        static final String ITEM_TYPE = "itemType";
-
-        /**
-         * The favorite is an application
-         */
-        static final int ITEM_TYPE_APPLICATION = 0;
-
-        /**
-         * The favorite is an application created shortcut
-         */
-        static final int ITEM_TYPE_SHORTCUT = 1;
-
-        /**
          * The favorite is a user created folder
          */
         static final int ITEM_TYPE_USER_FOLDER = 2;
@@ -180,43 +241,10 @@
          * value is 1, it is an application-created shortcut.
          * <P>Type: INTEGER</P>
          */
+        @Deprecated
         static final String IS_SHORTCUT = "isShortcut";
 
         /**
-         * 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";
-
-        /**
          * The URI associated with the favorite. It is used, for instance, by
          * live folders to find the content provider.
          * <P>Type: TEXT</P>
diff --git a/src/com/android/launcher/UninstallShortcutReceiver.java b/src/com/android/launcher/UninstallShortcutReceiver.java
index 334fbc2..bf71815 100644
--- a/src/com/android/launcher/UninstallShortcutReceiver.java
+++ b/src/com/android/launcher/UninstallShortcutReceiver.java
@@ -35,7 +35,7 @@
         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 },
+                new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT },
                 LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null);
 
             final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);