[2/2] OmniGears: lock screen shortcuts

custom shortcut apps on lockscreen
-config if camera and or dialer shortcut should be shown
-config to hide the informative text (custom shortcuts will
always hide the text if expanded)

Change-Id: I12f50a646725fa8bf626ce512ec1af3dd56a54fd
diff --git a/res/layout/app_item.xml b/res/layout/app_item.xml
new file mode 100644
index 0000000..6b3435f
--- /dev/null
+++ b/res/layout/app_item.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  Copyright (C) 2016 The OmniROM Project
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingTop="8dip"
+    android:paddingBottom="8dip"
+    android:orientation="horizontal" >
+
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="@android:dimen/app_icon_size"
+        android:layout_height="@android:dimen/app_icon_size"
+        android:layout_marginEnd="8dip"
+        android:layout_gravity="center_vertical"
+        android:scaleType="centerInside"
+        android:contentDescription="@null" />
+
+    <TextView
+        android:id="@+id/app_name"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:focusable="false"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textAlignment="viewStart" />
+
+</LinearLayout>
+
diff --git a/res/layout/shortcut_app_item.xml b/res/layout/shortcut_app_item.xml
new file mode 100644
index 0000000..bbe0e29
--- /dev/null
+++ b/res/layout/shortcut_app_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2013 The OmniROM Project
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:columnCount="3"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart" >
+
+    <org.omnirom.omnigears.ui.dslv.DragGripView
+        android:id="@+id/drag_handle"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:paddingBottom="12dp"
+        android:paddingTop="12dp" />
+
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="@android:dimen/app_icon_size"
+        android:layout_height="@android:dimen/app_icon_size"
+        android:layout_marginEnd="8dip"
+        android:layout_gravity="center_vertical"
+        android:scaleType="centerInside"
+        android:contentDescription="@null" />
+
+    <TextView
+        android:id="@+id/app_name"
+        android:layout_width="0dip"
+        android:layout_gravity="fill_horizontal|center_vertical"
+        android:ellipsize="end"
+        android:focusable="false"
+        android:singleLine="true"
+        android:textAlignment="viewStart"
+        android:textAppearance="?android:attr/textAppearanceListItem" />
+</GridLayout>
diff --git a/res/layout/shortcut_dialog.xml b/res/layout/shortcut_dialog.xml
new file mode 100644
index 0000000..e91de85
--- /dev/null
+++ b/res/layout/shortcut_dialog.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 The OmniROM Project
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="@dimen/alert_dialog_padding_material"
+    android:paddingEnd="@dimen/alert_dialog_padding_material" >
+    <org.omnirom.omnigears.ui.dslv.DragSortListView
+        android:id="@+id/shortcut_apps"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:dividerHeight="1dp" />
+</RelativeLayout>
diff --git a/res/values/custom_dimens.xml b/res/values/custom_dimens.xml
index beb265a..57af2c0 100755
--- a/res/values/custom_dimens.xml
+++ b/res/values/custom_dimens.xml
@@ -19,4 +19,6 @@
     <dimen name="alert_dialog_padding_material">20dp</dimen>
     <dimen name="color_preference_width">18dip</dimen>
     <dimen name="color_preference_height">18dip</dimen>
+    <dimen name="drag_grip_ridge_size">6dp</dimen>
+    <dimen name="drag_grip_ridge_gap">4dp</dimen>
 </resources>
diff --git a/res/values/custom_strings.xml b/res/values/custom_strings.xml
index a28c032..64b9bd2 100644
--- a/res/values/custom_strings.xml
+++ b/res/values/custom_strings.xml
@@ -212,7 +212,7 @@
     <!-- Lock screen config -->
     <string name="lockscreen_shortcut_title">Shortcuts</string>
     <string name="lockscreen_voice_shortcut_title">Voice assist shortcut</string>
-    <string name="lockscreen_voice_shortcut_summary">Show voice assist instead of dialer shortcut</string>
+    <string name="lockscreen_voice_shortcut_summary">Show voice assist instead of dialer</string>
     <string name="lockscreen_shortcuts_enable_title">Enable</string>
     <string name="lockscreen_clock_display_time_title">Display time</string>
     <string name="lockscreen_clock_display_date_title">Display date</string>
@@ -223,6 +223,17 @@
     <string name="lockscreen_clock_size_title">Time size</string>
     <string name="lockscreen_clock_enable_title">Enable</string>
     <string name="lockscreen_clock_shadow_title">Shadow</string>
+    <string name="lockscreen_shortcuts_title">Custom application shortcuts</string>
+    <string name="lockscreen_shortcuts_summary">Applications shown on the lock screen for quick access. Long press to hide.</string>
+    <string name="lockscreen_shortcut_add_dialog_title">Add shortcut</string>
+    <string name="lockscreen_shortcut_apps_title">Shortcuts</string>
+    <string name="lockscreen_shortcut_add">Add\u2026</string>
+    <string name="lockscreen_indicator_display_title">Show bottom status text</string>
+    <string name="lockscreen_indicator_display_summary">Note: custom shortcuts will hide text</string>
+    <string name="lockscreen_camera_shortcut_enable_title">Show camera shortcut</string>
+    <string name="lockscreen_camera_shortcut_enable_summary">Visibility depends on system setup</string>
+    <string name="lockscreen_left_shortcut_enable_title">Show dialer or voice assist shortcut</string>
+    <string name="lockscreen_left_shortcut_enable_summary">Visibility depends on system setup</string>
 
     <string name="notification_title">Notification panel</string>
     <string name="status_bar_custom_header_title">Custom header image</string>
diff --git a/res/values/dslv_attrs.xml b/res/values/dslv_attrs.xml
new file mode 100644
index 0000000..5e706d4
--- /dev/null
+++ b/res/values/dslv_attrs.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+Copyright 2012 Carl Bauer
+
+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.
+-->
+<resources>
+    <declare-styleable name="DragSortListView">
+        <attr name="collapsed_height" format="dimension" />
+        <attr name="drag_scroll_start" format="float" />
+        <attr name="max_drag_scroll_speed" format="float" />
+        <attr name="float_background_color" format="color" />
+        <attr name="remove_mode">
+            <enum name="clickRemove" value="0" />
+            <enum name="flingRight" value="1" />
+            <enum name="flingLeft" value="2" />
+            <enum name="slideRight" value="3" />
+            <enum name="slideLeft" value="4" />
+        </attr>
+        <attr name="track_drag_sort" format="boolean" />
+        <attr name="float_alpha" format="float" />
+        <attr name="slide_shuffle_speed" format="float" />
+        <attr name="remove_animation_duration" format="integer" />
+        <attr name="drop_animation_duration" format="integer" />
+        <attr name="drag_enabled" format="boolean" />
+        <attr name="sort_enabled" format="boolean" />
+        <attr name="remove_enabled" format="boolean" />
+        <attr name="drag_start_mode">
+            <enum name="onDown" value="0" />
+            <enum name="onMove" value="1" />
+            <enum name="onLongPress" value="2" />
+        </attr>
+        <attr name="drag_handle_id" format="integer" />
+        <attr name="click_remove_id" format="integer" />
+        <attr name="use_default_controller" format="boolean" />
+    </declare-styleable>
+</resources>
diff --git a/res/xml/lockscreen_settings.xml b/res/xml/lockscreen_settings.xml
index f639958..0cd4074 100644
--- a/res/xml/lockscreen_settings.xml
+++ b/res/xml/lockscreen_settings.xml
@@ -27,12 +27,43 @@
             android:defaultValue="true"/>
 
         <com.android.settings.preference.SecureCheckBoxPreference
+            android:key="lockscreen_camera_shortcut_enable"
+            android:title="@string/lockscreen_camera_shortcut_enable_title"
+            android:summary="@string/lockscreen_camera_shortcut_enable_summary"
+            android:defaultValue="true"
+            android:dependency="lockscreen_shortcuts_enable" />
+
+        <com.android.settings.preference.SecureCheckBoxPreference
+            android:key="lockscreen_left_shortcut_enable"
+            android:title="@string/lockscreen_left_shortcut_enable_title"
+            android:summary="@string/lockscreen_left_shortcut_enable_summary"
+            android:defaultValue="true"
+            android:dependency="lockscreen_shortcuts_enable" />
+
+        <com.android.settings.preference.SecureCheckBoxPreference
             android:key="lockscreen_voice_shortcut"
             android:title="@string/lockscreen_voice_shortcut_title"
             android:summary="@string/lockscreen_voice_shortcut_summary"
             android:defaultValue="true"
-            android:dependency="lockscreen_shortcuts_enable" />
+            android:dependency="lockscreen_left_shortcut_enable" />
 
+        <Preference
+            android:key="lockscreen_shortcuts"
+            android:title="@string/lockscreen_shortcuts_title"
+            android:summary="@string/lockscreen_shortcuts_summary"
+            android:persistent="false"
+            android:dependency="lockscreen_shortcuts_enable" />
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:key="lockscreen_other"
+        android:title="@string/other_category" >
+
+        <com.android.settings.preference.SystemSettingSwitchPreference
+            android:key="lockscreen_indicator_display"
+            android:title="@string/lockscreen_indicator_display_title"
+            android:summary="@string/lockscreen_indicator_display_summary"
+            android:defaultValue="true" />
     </PreferenceCategory>
 
     <PreferenceCategory
@@ -106,4 +137,5 @@
             android:dependency="lockscreen_clock_enable" />
 
     </PreferenceCategory>
+
 </PreferenceScreen>
diff --git a/src/org/omnirom/omnigears/interfacesettings/LockscreenSettings.java b/src/org/omnirom/omnigears/interfacesettings/LockscreenSettings.java
index 2bd3ceb..628f94c 100644
--- a/src/org/omnirom/omnigears/interfacesettings/LockscreenSettings.java
+++ b/src/org/omnirom/omnigears/interfacesettings/LockscreenSettings.java
@@ -32,6 +32,7 @@
 import android.preference.SwitchPreference;
 import android.provider.Settings;
 import android.provider.SearchIndexableResource;
+import android.text.TextUtils;
 import android.widget.Toast;
 
 import com.android.internal.logging.MetricsLogger;
@@ -40,12 +41,17 @@
 import com.android.settings.search.Indexable;
 
 import org.omnirom.omnigears.R;
+import org.omnirom.omnigears.preference.AppMultiSelectListPreference;
 import org.omnirom.omnigears.preference.FontPreference;
 import org.omnirom.omnigears.preference.NumberPickerPreference;
 import org.omnirom.omnigears.preference.ColorPickerPreference;
+import org.omnirom.omnigears.ui.ShortcutDialog;
 
-import java.util.List;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
 
 public class LockscreenSettings extends SettingsPreferenceFragment implements
         Preference.OnPreferenceChangeListener, Indexable {
@@ -60,6 +66,7 @@
     private static final String KEY_CLOCK_DISPLAY_TIME = "lockscreen_clock_display_time";
     private static final String KEY_CLOCK_DISPLAY_DATE = "lockscreen_clock_display_date";
     private static final String KEY_CLOCK_DISPLAY_ALARM = "lockscreen_clock_display_alarm";
+    private static final String KEY_SHORTCUTS = "lockscreen_shortcuts";
 
     private Preference mSetWallpaper;
     private Preference mClearWallpaper;
@@ -69,6 +76,7 @@
     private CheckBoxPreference mClockDisplayTime;
     private CheckBoxPreference mClockDisplayDate;
     private CheckBoxPreference mClockDisplayAlarm;
+    private Preference mShortcuts;
 
     @Override
     protected int getMetricsCategory() {
@@ -123,6 +131,8 @@
         mClockDisplayTime.setChecked((clockDisplay & Settings.System.LOCK_CLOCK_TIME) == Settings.System.LOCK_CLOCK_TIME);
         mClockDisplayDate.setChecked((clockDisplay & Settings.System.LOCK_CLOCK_DATE) == Settings.System.LOCK_CLOCK_DATE);
         mClockDisplayAlarm.setChecked((clockDisplay & Settings.System.LOCK_CLOCK_ALARM) == Settings.System.LOCK_CLOCK_ALARM);
+
+        mShortcuts = findPreference(KEY_SHORTCUTS);
     }
 
     @Override
@@ -148,6 +158,10 @@
             Settings.System.putInt(resolver,
                     Settings.System.LOCK_CLOCK_DISPLAY, getCurrentClockDisplayValue());
             return true;
+        } else if (preference == mShortcuts) {
+            ShortcutDialog d = new ShortcutDialog(getContext());
+            d.show();
+            return true;
         }
         return super.onPreferenceTreeClick(preferenceScreen, preference);
     }
diff --git a/src/org/omnirom/omnigears/preference/AppMultiSelectListPreference.java b/src/org/omnirom/omnigears/preference/AppMultiSelectListPreference.java
index 90efb74..545a25e 100644
--- a/src/org/omnirom/omnigears/preference/AppMultiSelectListPreference.java
+++ b/src/org/omnirom/omnigears/preference/AppMultiSelectListPreference.java
@@ -44,6 +44,7 @@
 
 import java.text.Collator;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
@@ -78,12 +79,12 @@
         Collections.sort(mPackageInfoList, sDisplayNameComparator);
     }
 
-    public void setValues(Set<String> values) {
+    public void setValues(Collection<String> values) {
         mValues.clear();
         mValues.addAll(values);
     }
 
-    public Set<String> getValues() {
+    public Collection<String> getValues() {
         return mValues;
     }
 
diff --git a/src/org/omnirom/omnigears/ui/ShortcutDialog.java b/src/org/omnirom/omnigears/ui/ShortcutDialog.java
new file mode 100644
index 0000000..3dc7726
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/ShortcutDialog.java
@@ -0,0 +1,447 @@
+/*
+ *  Copyright (C) 2016 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.omnigears.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.omnirom.omnigears.R;
+import org.omnirom.omnigears.ui.dslv.DragSortController;
+import org.omnirom.omnigears.ui.dslv.DragSortListView;
+
+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.List;
+import java.util.Set;
+import java.util.HashSet;
+
+public class ShortcutDialog extends AlertDialog implements
+        DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+    private LayoutInflater mInflater;
+    private List<String> mShortcutList;
+    private ShortcutListAdapter mShortcutAdapter;
+    private DragSortListView mShortcutConfigList;
+    private AlertDialog mAddShortcutDialog;
+    private final List<MyApplicationInfo> mPackageInfoList = new ArrayList<MyApplicationInfo>();
+
+    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT);
+
+    class MyApplicationInfo {
+        ApplicationInfo info;
+        CharSequence label;
+        ResolveInfo resolveInfo;
+    }
+
+    public class ShortcutListAdapter extends ArrayAdapter<String> {
+        public ShortcutListAdapter(Context context) {
+            super(context, R.layout.shortcut_app_item, mShortcutList);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            AppViewHolder holder = AppViewHolder.createOrRecycle(mInflater, convertView);
+            convertView = holder.rootView;
+            String intentString = getItem(position);
+            Intent intent = null;
+            try {
+                intent = Intent.parseUri(intentString, 0);
+            } catch (URISyntaxException e) {
+                // TODO should never happen - better build a intent list
+                return convertView;
+            }
+            final List<ResolveInfo> pkgAppsList = getContext().getPackageManager().queryIntentActivities(intent, 0);
+            if (pkgAppsList.size() > 0) {
+                Drawable icon = pkgAppsList.get(0).activityInfo.loadIcon(getContext().getPackageManager());
+                CharSequence label = pkgAppsList.get(0).activityInfo.loadLabel(getContext().getPackageManager());
+                holder.appName.setText(label);
+                holder.appIcon.setImageDrawable(icon);
+            }
+            return convertView;
+        }
+    }
+
+    private class ShortcutDragSortController extends DragSortController {
+
+        public ShortcutDragSortController() {
+            super(mShortcutConfigList, R.id.drag_handle,
+                    DragSortController.ON_DOWN,
+                    DragSortController.FLING_RIGHT_REMOVE);
+            setRemoveEnabled(true);
+            setSortEnabled(true);
+            setBackgroundColor(0x363636);
+        }
+
+        @Override
+        public void onDragFloatView(View floatView, Point floatPoint,
+                Point touchPoint) {
+            floatView.setLayoutParams(params);
+            mShortcutConfigList.setFloatAlpha(0.8f);
+        }
+
+        @Override
+        public View onCreateFloatView(int position) {
+            View v = mShortcutAdapter.getView(position, null,
+                    mShortcutConfigList);
+            v.setLayoutParams(params);
+            return v;
+        }
+
+        @Override
+        public void onDestroyFloatView(View floatView) {
+        }
+    }
+
+    public ShortcutDialog(Context context) {
+        super(context);
+        mInflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        List<ResolveInfo> installedAppsInfo = getContext().getPackageManager().queryIntentActivities(
+                mainIntent, 0);
+
+        for (ResolveInfo info : installedAppsInfo) {
+            MyApplicationInfo myInfo = new MyApplicationInfo();
+            myInfo.resolveInfo = info;
+            myInfo.label = getResolveInfoTitle(info);
+            mPackageInfoList.add(myInfo);
+        }
+        Collections.sort(mPackageInfoList, sDisplayNameComparator);
+
+        mShortcutList = new ArrayList<String>();
+        String shortcutStrings = Settings.Secure.getString(getContext().getContentResolver(), Settings.Secure.LOCK_SHORTCUTS);
+        if (shortcutStrings != null && shortcutStrings.length() != 0) {
+            String[] values = TextUtils.split(shortcutStrings, "##");
+
+            for (String intentString : values) {
+                Intent intent = null;
+                try {
+                    intent = Intent.parseUri(intentString, 0);
+                } catch (URISyntaxException e) {
+                    continue;
+                }
+
+                final List<ResolveInfo> pkgAppsList = getContext().getPackageManager().queryIntentActivities(intent, 0);
+                if (pkgAppsList.size() == 0) {
+                    continue;
+                }
+                mShortcutList.add(intentString);
+            }
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        final Context context = getContext();
+        final View view = getLayoutInflater().inflate(R.layout.shortcut_dialog,
+                null);
+        setView(view);
+        setTitle(R.string.lockscreen_shortcut_apps_title);
+        setCancelable(true);
+
+        setButton(DialogInterface.BUTTON_POSITIVE,
+                context.getString(android.R.string.ok), this);
+        setButton(DialogInterface.BUTTON_NEUTRAL,
+                context.getString(R.string.lockscreen_shortcut_add), this);
+        setButton(DialogInterface.BUTTON_NEGATIVE,
+                context.getString(android.R.string.cancel), this);
+
+        super.onCreate(savedInstanceState);
+
+        mShortcutConfigList = (DragSortListView) view
+                .findViewById(R.id.shortcut_apps);
+        mShortcutAdapter = new ShortcutListAdapter(context);
+        mShortcutConfigList.setAdapter(mShortcutAdapter);
+
+        final DragSortController dragSortController = new ShortcutDragSortController();
+        mShortcutConfigList.setFloatViewManager(dragSortController);
+        mShortcutConfigList
+                .setDropListener(new DragSortListView.DropListener() {
+                    @Override
+                    public void drop(int from, int to) {
+                        String intent = mShortcutList.remove(from);
+                        mShortcutList.add(to, intent);
+                        mShortcutAdapter.notifyDataSetChanged();
+                    }
+                });
+        mShortcutConfigList
+                .setRemoveListener(new DragSortListView.RemoveListener() {
+                    @Override
+                    public void remove(int which) {
+                        mShortcutList.remove(which);
+                        mShortcutAdapter.notifyDataSetChanged();
+                    }
+                });
+        mShortcutConfigList.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View view, MotionEvent motionEvent) {
+                return dragSortController.onTouch(view, motionEvent);
+            }
+        });
+        mShortcutConfigList.setItemsCanFocus(false);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        Button neutralButton = getButton(DialogInterface.BUTTON_NEUTRAL);
+        neutralButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showAddShortcutDialog();
+            }
+        });
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mAddShortcutDialog != null) {
+            mAddShortcutDialog.dismiss();
+        }
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        if (mAddShortcutDialog != null) {
+            mAddShortcutDialog = null;
+        }
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == DialogInterface.BUTTON_POSITIVE) {
+            Settings.Secure.putString(getContext().getContentResolver(), Settings.Secure.LOCK_SHORTCUTS,
+                    TextUtils.join("##", mShortcutList.toArray()));
+        } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+            cancel();
+        }
+    }
+
+    private String getResolveInfoTitle(ResolveInfo info) {
+        CharSequence label = info.loadLabel(getContext().getPackageManager());
+        if (label == null) label = info.activityInfo.name;
+        return label != null ? label.toString() : null;
+    }
+
+    private Intent getIntentForResolveInfo(ResolveInfo info, String action) {
+        Intent intent = new Intent(action);
+        ActivityInfo ai = info.activityInfo;
+        intent.setClassName(ai.packageName, ai.name);
+        return intent;
+    }
+
+    private void showAddShortcutDialog() {
+        if (mAddShortcutDialog != null && mAddShortcutDialog.isShowing()) {
+            return;
+        }
+
+        mAddShortcutDialog = new AddShortcutDialog(getContext());
+        mAddShortcutDialog.setOnDismissListener(this);
+        mAddShortcutDialog.show();
+    }
+
+    public void applyChanges(List<String> shortcutListEdit) {
+        mShortcutList.clear();
+        mShortcutList.addAll(shortcutListEdit);
+        mShortcutAdapter.notifyDataSetChanged();
+    }
+
+    private class AddShortcutDialog extends AlertDialog implements
+            DialogInterface.OnClickListener {
+        private AppListAdapter mAppAdapter;
+        private List<String> mShortcutListEdit;
+        private ListView mListView;
+
+        public class AppListAdapter extends ArrayAdapter<MyApplicationInfo> {
+            public AppListAdapter(Context context) {
+                super(context, R.layout.app_select_item, mPackageInfoList);
+            }
+
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                AppSelectViewHolder holder = AppSelectViewHolder.createOrRecycle(mInflater, convertView);
+                convertView = holder.rootView;
+                MyApplicationInfo info = getItem(position);
+                holder.appName.setText(info.label);
+                Drawable icon = info.resolveInfo.loadIcon(getContext().getPackageManager());
+                if (icon != null) {
+                    holder.appIcon.setImageDrawable(icon);
+                } else {
+                    holder.appIcon.setImageDrawable(null);
+                }
+
+                Intent intent = getIntentForResolveInfo(info.resolveInfo, Intent.ACTION_MAIN);
+                intent.addCategory(Intent.CATEGORY_LAUNCHER);
+                String value = intent.toUri(0).toString();
+                holder.checkBox.setChecked(mShortcutListEdit.contains(value));
+                return convertView;
+            }
+        }
+
+        protected AddShortcutDialog(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                applyChanges(mShortcutListEdit);
+            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                cancel();
+            }
+        }
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            final Context context = getContext();
+            final View view = getLayoutInflater().inflate(
+                    R.layout.preference_app_list, null);
+            setView(view);
+            setTitle(R.string.lockscreen_shortcut_add_dialog_title);
+            setCancelable(true);
+
+            setButton(DialogInterface.BUTTON_POSITIVE,
+                    context.getString(android.R.string.ok), this);
+            setButton(DialogInterface.BUTTON_NEGATIVE,
+                    context.getString(android.R.string.cancel), this);
+
+            super.onCreate(savedInstanceState);
+            mShortcutListEdit = new ArrayList<String>();
+            mShortcutListEdit.addAll(mShortcutList);
+            mListView = (ListView) view.findViewById(R.id.app_list);
+            mAppAdapter = new AppListAdapter(getContext());
+            mListView.setAdapter(mAppAdapter);
+            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+                @Override
+                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                    final AppSelectViewHolder holder = (AppSelectViewHolder) view.getTag();
+                    final boolean isChecked = !holder.checkBox.isChecked();
+                    holder.checkBox.setChecked(isChecked);
+
+                    MyApplicationInfo myInfo = mAppAdapter.getItem(position);
+                    ResolveInfo info = myInfo.resolveInfo;
+                    Intent intent = getIntentForResolveInfo(info, Intent.ACTION_MAIN);
+                    intent.addCategory(Intent.CATEGORY_LAUNCHER);
+                    String value = intent.toUri(0).toString();
+
+                    if (isChecked) {
+                        mShortcutListEdit.add(value);
+                    } else {
+                        mShortcutListEdit.remove(value);
+                    }
+                }
+            });
+        }
+    }
+
+    public static class AppSelectViewHolder {
+        public View rootView;
+        public TextView appName;
+        public ImageView appIcon;
+        public CheckBox checkBox;
+
+        public static AppSelectViewHolder createOrRecycle(LayoutInflater inflater, View convertView) {
+            if (convertView == null) {
+                convertView = inflater.inflate(R.layout.app_select_item, null);
+
+                // Creates a ViewHolder and store references to the two children views
+                // we want to bind data to.
+                AppSelectViewHolder holder = new AppSelectViewHolder();
+                holder.rootView = convertView;
+                holder.appName = (TextView) convertView.findViewById(R.id.app_name);
+                holder.appIcon = (ImageView) convertView.findViewById(R.id.app_icon);
+                holder.checkBox = (CheckBox) convertView.findViewById(android.R.id.checkbox);
+                convertView.setTag(holder);
+                return holder;
+            } else {
+                // Get the ViewHolder back to get fast access to the TextView
+                // and the ImageView.
+                return (AppSelectViewHolder)convertView.getTag();
+            }
+        }
+    }
+
+    public static class AppViewHolder {
+        public View rootView;
+        public TextView appName;
+        public ImageView appIcon;
+
+        public static AppViewHolder createOrRecycle(LayoutInflater inflater, View convertView) {
+            if (convertView == null) {
+                convertView = inflater.inflate(R.layout.shortcut_app_item, null);
+
+                // Creates a ViewHolder and store references to the two children views
+                // we want to bind data to.
+                AppViewHolder holder = new AppViewHolder();
+                holder.rootView = convertView;
+                holder.appName = (TextView) convertView.findViewById(R.id.app_name);
+                holder.appIcon = (ImageView) convertView.findViewById(R.id.app_icon);
+                convertView.setTag(holder);
+                return holder;
+            } else {
+                // Get the ViewHolder back to get fast access to the TextView
+                // and the ImageView.
+                return (AppViewHolder)convertView.getTag();
+            }
+        }
+    }
+
+    private final static Comparator<MyApplicationInfo> sDisplayNameComparator
+            = new Comparator<MyApplicationInfo>() {
+
+        private final Collator collator = Collator.getInstance();
+
+        public final int compare(MyApplicationInfo a, MyApplicationInfo b) {
+            return collator.compare(a.label, b.label);
+        }
+    };
+}
diff --git a/src/org/omnirom/omnigears/ui/dslv/DragGripView.java b/src/org/omnirom/omnigears/ui/dslv/DragGripView.java
new file mode 100644
index 0000000..8922035
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/DragGripView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.omnirom.omnigears.ui.dslv;
+
+import org.omnirom.omnigears.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+
+public class DragGripView extends View {
+    private static final int[] ATTRS = new int[]{
+            android.R.attr.gravity,
+            android.R.attr.color,
+    };
+
+    private static final int HORIZ_RIDGES = 3;
+
+    private int mGravity = Gravity.START;
+    private int mColor = Color.BLACK;
+
+    private Paint mRidgePaint;
+
+    private float mRidgeSize;
+    private float mRidgeGap;
+
+    private int mWidth;
+    private int mHeight;
+
+    public DragGripView(Context context) {
+        this(context, null, 0);
+    }
+
+    public DragGripView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DragGripView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+        mGravity = a.getInteger(0, mGravity);
+        mColor = a.getColor(1, mColor);
+        a.recycle();
+
+        TypedValue value = new TypedValue();
+        getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
+        mColor = getContext().getColor(value.resourceId);
+
+        final Resources res = getResources();
+        mRidgeSize = res.getDimensionPixelSize(R.dimen.drag_grip_ridge_size);
+        mRidgeGap = res.getDimensionPixelSize(R.dimen.drag_grip_ridge_gap);
+
+        mRidgePaint = new Paint();
+        mRidgePaint.setColor(mColor);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(
+                View.resolveSize(
+                        (int) (HORIZ_RIDGES * (mRidgeSize + mRidgeGap) - mRidgeGap)
+                                + getPaddingLeft() + getPaddingRight(),
+                        widthMeasureSpec),
+                View.resolveSize(
+                        (int) mRidgeSize,
+                        heightMeasureSpec));
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        float drawWidth = HORIZ_RIDGES * (mRidgeSize + mRidgeGap) - mRidgeGap;
+        float drawLeft;
+
+        switch (Gravity.getAbsoluteGravity(mGravity, getLayoutDirection())
+                & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.CENTER_HORIZONTAL:
+                drawLeft = getPaddingLeft()
+                        + ((mWidth - getPaddingLeft() - getPaddingRight()) - drawWidth) / 2;
+                break;
+            case Gravity.RIGHT:
+                drawLeft = getWidth() - getPaddingRight() - drawWidth;
+                break;
+            default:
+                drawLeft = getPaddingLeft();
+        }
+
+        int vertRidges = (int) ((mHeight - getPaddingTop() - getPaddingBottom() + mRidgeGap)
+                / (mRidgeSize + mRidgeGap));
+        float drawHeight = vertRidges * (mRidgeSize + mRidgeGap) - mRidgeGap;
+        float drawTop = getPaddingTop()
+                + ((mHeight - getPaddingTop() - getPaddingBottom()) - drawHeight) / 2;
+
+        for (int y = 0; y < vertRidges; y++) {
+            for (int x = 0; x < HORIZ_RIDGES; x++) {
+                canvas.drawRect(
+                        drawLeft + x * (mRidgeSize + mRidgeGap),
+                        drawTop + y * (mRidgeSize + mRidgeGap),
+                        drawLeft + x * (mRidgeSize + mRidgeGap) + mRidgeSize,
+                        drawTop + y * (mRidgeSize + mRidgeGap) + mRidgeSize,
+                        mRidgePaint);
+            }
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mHeight = h;
+        mWidth = w;
+    }
+}
diff --git a/src/org/omnirom/omnigears/ui/dslv/DragSortController.java b/src/org/omnirom/omnigears/ui/dslv/DragSortController.java
new file mode 100644
index 0000000..a456923
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/DragSortController.java
@@ -0,0 +1,489 @@
+/**
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 org.omnirom.omnigears.ui.dslv;
+
+import android.graphics.Point;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.AdapterView;
+
+/**
+ * Class that starts and stops item drags on a {@link DragSortListView}
+ * based on touch gestures. This class also inherits from
+ * {@link SimpleFloatViewManager}, which provides basic float View
+ * creation.
+ *
+ * An instance of this class is meant to be passed to the methods
+ * {@link DragSortListView#setTouchListener()} and
+ * {@link DragSortListView#setFloatViewManager()} of your
+ * {@link DragSortListView} instance.
+ */
+public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener {
+
+    /**
+     * Drag init mode enum.
+     */
+    public static final int ON_DOWN = 0;
+    public static final int ON_DRAG = 1;
+    public static final int ON_LONG_PRESS = 2;
+
+    private int mDragInitMode = ON_DOWN;
+
+    private boolean mSortEnabled = true;
+
+    /**
+     * Remove mode enum.
+     */
+    public static final int CLICK_REMOVE = 0;
+    public static final int FLING_RIGHT_REMOVE = 1;
+    public static final int FLING_LEFT_REMOVE = 2;
+    public static final int SLIDE_RIGHT_REMOVE = 3;
+    public static final int SLIDE_LEFT_REMOVE = 4;
+
+    /**
+     * The current remove mode.
+     */
+    private int mRemoveMode;
+
+    private boolean mRemoveEnabled = false;
+
+    private GestureDetector mDetector;
+
+    private GestureDetector mFlingRemoveDetector;
+
+    private int mTouchSlop;
+
+    public static final int MISS = -1;
+
+    private int mHitPos = MISS;
+
+    private int mClickRemoveHitPos = MISS;
+
+    private int[] mTempLoc = new int[2];
+
+    private int mItemX;
+    private int mItemY;
+
+    private int mCurrX;
+    private int mCurrY;
+
+    private boolean mDragging = false;
+
+    private float mFlingSpeed = 500f;
+
+    private float mOrigFloatAlpha = 1.0f;
+
+    private int mDragHandleId;
+
+    private int mClickRemoveId;
+
+    private DragSortListView mDslv;
+
+
+    /**
+     * Calls {@link #DragSortController(DragSortListView, int)} with a
+     * 0 drag handle id, FLING_RIGHT_REMOVE remove mode,
+     * and ON_DOWN drag init. By default, sorting is enabled, and
+     * removal is disabled.
+     *
+     * @param dslv The DSLV instance
+     */
+    public DragSortController(DragSortListView dslv) {
+        this(dslv, 0, ON_DOWN, FLING_RIGHT_REMOVE);
+    }
+
+    public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
+        this(dslv, dragHandleId, dragInitMode, removeMode, 0);
+    }
+
+    /**
+     * By default, sorting is enabled, and removal is disabled.
+     *
+     * @param dslv The DSLV instance
+     * @param dragHandleId The resource id of the View that represents
+     * the drag handle in a list item.
+     */
+    public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) {
+        super(dslv);
+        mDslv = dslv;
+        mDetector = new GestureDetector(dslv.getContext(), this);
+        mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
+        mFlingRemoveDetector.setIsLongpressEnabled(false);
+        mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
+        mDragHandleId = dragHandleId;
+        mClickRemoveId = clickRemoveId;
+        setRemoveMode(removeMode);
+        setDragInitMode(dragInitMode);
+        mOrigFloatAlpha = dslv.getFloatAlpha();
+    }
+
+
+    public int getDragInitMode() {
+        return mDragInitMode;
+    }
+
+    /**
+     * Set how a drag is initiated. Needs to be one of
+     * {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
+     *
+     * @param mode The drag init mode.
+     */
+    public void setDragInitMode(int mode) {
+        mDragInitMode = mode;
+    }
+
+    /**
+     * ROMAN'S ADDITION. Returns whether or not a drag is currently occurring.
+     */
+    public boolean isDragging() {
+        return mDragging;
+    }
+
+    /**
+     * Enable/Disable list item sorting. Disabling is useful if only item
+     * removal is desired. Prevents drags in the vertical direction.
+     *
+     * @param enabled Set <code>true</code> to enable list
+     * item sorting.
+     */
+    public void setSortEnabled(boolean enabled) {
+        mSortEnabled = enabled;
+    }
+
+    public boolean isSortEnabled() {
+        return mSortEnabled;
+    }
+
+    /**
+     * One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
+     * {@link FLING_LEFT_REMOVE},
+     * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
+     */
+    public void setRemoveMode(int mode) {
+        mRemoveMode = mode;
+    }
+
+    public int getRemoveMode() {
+        return mRemoveMode;
+    }
+
+    /**
+     * Enable/Disable item removal without affecting remove mode.
+     */
+    public void setRemoveEnabled(boolean enabled) {
+        mRemoveEnabled = enabled;
+    }
+
+    public boolean isRemoveEnabled() {
+        return mRemoveEnabled;
+    }
+
+    /**
+     * Set the resource id for the View that represents the drag
+     * handle in a list item.
+     *
+     * @param id An android resource id.
+     */
+    public void setDragHandleId(int id) {
+        mDragHandleId = id;
+    }
+
+    /**
+     * Set the resource id for the View that represents click
+     * removal button.
+     *
+     * @param id An android resource id.
+     */
+    public void setClickRemoveId(int id) {
+        mClickRemoveId = id;
+    }
+
+
+    /**
+     * Sets flags to restrict certain motions of the floating View
+     * based on DragSortController settings (such as remove mode).
+     * Starts the drag on the DragSortListView.
+     *
+     * @param position The list item position (includes headers).
+     * @param deltaX Touch x-coord minus left edge of floating View.
+     * @param deltaY Touch y-coord minus top edge of floating View.
+     *
+     * @return True if drag started, false otherwise.
+     */
+     public boolean startDrag(int position, int deltaX, int deltaY) {
+
+        int dragFlags = 0;
+        if (mSortEnabled) {
+            dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
+            //dragFlags |= DRAG_POS_Y; //for fun
+        }
+        if (mRemoveEnabled) {
+            if (mRemoveMode == FLING_RIGHT_REMOVE) {
+                dragFlags |= DragSortListView.DRAG_POS_X;
+            } else if (mRemoveMode == FLING_LEFT_REMOVE) {
+                dragFlags |= DragSortListView.DRAG_NEG_X;
+            }
+        }
+
+        mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX, deltaY);
+        return mDragging;
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        mDetector.onTouchEvent(ev);
+        if (mRemoveEnabled && mDragging && (mRemoveMode == FLING_RIGHT_REMOVE || mRemoveMode == FLING_LEFT_REMOVE)) {
+            mFlingRemoveDetector.onTouchEvent(ev);
+        }
+
+        int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        switch (action) {
+        case MotionEvent.ACTION_DOWN:
+            mCurrX = (int) ev.getX();
+            mCurrY = (int) ev.getY();
+            break;
+        case MotionEvent.ACTION_UP:
+            if (mRemoveEnabled) {
+                final int x = (int) ev.getX();
+                int thirdW = mDslv.getWidth() / 3;
+                int twoThirdW = mDslv.getWidth() - thirdW;
+                if ((mRemoveMode == SLIDE_RIGHT_REMOVE && x > twoThirdW) ||
+                        (mRemoveMode == SLIDE_LEFT_REMOVE && x < thirdW)) {
+                    mDslv.stopDrag(true);
+                }
+            }
+        case MotionEvent.ACTION_CANCEL:
+            mDragging = false;
+            break;
+        }
+
+        return false;
+    }
+
+    /**
+     * Overrides to provide fading when slide removal is enabled.
+     */
+    @Override
+    public void onDragFloatView(View floatView, Point position, Point touch) {
+
+        if (mRemoveEnabled) {
+            int x = touch.x;
+
+            if (mRemoveMode == SLIDE_RIGHT_REMOVE) {
+                int width = mDslv.getWidth();
+                int thirdWidth = width / 3;
+
+                float alpha;
+                if (x < thirdWidth) {
+                    alpha = 1.0f;
+                } else if (x < width - thirdWidth) {
+                    alpha = ((float) (width - thirdWidth - x)) / ((float) thirdWidth);
+                } else {
+                    alpha = 0.0f;
+                }
+                mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+            } else if (mRemoveMode == SLIDE_LEFT_REMOVE) {
+                int width = mDslv.getWidth();
+                int thirdWidth = width / 3;
+
+                float alpha;
+                if (x < thirdWidth) {
+                    alpha = 0.0f;
+                } else if (x < width - thirdWidth) {
+                    alpha = ((float) (x - thirdWidth)) / ((float) thirdWidth);
+                } else {
+                    alpha = 1.0f;
+                }
+                mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+            }
+        }
+    }
+
+    /**
+     * Get the position to start dragging based on the ACTION_DOWN
+     * MotionEvent. This function simply calls
+     * {@link #dragHandleHitPosition(MotionEvent)}. Override
+     * to change drag handle behavior;
+     * this function is called internally when an ACTION_DOWN
+     * event is detected.
+     *
+     * @param ev The ACTION_DOWN MotionEvent.
+     *
+     * @return The list position to drag if a drag-init gesture is
+     * detected; MISS if unsuccessful.
+     */
+    public int startDragPosition(MotionEvent ev) {
+        return dragHandleHitPosition(ev);
+    }
+
+    /**
+     * Checks for the touch of an item's drag handle (specified by
+     * {@link #setDragHandleId(int)}), and returns that item's position
+     * if a drag handle touch was detected.
+     *
+     * @param ev The ACTION_DOWN MotionEvent.
+
+     * @return The list position of the item whose drag handle was
+     * touched; MISS if unsuccessful.
+     */
+    public int dragHandleHitPosition(MotionEvent ev) {
+        return viewIdHitPosition(ev, mDragHandleId);
+    }
+
+    public int viewIdHitPosition(MotionEvent ev, int id) {
+        final int x = (int) ev.getX();
+        final int y = (int) ev.getY();
+
+        int touchPos = mDslv.pointToPosition(x, y); //includes headers/footers
+
+        final int numHeaders = mDslv.getHeaderViewsCount();
+        final int numFooters = mDslv.getFooterViewsCount();
+        final int count = mDslv.getCount();
+
+        //Log.d("mobeta", "touch down on position " + itemnum);
+        // We're only interested if the touch was on an
+        // item that's not a header or footer.
+        if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders && touchPos < (count - numFooters)) {
+            final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
+            final int rawX = (int) ev.getRawX();
+            final int rawY = (int) ev.getRawY();
+
+            //View dragBox = (View) item.getTag();
+            View dragBox = (View) item.findViewById(id);
+            if (dragBox != null) {
+                dragBox.getLocationOnScreen(mTempLoc);
+
+                if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
+                        rawX < mTempLoc[0] + dragBox.getWidth() &&
+                        rawY < mTempLoc[1] + dragBox.getHeight()) {
+
+                    mItemX = item.getLeft();
+                    mItemY = item.getTop();
+
+                    return touchPos;
+                }
+            }
+        }
+
+        return MISS;
+    }
+
+    @Override
+    public boolean onDown(MotionEvent ev) {
+        if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+            mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
+        }
+
+        mHitPos = startDragPosition(ev);
+        if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
+            startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        //Log.d("mobeta", "lift listener scrolled dX="+distanceX+" dY="+distanceY);
+
+        if (mHitPos != MISS && mDragInitMode == ON_DRAG && !mDragging) {
+            final int x1 = (int) e1.getX();
+            final int y1 = (int) e1.getY();
+            final int x2 = (int) e2.getX();
+            final int y2 = (int) e2.getY();
+
+            boolean start = false;
+            if (mRemoveEnabled && mSortEnabled) {
+                start = true;
+            } else if (mRemoveEnabled) {
+                start = Math.abs(x2 - x1) > mTouchSlop;
+            } else if (mSortEnabled) {
+                start = Math.abs(y2 - y1) > mTouchSlop;
+            }
+
+            if (start) {
+                startDrag(mHitPos, x2 - mItemX, y2 - mItemY);
+            }
+        }
+        // return whatever
+        return false;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+        //Log.d("mobeta", "lift listener long pressed");
+        if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
+            mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
+        }
+    }
+
+    // complete the OnGestureListener interface
+    @Override
+    public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        return false;
+    }
+
+    // complete the OnGestureListener interface
+    @Override
+    public boolean onSingleTapUp(MotionEvent ev) {
+        if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+            if (mClickRemoveHitPos != MISS) {
+                mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
+            }
+        }
+        return true;
+    }
+
+    // complete the OnGestureListener interface
+    @Override
+    public void onShowPress(MotionEvent ev) {
+        // do nothing
+    }
+
+    private GestureDetector.OnGestureListener mFlingRemoveListener =
+            new GestureDetector.SimpleOnGestureListener() {
+                @Override
+                public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+                    //Log.d("mobeta", "on fling remove called");
+                    if (mRemoveEnabled) {
+                        switch (mRemoveMode) {
+                        case FLING_RIGHT_REMOVE:
+                            if (velocityX > mFlingSpeed) {
+                                mDslv.stopDrag(true);
+                            }
+                            break;
+                        case FLING_LEFT_REMOVE:
+                            if (velocityX < -mFlingSpeed) {
+                                mDslv.stopDrag(true);
+                            }
+                            break;
+                        }
+                    }
+                    return false;
+                }
+            };
+
+}
+
diff --git a/src/org/omnirom/omnigears/ui/dslv/DragSortCursorAdapter.java b/src/org/omnirom/omnigears/ui/dslv/DragSortCursorAdapter.java
new file mode 100644
index 0000000..31b3f11
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/DragSortCursorAdapter.java
@@ -0,0 +1,261 @@
+/**
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 org.omnirom.omnigears.ui.dslv;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+
+
+/**
+ * A subclass of {@link android.widget.CursorAdapter} that provides
+ * reordering of the elements in the Cursor based on completed
+ * drag-sort operations. The reordering is a simple mapping of
+ * list positions into Cursor positions (the Cursor is unchanged).
+ * To persist changes made by drag-sorts, one can retrieve the
+ * mapping with the {@link #getCursorPositions()} method, which
+ * returns the reordered list of Cursor positions.
+ *
+ * An instance of this class is passed
+ * to {@link DragSortListView#setAdapter(ListAdapter)} and, since
+ * this class implements the {@link DragSortListView.DragSortListener}
+ * interface, it is automatically set as the DragSortListener for
+ * the DragSortListView instance.
+ */
+public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
+
+    public static final int REMOVED = -1;
+
+    /**
+     * Key is ListView position, value is Cursor position
+     */
+    private SparseIntArray mListMapping = new SparseIntArray();
+
+    private ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
+
+    @SuppressWarnings("deprecation")
+    public DragSortCursorAdapter(Context context, Cursor c) {
+        super(context, c);
+    }
+
+    public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) {
+        super(context, c, autoRequery);
+    }
+
+    public DragSortCursorAdapter(Context context, Cursor c, int flags) {
+        super(context, c, flags);
+    }
+
+    /**
+     * Swaps Cursor and clears list-Cursor mapping.
+     *
+     * @see android.widget.CursorAdapter#swapCursor(android.database.Cursor)
+     */
+    @Override
+    public Cursor swapCursor(Cursor newCursor) {
+        Cursor old = super.swapCursor(newCursor);
+        resetMappings();
+        return old;
+    }
+
+    /**
+     * Changes Cursor and clears list-Cursor mapping.
+     *
+     * @see android.widget.CursorAdapter#changeCursor(android.database.Cursor)
+     */
+    @Override
+    public void changeCursor(Cursor cursor) {
+        super.changeCursor(cursor);
+        resetMappings();
+    }
+
+    /**
+     * Resets list-cursor mapping.
+     */
+    public void reset() {
+        resetMappings();
+        notifyDataSetChanged();
+    }
+
+    private void resetMappings() {
+        mListMapping.clear();
+        mRemovedCursorPositions.clear();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return super.getItem(mListMapping.get(position, position));
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return super.getItemId(mListMapping.get(position, position));
+    }
+
+    @Override
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        return super.getDropDownView(mListMapping.get(position, position), convertView, parent);
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        return super.getView(mListMapping.get(position, position), convertView, parent);
+    }
+
+    /**
+     * On drop, this updates the mapping between Cursor positions
+     * and ListView positions. The Cursor is unchanged. Retrieve
+     * the current mapping with {@link getCursorPositions()}.
+     *
+     * @see DragSortListView.DropListener#drop(int, int)
+     */
+    @Override
+    public void drop(int from, int to) {
+        if (from != to) {
+            int cursorFrom = mListMapping.get(from, from);
+
+            if (from > to) {
+                for (int i = from; i > to; --i) {
+                    mListMapping.put(i, mListMapping.get(i - 1, i - 1));
+                }
+            } else {
+                for (int i = from; i < to; ++i) {
+                    mListMapping.put(i, mListMapping.get(i + 1, i + 1));
+                }
+            }
+            mListMapping.put(to, cursorFrom);
+
+            cleanMapping();
+            notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * On remove, this updates the mapping between Cursor positions
+     * and ListView positions. The Cursor is unchanged. Retrieve
+     * the current mapping with {@link getCursorPositions()}.
+     *
+     * @see DragSortListView.RemoveListener#remove(int)
+     */
+    @Override
+    public void remove(int which) {
+        int cursorPos = mListMapping.get(which, which);
+        if (!mRemovedCursorPositions.contains(cursorPos)) {
+            mRemovedCursorPositions.add(cursorPos);
+        }
+
+        int newCount = getCount();
+        for (int i = which; i < newCount; ++i) {
+            mListMapping.put(i, mListMapping.get(i + 1, i + 1));
+        }
+
+        mListMapping.delete(newCount);
+
+        cleanMapping();
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Does nothing. Just completes DragSortListener interface.
+     */
+    @Override
+    public void drag(int from, int to) {
+        // do nothing
+    }
+
+    /**
+     * Remove unnecessary mappings from sparse array.
+     */
+    private void cleanMapping() {
+        ArrayList<Integer> toRemove = new ArrayList<Integer>();
+
+        int size = mListMapping.size();
+        for (int i = 0; i < size; ++i) {
+            if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) {
+                toRemove.add(mListMapping.keyAt(i));
+            }
+        }
+
+        size = toRemove.size();
+        for (int i = 0; i < size; ++i) {
+            mListMapping.delete(toRemove.get(i));
+        }
+    }
+
+    @Override
+    public int getCount() {
+        return super.getCount() - mRemovedCursorPositions.size();
+    }
+
+    /**
+     * Get the Cursor position mapped to by the provided list position
+     * (given all previously handled drag-sort
+     * operations).
+     *
+     * @param position List position
+     *
+     * @return The mapped-to Cursor position
+     */
+    public int getCursorPosition(int position) {
+        return mListMapping.get(position, position);
+    }
+
+    /**
+     * Get the current order of Cursor positions presented by the
+     * list.
+     */
+    public ArrayList<Integer> getCursorPositions() {
+        ArrayList<Integer> result = new ArrayList<Integer>();
+
+        for (int i = 0; i < getCount(); ++i) {
+            result.add(mListMapping.get(i, i));
+        }
+
+        return result;
+    }
+
+    /**
+     * Get the list position mapped to by the provided Cursor position.
+     * If the provided Cursor position has been removed by a drag-sort,
+     * this returns {@link #REMOVED}.
+     *
+     * @param cursorPosition A Cursor position
+     * @return The mapped-to list position or REMOVED
+     */
+    public int getListPosition(int cursorPosition) {
+        if (mRemovedCursorPositions.contains(cursorPosition)) {
+            return REMOVED;
+        }
+
+        int index = mListMapping.indexOfValue(cursorPosition);
+        if (index < 0) {
+            return cursorPosition;
+        } else {
+            return mListMapping.keyAt(index);
+        }
+    }
+
+
+}
diff --git a/src/org/omnirom/omnigears/ui/dslv/DragSortItemView.java b/src/org/omnirom/omnigears/ui/dslv/DragSortItemView.java
new file mode 100644
index 0000000..d837eb5
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/DragSortItemView.java
@@ -0,0 +1,114 @@
+/**
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 org.omnirom.omnigears.ui.dslv;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+
+/**
+ * Lightweight ViewGroup that wraps list items obtained from user's
+ * ListAdapter. ItemView expects a single child that has a definite
+ * height (i.e. the child's layout height is not MATCH_PARENT).
+ * The width of
+ * ItemView will always match the width of its child (that is,
+ * the width MeasureSpec given to ItemView is passed directly
+ * to the child, and the ItemView measured width is set to the
+ * child's measured width). The height of ItemView can be anything;
+ *
+ * The purpose of this class is to optimize slide
+ * shuffle animations.
+ */
+public class DragSortItemView extends ViewGroup {
+
+    private int mGravity = Gravity.TOP;
+
+    public DragSortItemView(Context context) {
+        super(context);
+
+        // always init with standard ListView layout params
+        setLayoutParams(new AbsListView.LayoutParams(
+                LayoutParams.MATCH_PARENT,
+                LayoutParams.WRAP_CONTENT));
+
+        setClipChildren(true);
+    }
+
+    public void setGravity(int gravity) {
+        mGravity = gravity;
+    }
+
+    public int getGravity() {
+        return mGravity;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final View child = getChildAt(0);
+
+        if (child == null) {
+            return;
+        }
+
+        if (mGravity == Gravity.TOP) {
+            child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight());
+        } else {
+            child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight());
+        }
+    }
+
+    /**
+     *
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        final View child = getChildAt(0);
+        if (child == null) {
+            setMeasuredDimension(0, width);
+            return;
+        }
+
+        if (child.isLayoutRequested()) {
+            // Always let child be as tall as it wants.
+            measureChild(child, widthMeasureSpec,
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+        }
+
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            LayoutParams lp = getLayoutParams();
+
+            if (lp.height > 0) {
+                height = lp.height;
+            } else {
+                height = child.getMeasuredHeight();
+            }
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+}
diff --git a/src/org/omnirom/omnigears/ui/dslv/DragSortListView.java b/src/org/omnirom/omnigears/ui/dslv/DragSortListView.java
new file mode 100644
index 0000000..9a83410
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/DragSortListView.java
@@ -0,0 +1,2638 @@
+/*
+ * DragSortListView.
+ *
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 org.omnirom.omnigears.ui.dslv;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.omnirom.omnigears.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+
+/**
+ * ListView subclass that mediates drag and drop resorting of items.
+ *
+ *
+ * @author heycosmo
+ *
+ */
+public class DragSortListView extends ListView {
+    /**
+     * The View that floats above the ListView and represents
+     * the dragged item.
+     */
+    private View mFloatView;
+
+    /**
+     * The float View location. First based on touch location
+     * and given deltaX and deltaY. Then restricted by callback
+     * to FloatViewManager.onDragFloatView(). Finally restricted
+     * by bounds of DSLV.
+     */
+    private Point mFloatLoc = new Point();
+
+    private Point mTouchLoc = new Point();
+
+    /**
+     * The middle (in the y-direction) of the floating View.
+     */
+    private int mFloatViewMid;
+
+    /**
+     * Flag to make sure float View isn't measured twice
+     */
+    private boolean mFloatViewOnMeasured = false;
+
+    /**
+     * Watch the Adapter for data changes. Cancel a drag if
+     * coincident with a change.
+     */
+    private DataSetObserver mObserver;
+
+    /**
+     * Transparency for the floating View (XML attribute).
+     */
+    private float mFloatAlpha = 1.0f;
+    private float mCurrFloatAlpha = 1.0f;
+
+    /**
+     * While drag-sorting, the current position of the floating
+     * View. If dropped, the dragged item will land in this position.
+     */
+    private int mFloatPos;
+
+    /**
+     * The first expanded ListView position that helps represent
+     * the drop slot tracking the floating View.
+     */
+    private int mFirstExpPos;
+
+    /**
+     * The second expanded ListView position that helps represent
+     * the drop slot tracking the floating View. This can equal
+     * mFirstExpPos if there is no slide shuffle occurring; otherwise
+     * it is equal to mFirstExpPos + 1.
+     */
+    private int mSecondExpPos;
+
+    /**
+     * Flag set if slide shuffling is enabled.
+     */
+    private boolean mAnimate = false;
+
+    /**
+     * The user dragged from this position.
+     */
+    private int mSrcPos;
+
+    /**
+     * Offset (in x) within the dragged item at which the user
+     * picked it up (or first touched down with the digitalis).
+     */
+    private int mDragDeltaX;
+
+    /**
+     * Offset (in y) within the dragged item at which the user
+     * picked it up (or first touched down with the digitalis).
+     */
+    private int mDragDeltaY;
+
+    /**
+     * A listener that receives callbacks whenever the floating View
+     * hovers over a new position.
+     */
+    private DragListener mDragListener;
+
+    /**
+     * A listener that receives a callback when the floating View
+     * is dropped.
+     */
+    private DropListener mDropListener;
+
+    /**
+     * A listener that receives a callback when the floating View
+     * (or more precisely the originally dragged item) is removed
+     * by one of the provided gestures.
+     */
+    private RemoveListener mRemoveListener;
+
+    /**
+     * Enable/Disable item dragging
+     */
+    private boolean mDragEnabled = true;
+
+    /**
+     * Drag state enum.
+     */
+    private final static int IDLE = 0;
+    private final static int REMOVING = 1;
+    private final static int DROPPING = 2;
+    private final static int STOPPED = 3;
+    private final static int DRAGGING = 4;
+
+    private int mDragState = IDLE;
+
+    /**
+     * Height in pixels to which the originally dragged item
+     * is collapsed during a drag-sort. Currently, this value
+     * must be greater than zero.
+     */
+    private int mItemHeightCollapsed = 1;
+
+    /**
+     * Height of the floating View. Stored for the purpose of
+     * providing the tracking drop slot.
+     */
+    private int mFloatViewHeight;
+
+    /**
+     * Convenience member. See above.
+     */
+    private int mFloatViewHeightHalf;
+
+    /**
+     * Save the given width spec for use in measuring children
+     */
+    private int mWidthMeasureSpec = 0;
+
+    /**
+     * Sample Views ultimately used for calculating the height
+     * of ListView items that are off-screen.
+     */
+    private View[] mSampleViewTypes = new View[1];
+
+    /**
+     * Drag-scroll encapsulator!
+     */
+    private DragScroller mDragScroller;
+
+    /**
+     * Determines the start of the upward drag-scroll region
+     * at the top of the ListView. Specified by a fraction
+     * of the ListView height, thus screen resolution agnostic.
+     */
+    private float mDragUpScrollStartFrac = 1.0f / 3.0f;
+
+    /**
+     * Determines the start of the downward drag-scroll region
+     * at the bottom of the ListView. Specified by a fraction
+     * of the ListView height, thus screen resolution agnostic.
+     */
+    private float mDragDownScrollStartFrac = 1.0f / 3.0f;
+
+    /**
+     * The following are calculated from the above fracs.
+     */
+    private int mUpScrollStartY;
+    private int mDownScrollStartY;
+    private float mDownScrollStartYF;
+    private float mUpScrollStartYF;
+
+    /**
+     * Calculated from above above and current ListView height.
+     */
+    private float mDragUpScrollHeight;
+
+    /**
+     * Calculated from above above and current ListView height.
+     */
+    private float mDragDownScrollHeight;
+
+
+    /**
+     * Maximum drag-scroll speed in pixels per ms. Only used with
+     * default linear drag-scroll profile.
+     */
+    private float mMaxScrollSpeed = 0.5f;
+
+    /**
+     * Defines the scroll speed during a drag-scroll. User can
+     * provide their own; this default is a simple linear profile
+     * where scroll speed increases linearly as the floating View
+     * nears the top/bottom of the ListView.
+     */
+    private DragScrollProfile mScrollProfile = new DragScrollProfile() {
+        @Override
+        public float getSpeed(float w, long t) {
+            return mMaxScrollSpeed * w;
+        }
+    };
+
+    /**
+     * Current touch x.
+     */
+    private int mX;
+
+    /**
+     * Current touch y.
+     */
+    private int mY;
+
+    /**
+     * Last touch y.
+     */
+    private int mLastY;
+
+    /**
+     * Drag flag bit. Floating View can move in the positive
+     * x direction.
+     */
+    public final static int DRAG_POS_X = 0x1;
+
+    /**
+     * Drag flag bit. Floating View can move in the negative
+     * x direction.
+     */
+    public final static int DRAG_NEG_X = 0x2;
+
+    /**
+     * Drag flag bit. Floating View can move in the positive
+     * y direction. This is subtle. What this actually means is
+     * that, if enabled, the floating View can be dragged below its starting
+     * position. Remove in favor of upper-bounding item position?
+     */
+    public final static int DRAG_POS_Y = 0x4;
+
+    /**
+     * Drag flag bit. Floating View can move in the negative
+     * y direction. This is subtle. What this actually means is
+     * that the floating View can be dragged above its starting
+     * position. Remove in favor of lower-bounding item position?
+     */
+    public final static int DRAG_NEG_Y = 0x8;
+
+    /**
+     * Flags that determine limits on the motion of the
+     * floating View. See flags above.
+     */
+    private int mDragFlags = 0;
+
+    /**
+     * Last call to an on*TouchEvent was a call to
+     * onInterceptTouchEvent.
+     */
+    private boolean mLastCallWasIntercept = false;
+
+    /**
+     * A touch event is in progress.
+     */
+    private boolean mInTouchEvent = false;
+
+    /**
+     * Let the user customize the floating View.
+     */
+    private FloatViewManager mFloatViewManager = null;
+
+    /**
+     * Given to ListView to cancel its action when a drag-sort
+     * begins.
+     */
+    private MotionEvent mCancelEvent;
+
+    /**
+     * Enum telling where to cancel the ListView action when a
+     * drag-sort begins
+     */
+    private static final int NO_CANCEL = 0;
+    private static final int ON_TOUCH_EVENT = 1;
+    private static final int ON_INTERCEPT_TOUCH_EVENT = 2;
+
+    /**
+     * Where to cancel the ListView action when a
+     * drag-sort begins
+     */
+    private int mCancelMethod = NO_CANCEL;
+
+    /**
+     * Determines when a slide shuffle animation starts. That is,
+     * defines how close to the edge of the drop slot the floating
+     * View must be to initiate the slide.
+     */
+    private float mSlideRegionFrac = 0.25f;
+
+    /**
+     * Number between 0 and 1 indicating the relative location of
+     * a sliding item (only used if drag-sort animations
+     * are turned on). Nearly 1 means the item is
+     * at the top of the slide region (nearly full blank item
+     * is directly below).
+     */
+    private float mSlideFrac = 0.0f;
+
+    /**
+     * Wraps the user-provided ListAdapter. This is used to wrap each
+     * item View given by the user inside another View (currenly
+     * a RelativeLayout) which
+     * expands and collapses to simulate the item shuffling.
+     */
+    private AdapterWrapper mAdapterWrapper;
+
+    /**
+     * Turn on custom debugger.
+     */
+    private boolean mTrackDragSort = false;
+
+    /**
+     * Debugging class.
+     */
+    private DragSortTracker mDragSortTracker;
+
+    /**
+     * Needed for adjusting item heights from within layoutChildren
+     */
+    private boolean mBlockLayoutRequests = false;
+
+    /**
+     * Set to true when a down event happens during drag sort;
+     * for example, when drag finish animations are
+     * playing.
+     */
+    private boolean mIgnoreTouchEvent = false;
+
+    /**
+     * Caches DragSortItemView child heights. Sometimes DSLV has to
+     * know the height of an offscreen item. Since ListView virtualizes
+     * these, DSLV must get the item from the ListAdapter to obtain
+     * its height. That process can be expensive, but often the same
+     * offscreen item will be requested many times in a row. Once an
+     * offscreen item height is calculated, we cache it in this guy.
+     * Actually, we cache the height of the child of the
+     * DragSortItemView since the item height changes often during a
+     * drag-sort.
+     */
+    private static final int sCacheSize = 3;
+    private HeightCache mChildHeightCache = new HeightCache(sCacheSize);
+
+    private RemoveAnimator mRemoveAnimator;
+
+    private LiftAnimator mLiftAnimator;
+
+    private DropAnimator mDropAnimator;
+
+    public DragSortListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        int defaultDuration = 150;
+        int removeAnimDuration = defaultDuration; //ms
+        int dropAnimDuration = defaultDuration; //ms
+
+        if (attrs != null) {
+            TypedArray a = getContext().obtainStyledAttributes(attrs,
+                    R.styleable.DragSortListView, 0, 0);
+
+            mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize(
+                    R.styleable.DragSortListView_collapsed_height, 1));
+
+            mTrackDragSort = a.getBoolean(
+                    R.styleable.DragSortListView_track_drag_sort, false);
+
+            if (mTrackDragSort) {
+                mDragSortTracker = new DragSortTracker();
+            }
+
+            // alpha between 0 and 255, 0=transparent, 255=opaque
+            mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha);
+            mCurrFloatAlpha = mFloatAlpha;
+
+            mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled);
+
+            mSlideRegionFrac = Math.max(0.0f,
+                    Math.min(1.0f, 1.0f - a.getFloat(
+                    R.styleable.DragSortListView_slide_shuffle_speed,
+                    0.75f)));
+
+            mAnimate = mSlideRegionFrac > 0.0f;
+
+            float frac = a.getFloat(
+                    R.styleable.DragSortListView_drag_scroll_start,
+                    mDragUpScrollStartFrac);
+
+            setDragScrollStart(frac);
+
+            mMaxScrollSpeed = a.getFloat(
+                    R.styleable.DragSortListView_max_drag_scroll_speed,
+                    mMaxScrollSpeed);
+
+            removeAnimDuration = a.getInt(
+                    R.styleable.DragSortListView_remove_animation_duration,
+                    removeAnimDuration);
+
+            dropAnimDuration = a.getInt(
+                    R.styleable.DragSortListView_drop_animation_duration,
+                    dropAnimDuration);
+
+            boolean useDefault = a.getBoolean(
+                    R.styleable.DragSortListView_use_default_controller,
+                    true);
+
+            if (useDefault) {
+                boolean removeEnabled = a.getBoolean(
+                        R.styleable.DragSortListView_remove_enabled,
+                        false);
+                int removeMode = a.getInt(
+                        R.styleable.DragSortListView_remove_mode,
+                        DragSortController.FLING_RIGHT_REMOVE);
+                boolean sortEnabled = a.getBoolean(
+                        R.styleable.DragSortListView_sort_enabled,
+                        true);
+                int dragInitMode = a.getInt(
+                        R.styleable.DragSortListView_drag_start_mode,
+                        DragSortController.ON_DOWN);
+                int dragHandleId = a.getResourceId(
+                        R.styleable.DragSortListView_drag_handle_id,
+                        0);
+                int clickRemoveId = a.getResourceId(
+                        R.styleable.DragSortListView_click_remove_id,
+                        0);
+                int bgColor = a.getColor(
+                        R.styleable.DragSortListView_float_background_color,
+                        Color.BLACK);
+
+                DragSortController controller = new DragSortController(
+                        this, dragHandleId, dragInitMode, removeMode,
+                        clickRemoveId);
+                controller.setRemoveEnabled(removeEnabled);
+                controller.setSortEnabled(sortEnabled);
+                controller.setBackgroundColor(bgColor);
+
+                mFloatViewManager = controller;
+                setOnTouchListener(controller);
+            }
+
+            a.recycle();
+        }
+
+        mDragScroller = new DragScroller();
+
+        float smoothness = 0.5f;
+        if (removeAnimDuration > 0) {
+            mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration);
+        }
+        //mLiftAnimator = new LiftAnimator(smoothness, 100);
+        if (dropAnimDuration > 0) {
+            mDropAnimator = new DropAnimator(smoothness, dropAnimDuration);
+        }
+
+        if(mCancelEvent!=null){
+            mCancelEvent.recycle();
+        }
+        mCancelEvent = MotionEvent.obtain(0,0,MotionEvent.ACTION_CANCEL,0f,0f,0f,0f,0,0f,0f,0,0);
+
+        // construct the dataset observer
+        mObserver = new DataSetObserver() {
+                    private void cancel() {
+                        if (mDragState == DRAGGING) {
+                            cancelDrag();
+                        }
+                    }
+
+                    @Override
+                    public void onChanged() {
+                        cancel();
+                    }
+
+                    @Override
+                    public void onInvalidated() {
+                        cancel();
+                    }
+                };
+    }
+
+    /**
+     * Usually called from a FloatViewManager. The float alpha
+     * will be reset to the xml-defined value every time a drag
+     * is stopped.
+     */
+    public void setFloatAlpha(float alpha) {
+        mCurrFloatAlpha = alpha;
+    }
+
+    public float getFloatAlpha() {
+        return mCurrFloatAlpha;
+    }
+
+    /**
+     * Set maximum drag scroll speed in positions/second. Only applies
+     * if using default ScrollSpeedProfile.
+     *
+     * @param max Maximum scroll speed.
+     */
+    public void setMaxScrollSpeed(float max) {
+        mMaxScrollSpeed = max;
+    }
+
+    /**
+     * For each DragSortListView Listener interface implemented by
+     * <code>adapter</code>, this method calls the appropriate
+     * set*Listener method with <code>adapter</code> as the argument.
+     *
+     * @param adapter The ListAdapter providing data to back
+     * DragSortListView.
+     *
+     * @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
+     */
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        mAdapterWrapper = new AdapterWrapper(adapter);
+
+        if (adapter != null) {
+            adapter.registerDataSetObserver(mObserver);
+
+            if (adapter instanceof DropListener) {
+                setDropListener((DropListener) adapter);
+            }
+            if (adapter instanceof DragListener) {
+                setDragListener((DragListener) adapter);
+            }
+            if (adapter instanceof RemoveListener) {
+                setRemoveListener((RemoveListener) adapter);
+            }
+        }
+
+        super.setAdapter(mAdapterWrapper);
+    }
+
+    /**
+     * As opposed to {@link ListView#getAdapter()}, which returns
+     * a heavily wrapped ListAdapter (DragSortListView wraps the
+     * input ListAdapter {\emph and} ListView wraps the wrapped one).
+     *
+     * @return The ListAdapter set as the argument of {@link setAdapter()}
+     */
+    public ListAdapter getInputAdapter() {
+        if (mAdapterWrapper == null) {
+            return null;
+        } else {
+            return mAdapterWrapper.getAdapter();
+        }
+    }
+
+    private class AdapterWrapper extends HeaderViewListAdapter {
+        private ListAdapter mAdapter;
+
+        public AdapterWrapper(ListAdapter adapter) {
+            super(null, null, adapter);
+            mAdapter = adapter;
+        }
+
+        public ListAdapter getAdapter() {
+            return mAdapter;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+
+            DragSortItemView v;
+            View child;
+            //Log.d("mobeta", "getView: position="+position+" convertView="+convertView);
+            if (convertView != null) {
+                v = (DragSortItemView) convertView;
+                View oldChild = v.getChildAt(0);
+
+                child = mAdapter.getView(position, oldChild, v);
+                if (child != oldChild) {
+                    // shouldn't get here if user is reusing convertViews properly
+                    v.removeViewAt(0);
+                    v.addView(child);
+                }
+            } else {
+                v = new DragSortItemView(getContext());
+                v.setLayoutParams(new AbsListView.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT));
+                child = mAdapter.getView(position, null, v);
+                v.addView(child);
+            }
+
+            // Set the correct item height given drag state; passed
+            // View needs to be measured if measurement is required.
+            adjustItem(position + getHeaderViewsCount(), v, true);
+
+            return v;
+        }
+    }
+
+    private void drawDivider(int expPosition, Canvas canvas) {
+
+        final Drawable divider = getDivider();
+        final int dividerHeight = getDividerHeight();
+        //Log.d("mobeta", "div="+divider+" divH="+dividerHeight);
+
+        if (divider != null && dividerHeight != 0) {
+            final ViewGroup expItem = (ViewGroup) getChildAt(expPosition - getFirstVisiblePosition());
+            if (expItem != null) {
+                final int l = getPaddingLeft();
+                final int r = getWidth() - getPaddingRight();
+                final int t;
+                final int b;
+
+                final int childHeight = expItem.getChildAt(0).getHeight();
+
+                if (expPosition > mSrcPos) {
+                    t = expItem.getTop() + childHeight;
+                    b = t + dividerHeight;
+                } else {
+                    b = expItem.getBottom() - childHeight;
+                    t = b - dividerHeight;
+                }
+                //Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);
+
+                // Have to clip to support ColorDrawable on <= Gingerbread
+                canvas.save();
+                canvas.clipRect(l, t, r, b);
+                divider.setBounds(l, t, r, b);
+                divider.draw(canvas);
+                canvas.restore();
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mDragState != IDLE) {
+            // draw the divider over the expanded item
+            if (mFirstExpPos != mSrcPos) {
+                drawDivider(mFirstExpPos, canvas);
+            }
+            if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
+                drawDivider(mSecondExpPos, canvas);
+            }
+        }
+
+        if (mFloatView != null) {
+            // draw the float view over everything
+            final int w = mFloatView.getWidth();
+            final int h = mFloatView.getHeight();
+            final int alpha = (int) (255f * mCurrFloatAlpha);
+
+            canvas.save();
+            //Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+            canvas.translate(mFloatLoc.x, mFloatLoc.y);
+            canvas.clipRect(0, 0, w, h);
+
+            //Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+            canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
+            mFloatView.draw(canvas);
+            canvas.restore();
+            canvas.restore();
+        }
+    }
+
+    private int getItemHeight(int position) {
+        View v = getChildAt(position - getFirstVisiblePosition());
+
+        if (v != null) {
+            // item is onscreen, just get the height of the View
+            return v.getHeight();
+        } else {
+            // item is offscreen. get child height and calculate
+            // item height based on current shuffle state
+            return calcItemHeight(position, getChildHeight(position));
+        }
+    }
+
+    private class HeightCache {
+
+        private SparseIntArray mMap;
+        private ArrayList<Integer> mOrder;
+        private int mMaxSize;
+
+        public HeightCache(int size) {
+            mMap = new SparseIntArray(size);
+            mOrder = new ArrayList<Integer>(size);
+            mMaxSize = size;
+        }
+
+        /**
+         * Add item height at position if doesn't already exist.
+         */
+        public void add(int position, int height) {
+            int currHeight = mMap.get(position, -1);
+            if (currHeight != height) {
+                if (currHeight == -1) {
+                    if (mMap.size() == mMaxSize) {
+                        // remove oldest entry
+                        mMap.delete(mOrder.remove(0));
+                    }
+                } else {
+                    // move position to newest slot
+                    mOrder.remove((Integer) position);
+                }
+                mMap.put(position, height);
+                mOrder.add(position);
+            }
+        }
+
+        public int get(int position) {
+            return mMap.get(position, -1);
+        }
+
+        public void clear() {
+            mMap.clear();
+            mOrder.clear();
+        }
+
+    }
+
+    /**
+     * Get the shuffle edge for item at position when top of
+     * item is at y-coord top. Assumes that current item heights
+     * are consistent with current float view location and
+     * thus expanded positions and slide fraction. i.e. Should not be
+     * called between update of expanded positions/slide fraction
+     * and layoutChildren.
+     *
+     * @param position
+     * @param top
+     * @param height Height of item at position. If -1, this function
+     * calculates this height.
+     *
+     * @return Shuffle line between position-1 and position (for
+     * the given view of the list; that is, for when top of item at
+     * position has y-coord of given `top`). If
+     * floating View (treated as horizontal line) is dropped
+     * immediately above this line, it lands in position-1. If
+     * dropped immediately below this line, it lands in position.
+     */
+    private int getShuffleEdge(int position, int top) {
+
+        final int numHeaders = getHeaderViewsCount();
+        final int numFooters = getFooterViewsCount();
+
+        // shuffle edges are defined between items that can be
+        // dragged; there are N-1 of them if there are N draggable
+        // items.
+
+        if (position <= numHeaders || (position >= getCount() - numFooters)) {
+            return top;
+        }
+
+        int divHeight = getDividerHeight();
+
+        int edge;
+
+        int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+        int childHeight = getChildHeight(position);
+        int itemHeight = getItemHeight(position);
+
+        // first calculate top of item given that floating View is
+        // centered over src position
+        int otop = top;
+        if (mSecondExpPos <= mSrcPos) {
+            // items are expanded on and/or above the source position
+
+            if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+                if (position == mSrcPos) {
+                    otop = top + itemHeight - mFloatViewHeight;
+                } else {
+                    int blankHeight = itemHeight - childHeight;
+                    otop = top + blankHeight - maxBlankHeight;
+                }
+            } else if (position > mSecondExpPos && position <= mSrcPos) {
+                otop = top - maxBlankHeight;
+            }
+
+        } else {
+            // items are expanded on and/or below the source position
+
+            if (position > mSrcPos && position <= mFirstExpPos) {
+                otop = top + maxBlankHeight;
+            } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+                int blankHeight = itemHeight - childHeight;
+                otop = top + blankHeight;
+            }
+        }
+
+        // otop is set
+        if (position <= mSrcPos) {
+            edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2;
+        } else {
+            edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2;
+        }
+
+        return edge;
+    }
+
+    private boolean updatePositions() {
+
+        final int first = getFirstVisiblePosition();
+        int startPos = mFirstExpPos;
+        View startView = getChildAt(startPos - first);
+
+        if (startView == null) {
+            startPos = first + getChildCount() / 2;
+            startView = getChildAt(startPos - first);
+        }
+        int startTop = startView.getTop();
+
+        int itemHeight = startView.getHeight();
+
+        int edge = getShuffleEdge(startPos, startTop);
+        int lastEdge = edge;
+
+        int divHeight = getDividerHeight();
+
+        //Log.d("mobeta", "float mid="+mFloatViewMid);
+
+        int itemPos = startPos;
+        int itemTop = startTop;
+        if (mFloatViewMid < edge) {
+            // scanning up for float position
+            //Log.d("mobeta", "    edge="+edge);
+            while (itemPos >= 0) {
+                itemPos--;
+                itemHeight = getItemHeight(itemPos);
+
+                if (itemPos == 0) {
+                    edge = itemTop - divHeight - itemHeight;
+                    break;
+                }
+
+                itemTop -= itemHeight + divHeight;
+                edge = getShuffleEdge(itemPos, itemTop);
+                //Log.d("mobeta", "    edge="+edge);
+
+                if (mFloatViewMid >= edge) {
+                    break;
+                }
+
+                lastEdge = edge;
+            }
+        } else {
+            // scanning down for float position
+            //Log.d("mobeta", "    edge="+edge);
+            final int count = getCount();
+            while (itemPos < count) {
+                if (itemPos == count - 1) {
+                    edge = itemTop + divHeight + itemHeight;
+                    break;
+                }
+
+                itemTop += divHeight + itemHeight;
+                itemHeight = getItemHeight(itemPos + 1);
+                edge = getShuffleEdge(itemPos + 1, itemTop);
+                //Log.d("mobeta", "    edge="+edge);
+
+                // test for hit
+                if (mFloatViewMid < edge) {
+                    break;
+                }
+
+                lastEdge = edge;
+                itemPos++;
+            }
+        }
+
+        final int numHeaders = getHeaderViewsCount();
+        final int numFooters = getFooterViewsCount();
+
+        boolean updated = false;
+
+        int oldFirstExpPos = mFirstExpPos;
+        int oldSecondExpPos = mSecondExpPos;
+        float oldSlideFrac = mSlideFrac;
+
+        if (mAnimate) {
+            int edgeToEdge = Math.abs(edge - lastEdge);
+
+            int edgeTop, edgeBottom;
+            if (mFloatViewMid < edge) {
+                edgeBottom = edge;
+                edgeTop = lastEdge;
+            } else {
+                edgeTop = edge;
+                edgeBottom = lastEdge;
+            }
+            //Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);
+
+            int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge);
+            float slideRgnHeightF = (float) slideRgnHeight;
+            int slideEdgeTop = edgeTop + slideRgnHeight;
+            int slideEdgeBottom = edgeBottom - slideRgnHeight;
+
+            // Three regions
+            if (mFloatViewMid < slideEdgeTop) {
+                mFirstExpPos = itemPos - 1;
+                mSecondExpPos = itemPos;
+                mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF;
+                //Log.d("mobeta", "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+            } else if (mFloatViewMid < slideEdgeBottom) {
+                mFirstExpPos = itemPos;
+                mSecondExpPos = itemPos;
+            } else {
+                mFirstExpPos = itemPos;
+                mSecondExpPos = itemPos + 1;
+                mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) / slideRgnHeightF);
+                //Log.d("mobeta", "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+            }
+
+        } else {
+            mFirstExpPos = itemPos;
+            mSecondExpPos = itemPos;
+        }
+
+        // correct for headers and footers
+        if (mFirstExpPos < numHeaders) {
+            itemPos = numHeaders;
+            mFirstExpPos = itemPos;
+            mSecondExpPos = itemPos;
+        } else if (mSecondExpPos >= getCount() - numFooters) {
+            itemPos = getCount() - numFooters - 1;
+            mFirstExpPos = itemPos;
+            mSecondExpPos = itemPos;
+        }
+
+        if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos || mSlideFrac != oldSlideFrac) {
+            updated = true;
+        }
+
+        if (itemPos != mFloatPos) {
+            if (mDragListener != null) {
+                mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders);
+            }
+
+            mFloatPos = itemPos;
+            updated = true;
+        }
+
+        return updated;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mTrackDragSort) {
+            mDragSortTracker.appendState();
+        }
+    }
+
+    private class SmoothAnimator implements Runnable {
+        private long mStartTime;
+
+        private float mDurationF;
+
+        private float mAlpha;
+        private float mA, mB, mC, mD;
+
+        private boolean mCanceled;
+
+        public SmoothAnimator(float smoothness, int duration) {
+            mAlpha = smoothness;
+            mDurationF = (float) duration;
+            mA = mD = 1f / (2f * mAlpha * (1f - mAlpha));
+            mB = mAlpha / (2f * (mAlpha - 1f));
+            mC = 1f / (1f - mAlpha);
+        }
+
+        public float transform(float frac) {
+            if (frac < mAlpha) {
+                return mA * frac * frac;
+            } else if (frac < 1f - mAlpha) {
+                return mB + mC * frac;
+            } else {
+                return 1f - mD * (frac - 1f) * (frac - 1f);
+            }
+        }
+
+        public void start() {
+            mStartTime = SystemClock.uptimeMillis();
+            mCanceled = false;
+            onStart();
+            post(this);
+        }
+
+        public void cancel() {
+            mCanceled = true;
+        }
+
+        public void onStart() {
+            //stub
+        }
+
+        public void onUpdate(float frac, float smoothFrac) {
+            //stub
+        }
+
+        public void onStop() {
+            //stub
+        }
+
+        @Override
+        public void run() {
+            if (mCanceled) {
+                return;
+            }
+
+            float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF;
+
+            if (fraction >= 1f) {
+                onUpdate(1f, 1f);
+                onStop();
+            } else {
+                onUpdate(fraction, transform(fraction));
+                post(this);
+            }
+        }
+    }
+
+    /**
+     * Centers floating View under touch point.
+     */
+    private class LiftAnimator extends SmoothAnimator {
+
+        private float mInitDragDeltaY;
+        private float mFinalDragDeltaY;
+
+        public LiftAnimator(float smoothness, int duration) {
+            super(smoothness, duration);
+        }
+
+        @Override
+        public void onStart() {
+            mInitDragDeltaY = mDragDeltaY;
+            mFinalDragDeltaY = mFloatViewHeightHalf;
+        }
+
+        @Override
+        public void onUpdate(float frac, float smoothFrac) {
+            if (mDragState != DRAGGING) {
+                cancel();
+            } else {
+                mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) * mInitDragDeltaY);
+                mFloatLoc.y = mY - mDragDeltaY;
+                doDragFloatView(true);
+            }
+        }
+    }
+
+    /**
+     * Centers floating View over drop slot before destroying.
+     */
+    private class DropAnimator extends SmoothAnimator {
+
+        private int mDropPos;
+        private int srcPos;
+        private float mInitDeltaY;
+        private float mInitDeltaX;
+
+        public DropAnimator(float smoothness, int duration) {
+            super(smoothness, duration);
+        }
+
+        @Override
+        public void onStart() {
+            mDropPos = mFloatPos;
+            srcPos = mSrcPos;
+            mDragState = DROPPING;
+            mInitDeltaY = mFloatLoc.y - getTargetY();
+            mInitDeltaX = mFloatLoc.x - getPaddingLeft();
+        }
+
+        private int getTargetY() {
+            final int first = getFirstVisiblePosition();
+            final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2;
+            View v = getChildAt(mDropPos - first);
+            int targetY = -1;
+            if (v != null) {
+                if (mDropPos == srcPos) {
+                    targetY = v.getTop();
+                } else if (mDropPos < srcPos) {
+                    // expanded down
+                    targetY = v.getTop() - otherAdjust;
+                } else {
+                    // expanded up
+                    targetY = v.getBottom() + otherAdjust - mFloatViewHeight;
+                }
+            } else {
+                // drop position is not on screen?? no animation
+                cancel();
+            }
+
+            return targetY;
+        }
+
+        @Override
+        public void onUpdate(float frac, float smoothFrac) {
+            final int targetY = getTargetY();
+            final float deltaY = mFloatLoc.y - targetY;
+            final float f = 1f - smoothFrac;
+            if (f < Math.abs(deltaY / mInitDeltaY)) {
+                mFloatLoc.y = targetY + (int) (mInitDeltaY * f);
+                mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f);
+                doDragFloatView(true);
+            }
+        }
+
+        @Override
+        public void onStop() {
+            dropFloatView();
+        }
+
+    }
+
+    /**
+     * Collapses expanded items.
+     */
+    private class RemoveAnimator extends SmoothAnimator {
+
+        private float mFirstStartBlank;
+        private float mSecondStartBlank;
+
+        private int mFirstChildHeight = -1;
+        private int mSecondChildHeight = -1;
+
+        private int mFirstPos;
+        private int mSecondPos;
+
+        public RemoveAnimator(float smoothness, int duration) {
+            super(smoothness, duration);
+        }
+
+        @Override
+        public void onStart() {
+            mFirstChildHeight = -1;
+            mSecondChildHeight = -1;
+            mFirstPos = mFirstExpPos;
+            mSecondPos = mSecondExpPos;
+            mDragState = REMOVING;
+            destroyFloatView();
+        }
+
+        @Override
+        public void onUpdate(float frac, float smoothFrac) {
+            float f = 1f - smoothFrac;
+
+            final int firstVis = getFirstVisiblePosition();
+            View item = getChildAt(mFirstPos - firstVis);
+            ViewGroup.LayoutParams lp;
+            int blank;
+            if (item != null) {
+                if (mFirstChildHeight == -1) {
+                    mFirstChildHeight = getChildHeight(mFirstPos, item, false);
+                    mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight);
+                }
+                blank = Math.max((int) (f * mFirstStartBlank), 1);
+                lp = item.getLayoutParams();
+                lp.height = mFirstChildHeight + blank;
+                item.setLayoutParams(lp);
+            }
+            if (mSecondPos != mFirstPos) {
+                item = getChildAt(mSecondPos - firstVis);
+                if (item != null) {
+                    if (mSecondChildHeight == -1) {
+                        mSecondChildHeight = getChildHeight(mSecondPos, item, false);
+                        mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight);
+                    }
+                    blank = Math.max((int) (f * mSecondStartBlank), 1);
+                    lp = item.getLayoutParams();
+                    lp.height = mSecondChildHeight + blank;
+                    item.setLayoutParams(lp);
+                }
+            }
+        }
+
+        @Override
+        public void onStop() {
+            doRemoveItem();
+        }
+    }
+
+    /**
+     * Removes an item from the list and animates the removal.
+     *
+     * @param which Position to remove (NOTE: headers/footers ignored!
+     * this is a position in your input ListAdapter).
+     */
+    public void removeItem(int which) {
+        if (mDragState == IDLE || mDragState == DRAGGING) {
+            if (mDragState == IDLE) {
+                // called from outside drag-sort
+                mSrcPos = getHeaderViewsCount() + which;
+                mFirstExpPos = mSrcPos;
+                mSecondExpPos = mSrcPos;
+                mFloatPos = mSrcPos;
+                View v = getChildAt(mSrcPos - getFirstVisiblePosition());
+                if (v != null) {
+                    v.setVisibility(View.INVISIBLE);
+                }
+            }
+
+            if (mInTouchEvent) {
+                switch (mCancelMethod) {
+                case ON_TOUCH_EVENT:
+                    super.onTouchEvent(mCancelEvent);
+                    break;
+                case ON_INTERCEPT_TOUCH_EVENT:
+                    super.onInterceptTouchEvent(mCancelEvent);
+                    break;
+                }
+            }
+
+            if (mRemoveAnimator != null) {
+                mRemoveAnimator.start();
+            } else {
+                doRemoveItem(which);
+            }
+        }
+    }
+
+
+    /**
+     * Move an item, bypassing the drag-sort process. Simply calls
+     * through to {@link DropListener#drop(int, int)}.
+     *
+     * @param from Position to move (NOTE: headers/footers ignored!
+     * this is a position in your input ListAdapter).
+     * @param to Target position (NOTE: headers/footers ignored!
+     * this is a position in your input ListAdapter).
+     */
+    public void moveItem(int from, int to) {
+        if (mDropListener != null) {
+            final int count = getInputAdapter().getCount();
+            if (from >= 0 && from < count && to >= 0 && to < count) {
+                mDropListener.drop(from, to);
+            }
+        }
+    }
+
+    /**
+     * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with
+     * <code>true</code> as the first argument.
+     */
+    public void cancelDrag() {
+        if (mDragState == DRAGGING) {
+            mDragScroller.stopScrolling(true);
+            destroyFloatView();
+            clearPositions();
+            adjustAllItems();
+
+            if (mInTouchEvent) {
+                mDragState = STOPPED;
+            } else {
+                mDragState = IDLE;
+            }
+        }
+    }
+
+    private void clearPositions() {
+        mSrcPos = -1;
+        mFirstExpPos = -1;
+        mSecondExpPos = -1;
+        mFloatPos = -1;
+    }
+
+    private void dropFloatView() {
+        // must set to avoid cancelDrag being called from the
+        // DataSetObserver
+        mDragState = DROPPING;
+
+        if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
+            final int numHeaders = getHeaderViewsCount();
+            mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
+        }
+
+        destroyFloatView();
+
+        adjustOnReorder();
+        clearPositions();
+        adjustAllItems();
+
+        // now the drag is done
+        if (mInTouchEvent) {
+            mDragState = STOPPED;
+        } else {
+            mDragState = IDLE;
+        }
+    }
+
+    private void doRemoveItem() {
+        doRemoveItem(mSrcPos - getHeaderViewsCount());
+    }
+
+    /**
+     * Removes dragged item from the list. Calls RemoveListener.
+     */
+    private void doRemoveItem(int which) {
+        // must set to avoid cancelDrag being called from the
+        // DataSetObserver
+        mDragState = REMOVING;
+
+        // end it
+        if (mRemoveListener != null) {
+            mRemoveListener.remove(which);
+        }
+
+        destroyFloatView();
+
+        adjustOnReorder();
+        clearPositions();
+
+        // now the drag is done
+        if (mInTouchEvent) {
+            mDragState = STOPPED;
+        } else {
+            mDragState = IDLE;
+        }
+    }
+
+    private void adjustOnReorder() {
+        final int firstPos = getFirstVisiblePosition();
+        //Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
+        if (mSrcPos < firstPos) {
+            // collapsed src item is off screen;
+            // adjust the scroll after item heights have been fixed
+            View v = getChildAt(0);
+            int top = 0;
+            if (v != null) {
+                top = v.getTop();
+            }
+            //Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
+            setSelectionFromTop(firstPos - 1, top - getPaddingTop());
+        }
+    }
+
+    /**
+     * Stop a drag in progress. Pass <code>true</code> if you would
+     * like to remove the dragged item from the list.
+     *
+     * @param remove Remove the dragged item from the list. Calls
+     * a registered RemoveListener, if one exists. Otherwise, calls
+     * the DropListener, if one exists.
+     *
+     * @return True if the stop was successful. False if there is
+     * no floating View.
+     */
+    public boolean stopDrag(boolean remove) {
+        if (mFloatView != null) {
+            mDragScroller.stopScrolling(true);
+
+            if (remove) {
+                removeItem(mSrcPos - getHeaderViewsCount());
+            } else {
+                if (mDropAnimator != null) {
+                    mDropAnimator.start();
+                } else {
+                    dropFloatView();
+                }
+            }
+
+            if (mTrackDragSort) {
+                mDragSortTracker.stopTracking();
+            }
+
+            return true;
+        } else {
+            // stop failed
+            return false;
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mIgnoreTouchEvent) {
+            mIgnoreTouchEvent = false;
+            return false;
+        }
+
+        if (!mDragEnabled) {
+            return super.onTouchEvent(ev);
+        }
+
+        boolean more = false;
+
+        boolean lastCallWasIntercept = mLastCallWasIntercept;
+        mLastCallWasIntercept = false;
+
+        if (!lastCallWasIntercept) {
+            saveTouchCoords(ev);
+        }
+
+        //if (mFloatView != null) {
+        if (mDragState == DRAGGING) {
+            onDragTouchEvent(ev);
+            more = true; //give us more!
+        } else {
+            // what if float view is null b/c we dropped in middle
+            // of drag touch event?
+
+            //if (mDragState != STOPPED) {
+            if (mDragState == IDLE) {
+                if (super.onTouchEvent(ev)) {
+                    more = true;
+                }
+            }
+
+            int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+            switch (action) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                doActionUpOrCancel();
+                break;
+            default:
+                if (more) {
+                    mCancelMethod = ON_TOUCH_EVENT;
+                }
+            }
+        }
+
+        return more;
+    }
+
+    private void doActionUpOrCancel() {
+        mCancelMethod = NO_CANCEL;
+        mInTouchEvent = false;
+        if (mDragState == STOPPED) {
+            mDragState = IDLE;
+        }
+        mCurrFloatAlpha = mFloatAlpha;
+        mChildHeightCache.clear();
+    }
+
+    private void saveTouchCoords(MotionEvent ev) {
+        int action = ev.getAction() & MotionEvent.ACTION_MASK;
+        if (action != MotionEvent.ACTION_DOWN) {
+            mLastY = mY;
+        }
+        mX = (int) ev.getX();
+        mY = (int) ev.getY();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mLastY = mY;
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!mDragEnabled) {
+            return super.onInterceptTouchEvent(ev);
+        }
+
+        saveTouchCoords(ev);
+        mLastCallWasIntercept = true;
+
+        int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            if (mDragState != IDLE) {
+                // intercept and ignore
+                mIgnoreTouchEvent = true;
+                return true;
+            }
+            mInTouchEvent = true;
+        }
+
+        boolean intercept = false;
+
+        // the following deals with calls to super.onInterceptTouchEvent
+        if (mFloatView != null) {
+            // super's touch event canceled in startDrag
+            intercept = true;
+        } else {
+            if (super.onInterceptTouchEvent(ev)) {
+                intercept = true;
+            }
+
+            switch (action) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                doActionUpOrCancel();
+                break;
+            default:
+                if (intercept) {
+                    mCancelMethod = ON_TOUCH_EVENT;
+                } else {
+                    mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
+                }
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            mInTouchEvent = false;
+        }
+
+        return intercept;
+    }
+
+    /**
+     * Set the width of each drag scroll region by specifying
+     * a fraction of the ListView height.
+     *
+     * @param heightFraction Fraction of ListView height. Capped at
+     * 0.5f.
+     *
+     */
+    public void setDragScrollStart(float heightFraction) {
+        setDragScrollStarts(heightFraction, heightFraction);
+    }
+
+    /**
+     * Set the width of each drag scroll region by specifying
+     * a fraction of the ListView height.
+     *
+     * @param upperFrac Fraction of ListView height for up-scroll bound.
+     * Capped at 0.5f.
+     * @param lowerFrac Fraction of ListView height for down-scroll bound.
+     * Capped at 0.5f.
+     *
+     */
+    public void setDragScrollStarts(float upperFrac, float lowerFrac) {
+        if (lowerFrac > 0.5f) {
+            mDragDownScrollStartFrac = 0.5f;
+        } else {
+            mDragDownScrollStartFrac = lowerFrac;
+        }
+
+        if (upperFrac > 0.5f) {
+            mDragUpScrollStartFrac = 0.5f;
+        } else {
+            mDragUpScrollStartFrac = upperFrac;
+        }
+
+        if (getHeight() != 0) {
+            updateScrollStarts();
+        }
+    }
+
+    private void continueDrag(int x, int y) {
+
+        // proposed position
+        mFloatLoc.x = x - mDragDeltaX;
+        mFloatLoc.y = y - mDragDeltaY;
+
+        doDragFloatView(true);
+
+        int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
+        int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
+
+        // get the current scroll direction
+        int currentScrollDir = mDragScroller.getScrollDir();
+
+        if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
+            // dragged down, it is below the down scroll start and it is not scrolling up
+
+            if (currentScrollDir != DragScroller.STOP) {
+                // moved directly from up scroll to down scroll
+                mDragScroller.stopScrolling(true);
+            }
+
+            // start scrolling down
+            mDragScroller.startScrolling(DragScroller.DOWN);
+        } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
+            // dragged up, it is above the up scroll start and it is not scrolling up
+
+            if (currentScrollDir != DragScroller.STOP) {
+                // moved directly from down scroll to up scroll
+                mDragScroller.stopScrolling(true);
+            }
+
+            // start scrolling up
+            mDragScroller.startScrolling(DragScroller.UP);
+        }
+        else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY && mDragScroller.isScrolling()) {
+            // not in the upper nor in the lower drag-scroll regions but it is still scrolling
+
+            mDragScroller.stopScrolling(true);
+        }
+    }
+
+    private void updateScrollStarts() {
+        final int padTop = getPaddingTop();
+        final int listHeight = getHeight() - padTop - getPaddingBottom();
+        float heightF = (float) listHeight;
+
+        mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
+        mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
+
+        mUpScrollStartY = (int) mUpScrollStartYF;
+        mDownScrollStartY = (int) mDownScrollStartYF;
+
+        mDragUpScrollHeight = mUpScrollStartYF - padTop;
+        mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        updateScrollStarts();
+    }
+
+    private void adjustAllItems() {
+        final int first = getFirstVisiblePosition();
+        final int last = getLastVisiblePosition();
+
+        int begin = Math.max(0, getHeaderViewsCount() - first);
+        int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
+
+        for (int i = begin; i <= end; ++i) {
+            View v = getChildAt(i);
+            if (v != null) {
+                adjustItem(first + i, v, false);
+            }
+        }
+    }
+
+    /**
+     * Sets layout param height, gravity, and visibility  on
+     * wrapped item.
+     */
+    private void adjustItem(int position, View v, boolean invalidChildHeight) {
+
+        // Adjust item height
+
+        int height = calcItemHeight(position, v, invalidChildHeight);
+        ViewGroup.LayoutParams lp = v.getLayoutParams();
+
+        if (height != lp.height) {
+            lp.height = height;
+            v.setLayoutParams(lp);
+        }
+
+        // Adjust item gravity
+
+        if (position == mFirstExpPos || position == mSecondExpPos) {
+            if (position < mSrcPos) {
+                ((DragSortItemView) v).setGravity(Gravity.BOTTOM);
+            } else if (position > mSrcPos) {
+                ((DragSortItemView) v).setGravity(Gravity.TOP);
+            }
+        }
+
+        // Finally adjust item visibility
+
+        int oldVis = v.getVisibility();
+        int vis = View.VISIBLE;
+
+        if (position == mSrcPos && mFloatView != null) {
+            vis = View.INVISIBLE;
+        }
+
+        if (vis != oldVis) {
+            v.setVisibility(vis);
+        }
+    }
+
+    private int getChildHeight(int position) {
+        if (position == mSrcPos) {
+            return 0;
+        }
+
+        View v = getChildAt(position - getFirstVisiblePosition());
+
+        if (v != null) {
+            // item is onscreen, therefore child height is valid,
+            // hence the "true"
+            return getChildHeight(position, v, false);
+        } else {
+            // item is offscreen
+            // first check cache for child height at this position
+            int childHeight = mChildHeightCache.get(position);
+            if (childHeight != -1) {
+                //Log.d("mobeta", "found child height in cache!");
+                return childHeight;
+            }
+
+            final ListAdapter adapter = getAdapter();
+            int type = adapter.getItemViewType(position);
+
+            // There might be a better place for checking for the following
+            final int typeCount = adapter.getViewTypeCount();
+            if (typeCount != mSampleViewTypes.length) {
+                mSampleViewTypes = new View[typeCount];
+            }
+
+            if (type >= 0) {
+                if (mSampleViewTypes[type] == null) {
+                    v = adapter.getView(position, null, this);
+                    mSampleViewTypes[type] = v;
+                } else {
+                    v = adapter.getView(position, mSampleViewTypes[type], this);
+                }
+            } else {
+                // type is HEADER_OR_FOOTER or IGNORE
+                v = adapter.getView(position, null, this);
+            }
+
+            // current child height is invalid, hence "true" below
+            childHeight = getChildHeight(position, v, true);
+
+            // cache it because this could have been expensive
+            mChildHeightCache.add(position, childHeight);
+
+            return childHeight;
+        }
+    }
+
+    private int getChildHeight(int position, View item, boolean invalidChildHeight) {
+        if (position == mSrcPos) {
+            return 0;
+        }
+
+        View child;
+        if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) {
+            child = item;
+        } else {
+            child = ((ViewGroup) item).getChildAt(0);
+        }
+
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+        if (lp != null) {
+            if (lp.height > 0) {
+                return lp.height;
+            }
+        }
+
+        int childHeight = child.getHeight();
+
+        if (childHeight == 0 || invalidChildHeight) {
+            measureItem(child);
+            childHeight = child.getMeasuredHeight();
+        }
+
+        return childHeight;
+    }
+
+    private int calcItemHeight(int position, View item, boolean invalidChildHeight) {
+        return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight));
+    }
+
+    private int calcItemHeight(int position, int childHeight) {
+
+        boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
+        int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+        int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);
+
+        int height;
+
+        if (position == mSrcPos) {
+            if (mSrcPos == mFirstExpPos) {
+                if (isSliding) {
+                    height = slideHeight + mItemHeightCollapsed;
+                } else {
+                    height = mFloatViewHeight;
+                }
+            } else if (mSrcPos == mSecondExpPos) {
+                // if gets here, we know an item is sliding
+                height = mFloatViewHeight - slideHeight;
+            } else {
+                height = mItemHeightCollapsed;
+            }
+        } else if (position == mFirstExpPos) {
+            if (isSliding) {
+                height = childHeight + slideHeight;
+            } else {
+                height = childHeight + maxNonSrcBlankHeight;
+            }
+        } else if (position == mSecondExpPos) {
+            // we know an item is sliding (b/c 2ndPos != 1stPos)
+            height = childHeight + maxNonSrcBlankHeight - slideHeight;
+        } else {
+            height = childHeight;
+        }
+
+        return height;
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mBlockLayoutRequests) {
+            super.requestLayout();
+        }
+    }
+
+    private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) {
+        int adjust = 0;
+
+        final int childHeight = getChildHeight(movePos);
+
+        int moveHeightBefore = moveItem.getHeight();
+        int moveHeightAfter = calcItemHeight(movePos, childHeight);
+
+        int moveBlankBefore = moveHeightBefore;
+        int moveBlankAfter = moveHeightAfter;
+        if (movePos != mSrcPos) {
+            moveBlankBefore -= childHeight;
+            moveBlankAfter -= childHeight;
+        }
+
+        int maxBlank = mFloatViewHeight;
+        if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
+            maxBlank -= mItemHeightCollapsed;
+        }
+
+        if (movePos <= oldFirstExpPos) {
+            if (movePos > mFirstExpPos) {
+                adjust += maxBlank - moveBlankAfter;
+            }
+        } else if (movePos == oldSecondExpPos) {
+            if (movePos <= mFirstExpPos) {
+                adjust += moveBlankBefore - maxBlank;
+            } else if (movePos == mSecondExpPos) {
+                adjust += moveHeightBefore - moveHeightAfter;
+            } else {
+                adjust += moveBlankBefore;
+            }
+        } else {
+            if (movePos <= mFirstExpPos) {
+                adjust -= maxBlank;
+            } else if (movePos == mSecondExpPos) {
+                adjust -= moveBlankAfter;
+            }
+        }
+
+        return adjust;
+    }
+
+    private void measureItem(View item) {
+        ViewGroup.LayoutParams lp = item.getLayoutParams();
+        if (lp == null) {
+            lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        }
+        int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp.width);
+        int hspec;
+        if (lp.height > 0) {
+            hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+        } else {
+            hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        }
+        item.measure(wspec, hspec);
+    }
+
+    private void measureFloatView() {
+        if (mFloatView != null) {
+            measureItem(mFloatView);
+            mFloatViewHeight = mFloatView.getMeasuredHeight();
+            mFloatViewHeightHalf = mFloatViewHeight / 2;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        //Log.d("mobeta", "onMeasure called");
+        if (mFloatView != null) {
+            if (mFloatView.isLayoutRequested()) {
+                measureFloatView();
+            }
+            mFloatViewOnMeasured = true; //set to false after layout
+        }
+        mWidthMeasureSpec = widthMeasureSpec;
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+
+        if (mFloatView != null) {
+            if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
+                // Have to measure here when usual android measure
+                // pass is skipped. This happens during a drag-sort
+                // when layoutChildren is called directly.
+                measureFloatView();
+            }
+            mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
+            mFloatViewOnMeasured = false;
+        }
+    }
+
+
+    protected boolean onDragTouchEvent(MotionEvent ev) {
+        // we are in a drag
+        switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+        case MotionEvent.ACTION_CANCEL:
+            if (mDragState == DRAGGING) {
+                cancelDrag();
+            }
+            doActionUpOrCancel();
+            break;
+        case MotionEvent.ACTION_UP:
+            //Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
+            if (mDragState == DRAGGING) {
+                stopDrag(false);
+            }
+            doActionUpOrCancel();
+            break;
+        case MotionEvent.ACTION_MOVE:
+            continueDrag((int) ev.getX(), (int) ev.getY());
+            break;
+        }
+
+        return true;
+    }
+
+    /**
+     * Start a drag of item at <code>position</code> using the
+     * registered FloatViewManager. Calls through
+     * to {@link #startDrag(int,View,int,int,int)} after obtaining
+     * the floating View from the FloatViewManager.
+     *
+     * @param position Item to drag.
+     * @param dragFlags Flags that restrict some movements of the
+     * floating View. For example, set <code>dragFlags |=
+     * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+     * View in all directions except off the screen to the left.
+     * @param deltaX Offset in x of the touch coordinate from the
+     * left edge of the floating View (i.e. touch-x minus float View
+     * left).
+     * @param deltaY Offset in y of the touch coordinate from the
+     * top edge of the floating View (i.e. touch-y minus float View
+     * top).
+     *
+     * @return True if the drag was started, false otherwise. This
+     * <code>startDrag</code> will fail if we are not currently in
+     * a touch event, there is no registered FloatViewManager,
+     * or the FloatViewManager returns a null View.
+     */
+    public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
+        if (!mInTouchEvent || mFloatViewManager == null) {
+            return false;
+        }
+
+        View v = mFloatViewManager.onCreateFloatView(position);
+
+        if (v == null) {
+            return false;
+        } else {
+            return startDrag(position, v, dragFlags, deltaX, deltaY);
+        }
+
+    }
+
+    /**
+     * Start a drag of item at <code>position</code> without using
+     * a FloatViewManager.
+     *
+     * @param position Item to drag.
+     * @param floatView Floating View.
+     * @param dragFlags Flags that restrict some movements of the
+     * floating View. For example, set <code>dragFlags |= 
+     * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+     * View in all directions except off the screen to the left.
+     * @param deltaX Offset in x of the touch coordinate from the
+     * left edge of the floating View (i.e. touch-x minus float View
+     * left).
+     * @param deltaY Offset in y of the touch coordinate from the
+     * top edge of the floating View (i.e. touch-y minus float View
+     * top).
+     *
+     * @return True if the drag was started, false otherwise. This
+     * <code>startDrag</code> will fail if we are not currently in
+     * a touch event, <code>floatView</code> is null, or there is
+     * a drag in progress.
+     */
+    public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
+        if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null) {
+            return false;
+        }
+
+        if (getParent() != null) {
+            getParent().requestDisallowInterceptTouchEvent(true);
+        }
+
+        int pos = position + getHeaderViewsCount();
+        mFirstExpPos = pos;
+        mSecondExpPos = pos;
+        mSrcPos = pos;
+        mFloatPos = pos;
+
+        //mDragState = dragType;
+        mDragState = DRAGGING;
+        mDragFlags = 0;
+        mDragFlags |= dragFlags;
+
+        mFloatView = floatView;
+        measureFloatView(); //sets mFloatViewHeight
+
+        mDragDeltaX = deltaX;
+        mDragDeltaY = deltaY;
+
+        //updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
+        mFloatLoc.x = mX - mDragDeltaX;
+        mFloatLoc.y = mY - mDragDeltaY;
+
+        // set src item invisible
+        final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
+
+        if (srcItem != null) {
+            srcItem.setVisibility(View.INVISIBLE);
+        }
+
+        if (mTrackDragSort) {
+            mDragSortTracker.startTracking();
+        }
+
+        // once float view is created, events are no longer passed
+        // to ListView
+        switch (mCancelMethod) {
+        case ON_TOUCH_EVENT:
+            super.onTouchEvent(mCancelEvent);
+            break;
+        case ON_INTERCEPT_TOUCH_EVENT:
+            super.onInterceptTouchEvent(mCancelEvent);
+            break;
+        }
+
+        requestLayout();
+
+        if (mLiftAnimator != null) {
+            mLiftAnimator.start();
+        }
+
+        return true;
+    }
+
+    private void doDragFloatView(boolean forceInvalidate) {
+        int movePos = getFirstVisiblePosition() + getChildCount() / 2;
+        View moveItem = getChildAt(getChildCount() / 2);
+
+        if (moveItem == null) {
+            return;
+        }
+
+        doDragFloatView(movePos, moveItem, forceInvalidate);
+    }
+
+    private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) {
+        mBlockLayoutRequests = true;
+
+        updateFloatView();
+
+        int oldFirstExpPos = mFirstExpPos;
+        int oldSecondExpPos = mSecondExpPos;
+
+        boolean updated = updatePositions();
+
+        if (updated) {
+            adjustAllItems();
+            int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos);
+            //Log.d("mobeta", "  adjust scroll="+scroll);
+
+            setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop());
+            layoutChildren();
+        }
+
+        if (updated || forceInvalidate) {
+            invalidate();
+        }
+
+        mBlockLayoutRequests = false;
+    }
+
+    /**
+     * Sets float View location based on suggested values and
+     * constraints set in mDragFlags.
+     */
+    private void updateFloatView() {
+
+        if (mFloatViewManager != null) {
+            mTouchLoc.set(mX, mY);
+            mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
+        }
+
+        final int floatX = mFloatLoc.x;
+        final int floatY = mFloatLoc.y;
+
+        // restrict x motion
+        int padLeft = getPaddingLeft();
+        if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
+            mFloatLoc.x = padLeft;
+        } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
+            mFloatLoc.x = padLeft;
+        }
+
+        // keep floating view from going past bottom of last header view
+        final int numHeaders = getHeaderViewsCount();
+        final int numFooters = getFooterViewsCount();
+        final int firstPos = getFirstVisiblePosition();
+        final int lastPos = getLastVisiblePosition();
+
+        //Log.d("mobeta", "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
+        int topLimit = getPaddingTop();
+        if (firstPos < numHeaders) {
+            topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
+        }
+        if ((mDragFlags & DRAG_NEG_Y) == 0) {
+            if (firstPos <= mSrcPos) {
+                topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
+            }
+        }
+        // bottom limit is top of first footer View or
+        // bottom of last item in list
+        int bottomLimit = getHeight() - getPaddingBottom();
+        if (lastPos >= getCount() - numFooters - 1) {
+            bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
+        }
+        if ((mDragFlags & DRAG_POS_Y) == 0) {
+            if (lastPos >= mSrcPos) {
+                bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
+            }
+        }
+
+        //Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
+        //Log.d("mobeta", "limit=" + limit);
+        //Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
+
+        if (floatY < topLimit) {
+            mFloatLoc.y = topLimit;
+        } else if (floatY + mFloatViewHeight > bottomLimit) {
+            mFloatLoc.y = bottomLimit - mFloatViewHeight;
+        }
+
+        // get y-midpoint of floating view (constrained to ListView bounds)
+        mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
+    }
+
+    private void destroyFloatView() {
+        if (mFloatView != null) {
+            mFloatView.setVisibility(GONE);
+            if (mFloatViewManager != null) {
+                mFloatViewManager.onDestroyFloatView(mFloatView);
+            }
+            mFloatView = null;
+            invalidate();
+        }
+    }
+
+    /**
+     * Interface for customization of the floating View appearance
+     * and dragging behavior. Implement
+     * your own and pass it to {@link #setFloatViewManager}. If
+     * your own is not passed, the default {@link SimpleFloatViewManager}
+     * implementation is used.
+     */
+    public interface FloatViewManager {
+        /**
+         * Return the floating View for item at <code>position</code>.
+         * DragSortListView will measure and layout this View for you,
+         * so feel free to just inflate it. You can help DSLV by
+         * setting some {@link ViewGroup.LayoutParams} on this View;
+         * otherwise it will set some for you (with a width of FILL_PARENT
+         * and a height of WRAP_CONTENT).
+         *
+         * @param position Position of item to drag (NOTE:
+         * <code>position</code> excludes header Views; thus, if you
+         * want to call {@link ListView#getChildAt(int)}, you will need
+         * to add {@link ListView#getHeaderViewsCount()} to the index).
+         *
+         * @return The View you wish to display as the floating View.
+         */
+        public View onCreateFloatView(int position);
+
+        /**
+         * Called whenever the floating View is dragged. Float View
+         * properties can be changed here. Also, the upcoming location
+         * of the float View can be altered by setting
+         * <code>location.x</code> and <code>location.y</code>.
+         *
+         * @param floatView The floating View.
+         * @param location The location (top-left; relative to DSLV
+         * top-left) at which the float
+         * View would like to appear, given the current touch location
+         * and the offset provided in {@link DragSortListView#startDrag}.
+         * @param touch The current touch location (relative to DSLV
+         * top-left).
+         * @param pendingScroll
+         */
+        public void onDragFloatView(View floatView, Point location, Point touch);
+
+        /**
+         * Called when the float View is dropped; lets you perform
+         * any necessary cleanup. The internal DSLV floating View
+         * reference is set to null immediately after this is called.
+         *
+         * @param floatView The floating View passed to
+         * {@link #onCreateFloatView(int)}.
+         */
+        public void onDestroyFloatView(View floatView);
+    }
+
+    public void setFloatViewManager(FloatViewManager manager) {
+        mFloatViewManager = manager;
+    }
+
+    public void setDragListener(DragListener l) {
+        mDragListener = l;
+    }
+
+    /**
+     * Allows for easy toggling between a DragSortListView
+     * and a regular old ListView. If enabled, items are
+     * draggable, where the drag init mode determines how
+     * items are lifted (see {@link setDragInitMode(int)}).
+     * If disabled, items cannot be dragged.
+     *
+     * @param enabled Set <code>true</code> to enable list
+     * item dragging
+     */
+    public void setDragEnabled(boolean enabled) {
+        mDragEnabled = enabled;
+    }
+
+    public boolean isDragEnabled() {
+        return mDragEnabled;
+    }
+
+    /**
+     * This better reorder your ListAdapter! DragSortListView does not do this
+     * for you; doesn't make sense to. Make sure
+     * {@link BaseAdapter#notifyDataSetChanged()} or something like it is
+     * called in your implementation.
+     *
+     * @param l
+     */
+    public void setDropListener(DropListener l) {
+        mDropListener = l;
+    }
+
+    /**
+     * Probably a no-brainer, but make sure that your remove listener
+     * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it.
+     * When an item removal occurs, DragSortListView
+     * relies on a redraw of all the items to recover invisible views
+     * and such. Strictly speaking, if you remove something, your dataset
+     * has changed...
+     *
+     * @param l
+     */
+    public void setRemoveListener(RemoveListener l) {
+        mRemoveListener = l;
+    }
+
+    public interface DragListener {
+        public void drag(int from, int to);
+    }
+
+    /**
+     * Your implementation of this has to reorder your ListAdapter!
+     * Make sure to call
+     * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+     * in your implementation.
+     *
+     * @author heycosmo
+     *
+     */
+    public interface DropListener {
+        public void drop(int from, int to);
+    }
+
+    /**
+     * Make sure to call
+     * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+     * in your implementation.
+     *
+     * @author heycosmo
+     *
+     */
+    public interface RemoveListener {
+        public void remove(int which);
+    }
+
+    public interface DragSortListener extends DropListener, DragListener, RemoveListener {}
+
+    public void setDragSortListener(DragSortListener l) {
+        setDropListener(l);
+        setDragListener(l);
+        setRemoveListener(l);
+    }
+
+    /**
+     * Completely custom scroll speed profile. Default increases linearly
+     * with position and is constant in time. Create your own by implementing
+     * {@link DragSortListView.DragScrollProfile}.
+     *
+     * @param ssp
+     */
+    public void setDragScrollProfile(DragScrollProfile ssp) {
+        if (ssp != null) {
+            mScrollProfile = ssp;
+        }
+    }
+
+    /**
+     * Interface for controlling
+     * scroll speed as a function of touch position and time. Use
+     * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to
+     * set custom profile.
+     *
+     * @author heycosmo
+     *
+     */
+    public interface DragScrollProfile {
+        /**
+         * Return a scroll speed in pixels/millisecond. Always return a
+         * positive number.
+         *
+         * @param w Normalized position in scroll region (i.e. w \in [0,1]).
+         * Small w typically means slow scrolling.
+         * @param t Time (in milliseconds) since start of scroll (handy if you
+         * want scroll acceleration).
+         * @return Scroll speed at position w and time t in pixels/ms.
+         */
+        float getSpeed(float w, long t);
+    }
+
+    private class DragScroller implements Runnable {
+
+        private boolean mAbort;
+
+        private long mPrevTime;
+        private long mCurrTime;
+
+        private int dy;
+        private float dt;
+        private long tStart;
+        private int scrollDir;
+
+        public final static int STOP = -1;
+        public final static int UP = 0;
+        public final static int DOWN = 1;
+
+        private float mScrollSpeed; // pixels per ms
+
+        private boolean mScrolling = false;
+
+        public boolean isScrolling() {
+            return mScrolling;
+        }
+
+        public int getScrollDir() {
+            return mScrolling ? scrollDir : STOP;
+        }
+
+        public DragScroller() {}
+
+        public void startScrolling(int dir) {
+            if (!mScrolling) {
+                //Debug.startMethodTracing("dslv-scroll");
+                mAbort = false;
+                mScrolling = true;
+                tStart = SystemClock.uptimeMillis();
+                mPrevTime = tStart;
+                scrollDir = dir;
+                post(this);
+            }
+        }
+
+        public void stopScrolling(boolean now) {
+            if (now) {
+                DragSortListView.this.removeCallbacks(this);
+                mScrolling = false;
+            } else {
+                mAbort = true;
+            }
+
+            //Debug.stopMethodTracing();
+        }
+
+        @Override
+        public void run() {
+            if (mAbort) {
+                mScrolling = false;
+                return;
+            }
+
+            //Log.d("mobeta", "scroll");
+
+            final int first = getFirstVisiblePosition();
+            final int last = getLastVisiblePosition();
+            final int count = getCount();
+            final int padTop = getPaddingTop();
+            final int listHeight = getHeight() - padTop - getPaddingBottom();
+
+            int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
+            int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
+
+            if (scrollDir == UP) {
+                View v = getChildAt(0);
+                //Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
+                if (v == null) {
+                    mScrolling = false;
+                    return;
+                } else {
+                    if (first == 0 && v.getTop() == padTop) {
+                        mScrolling = false;
+                        return;
+                    }
+                }
+                mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) / mDragUpScrollHeight, mPrevTime);
+            } else {
+                View v = getChildAt(last - first);
+                if (v == null) {
+                    mScrolling = false;
+                    return;
+                } else {
+                    if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
+                        mScrolling = false;
+                        return;
+                    }
+                }
+                mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) / mDragDownScrollHeight, mPrevTime);
+            }
+
+            mCurrTime = SystemClock.uptimeMillis();
+            dt = (float) (mCurrTime - mPrevTime);
+
+            // dy is change in View position of a list item; i.e. positive dy
+            // means user is scrolling up (list item moves down the screen, remember
+            // y=0 is at top of View).
+            dy = (int) Math.round(mScrollSpeed * dt);
+
+            int movePos;
+            if (dy >= 0) {
+                dy = Math.min(listHeight, dy);
+                movePos = first;
+            } else {
+                dy = Math.max(-listHeight, dy);
+                movePos = last;
+            }
+
+            final View moveItem = getChildAt(movePos - first);
+            int top = moveItem.getTop() + dy;
+
+            if (movePos == 0 && top > padTop) {
+                top = padTop;
+            }
+
+            // always do scroll
+            mBlockLayoutRequests = true;
+
+            setSelectionFromTop(movePos, top - padTop);
+            DragSortListView.this.layoutChildren();
+            invalidate();
+
+            mBlockLayoutRequests = false;
+
+            // scroll means relative float View movement
+            doDragFloatView(movePos, moveItem, false);
+
+            mPrevTime = mCurrTime;
+            //Log.d("mobeta", "  updated prevTime="+mPrevTime);
+
+            post(this);
+        }
+    }
+
+    private class DragSortTracker {
+        StringBuilder mBuilder = new StringBuilder();
+
+        File mFile;
+
+        private int mNumInBuffer = 0;
+        private int mNumFlushes = 0;
+
+        private boolean mTracking = false;
+
+        public DragSortTracker() {
+            File root = Environment.getExternalStorageDirectory();
+            mFile = new File(root, "dslv_state.txt");
+
+            if (!mFile.exists()) {
+                try {
+                    mFile.createNewFile();
+                    Log.d("mobeta", "file created");
+                } catch (IOException e) {
+                    Log.w("mobeta", "Could not create dslv_state.txt");
+                    Log.d("mobeta", e.getMessage());
+                }
+            }
+
+        }
+
+        public void startTracking() {
+            mBuilder.append("<DSLVStates>\n");
+            mNumFlushes = 0;
+            mTracking = true;
+        }
+
+        public void appendState() {
+            if (!mTracking) {
+                return;
+            }
+
+            mBuilder.append("<DSLVState>\n");
+            final int children = getChildCount();
+            final int first = getFirstVisiblePosition();
+            mBuilder.append("    <Positions>");
+            for (int i = 0; i < children; ++i) {
+                mBuilder.append(first + i).append(",");
+            }
+            mBuilder.append("</Positions>\n");
+
+            mBuilder.append("    <Tops>");
+            for (int i = 0; i < children; ++i) {
+                mBuilder.append(getChildAt(i).getTop()).append(",");
+            }
+            mBuilder.append("</Tops>\n");
+            mBuilder.append("    <Bottoms>");
+            for (int i = 0; i < children; ++i) {
+                mBuilder.append(getChildAt(i).getBottom()).append(",");
+            }
+            mBuilder.append("</Bottoms>\n");
+
+            mBuilder.append("    <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
+            mBuilder.append("    <FirstExpBlankHeight>")
+                            .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos))
+                            .append("</FirstExpBlankHeight>\n");
+            mBuilder.append("    <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
+            mBuilder.append("    <SecondExpBlankHeight>")
+                            .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos))
+                            .append("</SecondExpBlankHeight>\n");
+            mBuilder.append("    <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
+            mBuilder.append("    <SrcHeight>").append(mFloatViewHeight + getDividerHeight()).append("</SrcHeight>\n");
+            mBuilder.append("    <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
+            mBuilder.append("    <LastY>").append(mLastY).append("</LastY>\n");
+            mBuilder.append("    <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
+            mBuilder.append("    <ShuffleEdges>");
+            for (int i = 0; i < children; ++i) {
+                mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
+            }
+            mBuilder.append("</ShuffleEdges>\n");
+
+            mBuilder.append("</DSLVState>\n");
+            mNumInBuffer++;
+
+            if (mNumInBuffer > 1000) {
+                flush();
+                mNumInBuffer = 0;
+            }
+        }
+
+        public void flush() {
+            if (!mTracking) {
+                return;
+            }
+
+            // save to file on sdcard
+            try {
+                boolean append = true;
+                if (mNumFlushes == 0) {
+                    append = false;
+                }
+                FileWriter writer = new FileWriter(mFile, append);
+
+                writer.write(mBuilder.toString());
+                mBuilder.delete(0, mBuilder.length());
+
+                writer.flush();
+                writer.close();
+
+                mNumFlushes++;
+            } catch (IOException e) {
+                // do nothing
+            }
+        }
+
+        public void stopTracking() {
+            if (mTracking) {
+                mBuilder.append("</DSLVStates>\n");
+                flush();
+                mTracking = false;
+            }
+        }
+    }
+}
diff --git a/src/org/omnirom/omnigears/ui/dslv/SimpleFloatViewManager.java b/src/org/omnirom/omnigears/ui/dslv/SimpleFloatViewManager.java
new file mode 100644
index 0000000..8e523e6
--- /dev/null
+++ b/src/org/omnirom/omnigears/ui/dslv/SimpleFloatViewManager.java
@@ -0,0 +1,105 @@
+/**
+ * A subclass of the Android ListView component that enables drag
+ * and drop re-ordering of list items.
+ *
+ * Copyright 2012 Carl Bauer
+ *
+ * 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 org.omnirom.omnigears.ui.dslv;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ListView;
+
+/**
+ * Simple implementation of the FloatViewManager class. Uses list
+ * items as they appear in the ListView to create the floating View.
+ */
+public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
+
+    private Bitmap mFloatBitmap;
+
+    private ImageView mImageView;
+
+    private int mFloatBGColor = Color.BLACK;
+
+    protected ListView mListView;
+
+    public SimpleFloatViewManager(ListView lv) {
+        mListView = lv;
+    }
+
+    public void setBackgroundColor(int color) {
+        mFloatBGColor = color;
+    }
+
+    /**
+     * This simple implementation creates a Bitmap copy of the
+     * list item currently shown at ListView <code>position</code>.
+     */
+    @Override
+    public View onCreateFloatView(int position) {
+        // Guaranteed that this will not be null? I think so. Nope, got
+        // a NullPointerException once...
+        View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
+
+        if (v == null) {
+            return null;
+        }
+
+        v.setPressed(false);
+
+        // Create a copy of the drawing cache so that it does not get
+        // recycled by the framework when the list tries to clean up memory
+        //v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
+        v.setDrawingCacheEnabled(true);
+        mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
+        v.setDrawingCacheEnabled(false);
+
+        if (mImageView == null) {
+            mImageView = new ImageView(mListView.getContext());
+        }
+        mImageView.setBackgroundColor(mFloatBGColor);
+        mImageView.setPadding(0, 0, 0, 0);
+        mImageView.setImageBitmap(mFloatBitmap);
+
+        return mImageView;
+    }
+
+    /**
+     * This does nothing
+     */
+    @Override
+    public void onDragFloatView(View floatView, Point position, Point touch) {
+        // do nothing
+    }
+
+    /**
+     * Removes the Bitmap from the ImageView created in
+     * onCreateFloatView() and tells the system to recycle it.
+     */
+    @Override
+    public void onDestroyFloatView(View floatView) {
+        ((ImageView) floatView).setImageDrawable(null);
+
+        mFloatBitmap.recycle();
+        mFloatBitmap = null;
+    }
+
+}
+