Merge "Send predictedApps rank via user event logging" into ub-launcher3-calgary
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e7b703c..9192764 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,8 +53,9 @@
     <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
 
     <application
-        android:allowBackup="@bool/enable_backup"
-        android:backupAgent="com.android.launcher3.LauncherBackupAgentHelper"
+        android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+        android:fullBackupOnly="true"
+        android:fullBackupContent="@xml/backupscheme"
         android:hardwareAccelerated="true"
         android:icon="@mipmap/ic_launcher_home"
         android:label="@string/app_name"
diff --git a/build.gradle b/build.gradle
index 4df4063..d777e95 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,7 @@
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:1.5.0'
+        classpath 'com.android.tools.build:gradle:2.1.0'
         classpath 'com.google.protobuf:protobuf-gradle-plugin:0.7.0'
     }
 }
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
new file mode 100644
index 0000000..7e833a0
--- /dev/null
+++ b/res/xml/backupscheme.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <include domain="database" path="launcher.db" />
+    <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
+
+</full-backup-content>
\ No newline at end of file
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 0d71a0c..a04c557 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -630,7 +630,7 @@
                     copyInteger(myValues, childValues, Favorites.CELLY);
 
                     addedId = folderItems.get(0);
-                    mDb.update(LauncherProvider.TABLE_FAVORITES, childValues,
+                    mDb.update(Favorites.TABLE_NAME, childValues,
                             Favorites._ID + "=" + addedId, null);
                 }
             }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index bb70be6..7e1ecf5 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,8 +16,12 @@
 
 package com.android.launcher3;
 
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -27,12 +31,13 @@
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
+import com.android.launcher3.dynamicui.ExtractedColors;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 
 public class Hotseat extends FrameLayout
-        implements UserEventDispatcher.LaunchSourceProvider{
+        implements UserEventDispatcher.LaunchSourceProvider, Insettable {
 
     private CellLayout mContent;
 
@@ -44,6 +49,14 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private final boolean mHasVerticalHotseat;
 
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private Rect mInsets = new Rect();
+
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private int mBackgroundColor;
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private ColorDrawable mBackground;
+
     public Hotseat(Context context) {
         this(context, null);
     }
@@ -56,6 +69,8 @@
         super(context, attrs, defStyle);
         mLauncher = (Launcher) context;
         mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
+        mBackground = new ColorDrawable();
+        setBackground(mBackground);
     }
 
     public CellLayout getLayout() {
@@ -166,4 +181,46 @@
         target.gridY = info.cellY;
         targetParent.containerType = LauncherLogProto.HOTSEAT;
     }
+
+    //Overridden so that the background color extends behind the navigation buttons.
+    @Override
+    public void setInsets(Rect insets) {
+        int rightInset = insets.right - mInsets.right;
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        LayoutParams lp = (LayoutParams) getLayoutParams();
+        if (mHasVerticalHotseat) {
+            setPadding(getPaddingLeft(), getPaddingTop(),
+            getPaddingRight() + rightInset, getPaddingBottom());
+            if (lp.width != LayoutParams.MATCH_PARENT && lp.width != LayoutParams.WRAP_CONTENT) {
+                lp.width += rightInset;
+            }
+        } else {
+            setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+            getPaddingBottom() + bottomInset);
+            if (lp.height != LayoutParams.MATCH_PARENT && lp.height != LayoutParams.WRAP_CONTENT) {
+                lp.height += bottomInset;
+            }
+        }
+    }
+
+    public void updateColor(ExtractedColors extractedColors, boolean animate) {
+        if (!mHasVerticalHotseat) {
+            int color = extractedColors.getColor(ExtractedColors.HOTSEAT_INDEX, Color.TRANSPARENT);
+            if (!animate) {
+                setBackgroundColor(color);
+            } else {
+                ValueAnimator animator = ValueAnimator.ofInt(mBackgroundColor, color);
+                animator.setEvaluator(new ArgbEvaluator());
+                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        mBackground.setColor((Integer) animation.getAnimatedValue());
+                    }
+                });
+                animator.start();
+            }
+            mBackgroundColor = color;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b2f96d0..eacf72a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -114,6 +114,7 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.ViewOnDrawExecutor;
@@ -123,11 +124,9 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -307,11 +306,6 @@
     private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>();
     private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false;
 
-    private static final ArrayList<String> sDumpLogs = new ArrayList<String>();
-    private static final Date sDateStamp = new Date();
-    private static final DateFormat sDateFormat =
-            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
-
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
     private SharedPreferences mSharedPrefs;
@@ -502,9 +496,10 @@
     }
 
     private void loadExtractedColorsAndColorItems() {
-        if (mExtractedColors != null) {
+        // TODO: do this in pre-N as well, once the extraction part is complete.
+        if (mExtractedColors != null && Utilities.isNycOrAbove()) {
             mExtractedColors.load(this);
-            // TODO: pass mExtractedColors to interested items such as hotseat.
+            mHotseat.updateColor(mExtractedColors, !mPaused);
         }
     }
 
@@ -3976,7 +3971,7 @@
 
             // Verify that we own the widget
             if (appWidgetInfo == null) {
-                Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
+                FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
                 deleteWidgetInfo(item);
                 return;
             }
@@ -4649,12 +4644,10 @@
             }
         }
 
-        synchronized (sDumpLogs) {
-            writer.println();
-            writer.println(prefix + "Debug logs");
-            for (String log : sDumpLogs) {
-                writer.println(prefix + "  " + log);
-            }
+        try {
+            FileLog.flushAll(writer);
+        } catch (Exception e) {
+            // Ignore
         }
 
         if (mLauncherCallbacks != null) {
@@ -4662,14 +4655,6 @@
         }
     }
 
-    public static void addDumpLog(String tag, String log) {
-        Log.d(tag, log);
-        synchronized(sDumpLogs) {
-            sDateStamp.setTime(System.currentTimeMillis());
-            sDumpLogs.add(sDateFormat.format(sDateStamp) + ": " + tag + ", " + log);
-        }
-    }
-
     public static CustomAppWidget getCustomAppWidget(String name) {
         return sCustomAppWidgets.get(name);
     }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index f84e4b5..9d889e0 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -27,9 +27,9 @@
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.util.ConfigMonitor;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
 
@@ -79,6 +79,7 @@
         // is the first component to get created. Initializing application context here ensures
         // that LauncherAppState always exists in the main process.
         sContext = provider.getContext().getApplicationContext();
+        FileLog.setDir(sContext.getFilesDir());
     }
 
     private LauncherAppState() {
@@ -184,8 +185,4 @@
     public InvariantDeviceProfile getInvariantDeviceProfile() {
         return mInvariantDeviceProfile;
     }
-
-    public static boolean isDogfoodBuild() {
-        return FeatureFlags.IS_ALPHA_BUILD || FeatureFlags.IS_DEV_BUILD;
-    }
 }
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
index 570607e..28557d0 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -256,7 +256,7 @@
     @Override
     public void requestChildFocus(View child, View focused) {
         super.requestChildFocus(child, focused);
-        dispatchChildFocus(focused != null);
+        dispatchChildFocus(mChildrenFocused && focused != null);
         if (focused != null) {
             focused.setFocusableInTouchMode(false);
         }
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
new file mode 100644
index 0000000..b2f5c57
--- /dev/null
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -0,0 +1,133 @@
+package com.android.launcher3;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.ParcelFileDescriptor;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.logging.FileLog;
+
+import java.io.InvalidObjectException;
+
+public class LauncherBackupAgent extends BackupAgent {
+
+    private static final String TAG = "LauncherBackupAgent";
+
+    private static final String INFO_COLUMN_NAME = "name";
+    private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
+
+    @Override
+    public void onRestore(
+            BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
+        // Doesn't do incremental backup/restore
+    }
+
+    @Override
+    public void onBackup(
+            ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
+        // Doesn't do incremental backup/restore
+    }
+
+    @Override
+    public void onRestoreFinished() {
+        DatabaseHelper helper = new DatabaseHelper(this, null, LauncherFiles.LAUNCHER_DB);
+
+        if (!sanitizeDBSafely(helper)) {
+            helper.createEmptyDB(helper.getWritableDatabase());
+        }
+
+        try {
+            // Flush all logs before the process is killed.
+            FileLog.flushAll(null);
+        } catch (Exception e) { }
+    }
+
+    private boolean sanitizeDBSafely(DatabaseHelper helper) {
+        SQLiteDatabase db = helper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            sanitizeDB(helper, db);
+            db.setTransactionSuccessful();
+            return true;
+        } catch (Exception e) {
+            FileLog.e(TAG, "Failed to verify db", e);
+            return false;
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Makes the following changes in the provider DB.
+     *   1. Removes all entries belonging to a managed profile as managed profiles
+     *      cannot be restored.
+     *   2. Marks all entries as restored. The flags are updated during first load or as
+     *      the restored apps get installed.
+     *   3. If the user serial for primary profile is different than that of the previous device,
+     *      update the entries to the new profile id.
+     */
+    private void sanitizeDB(DatabaseHelper helper, SQLiteDatabase db) throws Exception {
+        long oldProfileId = getDefaultProfileId(db);
+        // Delete all entries which do not belong to the main user
+        int itemsDeleted = db.delete(
+                Favorites.TABLE_NAME, "profileId != ?", new String[]{Long.toString(oldProfileId)});
+        if (itemsDeleted > 0) {
+            FileLog.d(TAG, itemsDeleted + " items belonging to a managed profile, were deleted");
+        }
+
+        // Mark all items as restored.
+        ContentValues values = new ContentValues();
+        values.put(Favorites.RESTORED, 1);
+        db.update(Favorites.TABLE_NAME, values, null, null);
+
+        // Mark widgets with appropriate restore flag
+        values.put(Favorites.RESTORED,
+                LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
+                LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
+                LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
+        db.update(Favorites.TABLE_NAME, values, "itemType = ?",
+                new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
+
+        long myProfileId = helper.getDefaultUserSerial();
+        if (Utilities.longCompare(oldProfileId, myProfileId) != 0) {
+            FileLog.d(TAG, "Changing primary user id from " + oldProfileId + " to " + myProfileId);
+            migrateProfileId(db, myProfileId);
+        }
+    }
+
+    /**
+     * Updates profile id of all entries and changes the default value for the column.
+     */
+    protected void migrateProfileId(SQLiteDatabase db, long newProfileId) {
+        // Update existing entries.
+        ContentValues values = new ContentValues();
+        values.put(Favorites.PROFILE_ID, newProfileId);
+        db.update(Favorites.TABLE_NAME, values, null, null);
+
+        // Change default value of the column.
+        db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;");
+        Favorites.addTableToDb(db, newProfileId, false);
+        db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;");
+        db.execSQL("DROP TABLE favorites_old;");
+    }
+
+    /**
+     * Returns the profile id for used in the favorites table of the provided db.
+     */
+    protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
+        try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+            int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
+            while (c.moveToNext()) {
+                if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
+                    return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE));
+                }
+            }
+            throw new InvalidObjectException("Table does not have a profile id column");
+        }
+    }
+}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 884685c..2fd12fd 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -60,6 +60,7 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.CursorIconInfo;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
@@ -1335,7 +1336,7 @@
                 try {
                     screenIds.add(sc.getLong(idIndex));
                 } catch (Exception e) {
-                    addDumpLog("Invalid screen id: " + e);
+                    FileLog.d(TAG, "Invalid screen id", e);
                 }
             }
         } finally {
@@ -1813,7 +1814,7 @@
                                             if (intent == null) {
                                                 // The app is installed but the component is no
                                                 // longer available.
-                                                addDumpLog("Invalid component removed: " + cn);
+                                                FileLog.d(TAG, "Invalid component removed: " + cn);
                                                 itemsToRemove.add(id);
                                                 continue;
                                             } else {
@@ -1824,7 +1825,7 @@
                                         } else if (restored) {
                                             // Package is not yet available but might be
                                             // installed later.
-                                            addDumpLog("package not yet restored: " + cn);
+                                            FileLog.d(TAG, "package not yet restored: " + cn);
 
                                             if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) {
                                                 // Restore has started once.
@@ -1850,12 +1851,12 @@
                                                     itemReplaced = true;
 
                                                 } else if (REMOVE_UNRESTORED_ICONS) {
-                                                    addDumpLog("Unrestored package removed: " + cn);
+                                                    FileLog.d(TAG, "Unrestored package removed: " + cn);
                                                     itemsToRemove.add(id);
                                                     continue;
                                                 }
                                             } else if (REMOVE_UNRESTORED_ICONS) {
-                                                addDumpLog("Unrestored package removed: " + cn);
+                                                FileLog.d(TAG, "Unrestored package removed: " + cn);
                                                 itemsToRemove.add(id);
                                                 continue;
                                             }
@@ -1880,7 +1881,7 @@
                                         } else {
                                             // Do not wait for external media load anymore.
                                             // Log the invalid package, and remove it
-                                            addDumpLog("Invalid package removed: " + cn);
+                                            FileLog.d(TAG, "Invalid package removed: " + cn);
                                             itemsToRemove.add(id);
                                             continue;
                                         }
@@ -1890,7 +1891,7 @@
                                         restored = false;
                                     }
                                 } catch (URISyntaxException e) {
-                                    addDumpLog("Invalid uri: " + intentDescription);
+                                    FileLog.d(TAG, "Invalid uri: " + intentDescription);
                                     itemsToRemove.add(id);
                                     continue;
                                 }
@@ -2073,7 +2074,7 @@
                                 final boolean isProviderReady = isValidProvider(provider);
                                 if (!isSafeMode && !customWidget &&
                                         wasProviderReady && !isProviderReady) {
-                                    addDumpLog("Deleting widget that isn't installed anymore: "
+                                    FileLog.d(TAG, "Deleting widget that isn't installed anymore: "
                                             + provider);
                                     itemsToRemove.add(id);
                                 } else {
@@ -2115,7 +2116,7 @@
                                             appWidgetInfo.restoreStatus |=
                                                     LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
                                         } else if (REMOVE_UNRESTORED_ICONS && !isSafeMode) {
-                                            addDumpLog("Unrestored widget removed: " + component);
+                                            FileLog.d(TAG, "Unrestored widget removed: " + component);
                                             itemsToRemove.add(id);
                                             continue;
                                         }
@@ -2171,9 +2172,7 @@
                         }
                     }
                 } finally {
-                    if (c != null) {
-                        c.close();
-                    }
+                    Utilities.closeSilently(c);
                 }
 
                 // Break early if we've stopped loading
@@ -3541,8 +3540,4 @@
     public static Looper getWorkerLooper() {
         return sWorkerThread.getLooper();
     }
-
-    @Thunk static final void addDumpLog(String log) {
-        Launcher.addDumpLog(TAG, log);
-    }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 53026ac..f10099e 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -42,20 +42,25 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
 import android.os.Process;
 import android.os.UserManager;
+import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherSettings.WorkspaceScreens;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.NoLocaleSqliteContext;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
 import java.net.URISyntaxException;
@@ -71,18 +76,19 @@
 
     public static final String AUTHORITY = ProviderConfig.AUTHORITY;
 
-    static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME;
-    static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME;
     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
     private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
 
-    private static final Object LISTENER_LOCK = new Object();
-    @Thunk LauncherProviderChangeListener mListener;
+    private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
+    private Handler mListenerHandler;
+
     protected DatabaseHelper mOpenHelper;
 
     @Override
     public boolean onCreate() {
+        mListenerHandler = new Handler(mListenerWrapper);
+
         LauncherAppState.setLauncherProvider(this);
         return true;
     }
@@ -91,9 +97,8 @@
      * Sets a provider listener.
      */
     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
-        synchronized (LISTENER_LOCK) {
-            mListener = listener;
-        }
+        Preconditions.assertUIThread();
+        mListenerWrapper.mListener = listener;
     }
 
     @Override
@@ -111,7 +116,7 @@
      */
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = new DatabaseHelper(getContext(), this);
+            mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
         }
     }
 
@@ -159,7 +164,7 @@
 
         // In very limited cases, we support system|signature permission apps to modify the db.
         if (Binder.getCallingPid() != Process.myPid()) {
-            if (!mOpenHelper.initializeExternalAdd(initialValues)) {
+            if (!initializeExternalAdd(initialValues)) {
                 return null;
             }
         }
@@ -189,6 +194,59 @@
         return uri;
     }
 
+    private boolean initializeExternalAdd(ContentValues values) {
+        // 1. Ensure that externally added items have a valid item id
+        long id = mOpenHelper.generateNewItemId();
+        values.put(LauncherSettings.Favorites._ID, id);
+
+        // 2. In the case of an app widget, and if no app widget id is specified, we
+        // attempt allocate and bind the widget.
+        Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
+        if (itemType != null &&
+                itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
+                !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
+
+            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
+            ComponentName cn = ComponentName.unflattenFromString(
+                    values.getAsString(Favorites.APPWIDGET_PROVIDER));
+
+            if (cn != null) {
+                try {
+                    int appWidgetId = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID)
+                            .allocateAppWidgetId();
+                    values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
+                    if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
+                        return false;
+                    }
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Failed to initialize external widget", e);
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        // Add screen id if not present
+        long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
+        SQLiteStatement stmp = null;
+        try {
+            stmp = mOpenHelper.getWritableDatabase().compileStatement(
+                    "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " +
+                            "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens");
+            stmp.bindLong(1, screenId);
+
+            ContentValues valuesInserted = new ContentValues();
+            valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert());
+            mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted);
+            return true;
+        } catch (Exception e) {
+            return false;
+        } finally {
+            Utilities.closeSilently(stmp);
+        }
+    }
+
     @Override
     public int bulkInsert(Uri uri, ContentValues[] values) {
         createDbIfNotExists();
@@ -295,17 +353,7 @@
                         .putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors)
                         .putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId)
                         .apply();
-                new MainThreadExecutor().execute(new Runnable() {
-                    @Override
-                    public void run() {
-                        synchronized (LISTENER_LOCK) {
-                            if (mListener != null) {
-                                mListener.onExtractedColorsChanged();
-                            }
-                        }
-
-                    }
-                });
+                mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED);
                 Bundle result = new Bundle();
                 result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors);
                 return result;
@@ -340,6 +388,8 @@
             case LauncherSettings.Settings.METHOD_MIGRATE_LAUNCHER2_SHORTCUTS: {
                 mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
                         Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
+                Utilities.getPrefs(getContext()).edit().putBoolean(EMPTY_DATABASE_CREATED, false)
+                        .commit();
                 return null;
             }
             case LauncherSettings.Settings.METHOD_UPDATE_FOLDER_ITEMS_RANK: {
@@ -373,8 +423,8 @@
                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
                             LauncherSettings.Favorites.CONTAINER + " FROM "
-                                + TABLE_FAVORITES + ")";
-            Cursor c = db.query(TABLE_FAVORITES,
+                                + Favorites.TABLE_NAME + ")";
+            Cursor c = db.query(Favorites.TABLE_NAME,
                     new String[] {LauncherSettings.Favorites._ID},
                     selection, null, null, null, null);
             while (c.moveToNext()) {
@@ -382,7 +432,7 @@
             }
             c.close();
             if (!folderIds.isEmpty()) {
-                db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery(
+                db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
                         LauncherSettings.Favorites._ID, folderIds), null);
             }
             db.setTransactionSuccessful();
@@ -401,11 +451,7 @@
     protected void notifyListeners() {
         // always notify the backup agent
         LauncherBackupAgentHelper.dataChanged(getContext());
-        synchronized (LISTENER_LOCK) {
-            if (mListener != null) {
-                mListener.onLauncherProviderChange();
-            }
-        }
+        mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
     }
 
     @Thunk static void addModifiedTime(ContentValues values) {
@@ -436,10 +482,10 @@
         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
             Log.d(TAG, "loading default workspace");
 
-            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction();
+            AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
+            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
             if (loader == null) {
-                loader = AutoInstallsLayout.get(getContext(),
-                        mOpenHelper.mAppWidgetHost, mOpenHelper);
+                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
             }
             if (loader == null) {
                 final Partner partner = Partner.get(getContext().getPackageManager());
@@ -448,7 +494,7 @@
                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                             "xml", partner.getPackageName());
                     if (workspaceResId != 0) {
-                        loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
+                        loader = new DefaultLayoutParser(getContext(), widgetHost,
                                 mOpenHelper, partnerRes, workspaceResId);
                     }
                 }
@@ -456,7 +502,7 @@
 
             final boolean usingExternallyProvidedLayout = loader != null;
             if (loader == null) {
-                loader = getDefaultLayoutParser();
+                loader = getDefaultLayoutParser(widgetHost);
             }
 
             // There might be some partially restored DB items, due to buggy restore logic in
@@ -468,7 +514,7 @@
                 // Unable to load external layout. Cleanup and load the internal layout.
                 createEmptyDB();
                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
-                        getDefaultLayoutParser());
+                        getDefaultLayoutParser(widgetHost));
             }
             clearFlagEmptyDbCreated();
         }
@@ -480,7 +526,7 @@
      * @return the loader if the restrictions are set and the resource exists; null otherwise.
      */
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() {
+    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
         // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18
         if (!Utilities.ATLEAST_JB_MR2) {
             return null;
@@ -499,7 +545,7 @@
                 Resources targetResources = ctx.getPackageManager()
                         .getResourcesForApplication(packageName);
                 return AutoInstallsLayout.get(ctx, packageName, targetResources,
-                        mOpenHelper.mAppWidgetHost, mOpenHelper);
+                        widgetHost, mOpenHelper);
             } catch (NameNotFoundException e) {
                 Log.e(TAG, "Target package for restricted profile not found", e);
                 return null;
@@ -508,50 +554,28 @@
         return null;
     }
 
-    private DefaultLayoutParser getDefaultLayoutParser() {
+    private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
         int defaultLayout = LauncherAppState.getInstance()
                 .getInvariantDeviceProfile().defaultLayoutId;
-        return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
+        return new DefaultLayoutParser(getContext(), widgetHost,
                 mOpenHelper, getContext().getResources(), defaultLayout);
     }
 
     /**
-     * Send notification that we've deleted the {@link AppWidgetHost},
-     * probably as part of the initial database creation. The receiver may
-     * want to re-call {@link AppWidgetHost#startListening()} to ensure
-     * callbacks are correctly set.
-     */
-    @Thunk void notifyAppHostReset() {
-        new MainThreadExecutor().execute(new Runnable() {
-
-            @Override
-            public void run() {
-                synchronized (LISTENER_LOCK) {
-                    if (mListener != null) {
-                        mListener.onAppWidgetHostReset();
-                    }
-                }
-            }
-        });
-    }
-
-    /**
      * The class is subclassed in tests to create an in-memory db.
      */
-    protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
-        private final LauncherProvider mProvider;
+    public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
+        private final Handler mWidgetHostResetHandler;
         private final Context mContext;
-        @Thunk final AppWidgetHost mAppWidgetHost;
         private long mMaxItemId = -1;
         private long mMaxScreenId = -1;
 
-        DatabaseHelper(Context context, LauncherProvider provider) {
-            this(context, provider, LauncherFiles.LAUNCHER_DB,
-                    new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID));
+        DatabaseHelper(Context context, Handler widgetHostResetHandler) {
+            this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
             // Table creation sometimes fails silently, which leads to a crash loop.
             // This way, we will try to create a table every time after crash, so the device
             // would eventually be able to recover.
-            if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) {
+            if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
                 // This operation is a no-op if the table already exists.
                 addFavoritesTable(getWritableDatabase(), true);
@@ -562,14 +586,13 @@
         }
 
         /**
-         * Constructor used only in tests.
+         * Constructor used in tests and for restore.
          */
         public DatabaseHelper(
-                Context context, LauncherProvider provider, String tableName, AppWidgetHost host) {
+                Context context, Handler widgetHostResetHandler, String tableName) {
             super(new NoLocaleSqliteContext(context), tableName, null, DATABASE_VERSION);
             mContext = context;
-            mProvider = provider;
-            mAppWidgetHost = host;
+            mWidgetHostResetHandler = widgetHostResetHandler;
         }
 
         protected void initIds() {
@@ -605,12 +628,6 @@
             addFavoritesTable(db, false);
             addWorkspacesTable(db, false);
 
-            // Database was just created, so wipe any previous widgets
-            if (mAppWidgetHost != null) {
-                mAppWidgetHost.deleteHost();
-                mProvider.notifyAppHostReset();
-            }
-
             // Fresh and clean launcher DB.
             mMaxItemId = initializeMaxItemId(db);
             onEmptyDbCreated();
@@ -620,6 +637,13 @@
          * Overriden in tests.
          */
         protected void onEmptyDbCreated() {
+            // Database was just created, so wipe any previous widgets
+            if (mWidgetHostResetHandler != null) {
+                new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID).deleteHost();
+                mWidgetHostResetHandler.sendEmptyMessage(
+                        ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
+            }
+
             // Set the flag for empty DB
             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
 
@@ -634,35 +658,12 @@
         }
 
         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
-            String ifNotExists = optional ? " IF NOT EXISTS " : "";
-            db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" +
-                    "_id INTEGER PRIMARY KEY," +
-                    "title TEXT," +
-                    "intent TEXT," +
-                    "container INTEGER," +
-                    "screen INTEGER," +
-                    "cellX INTEGER," +
-                    "cellY INTEGER," +
-                    "spanX INTEGER," +
-                    "spanY INTEGER," +
-                    "itemType INTEGER," +
-                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
-                    "iconType INTEGER," +
-                    "iconPackage TEXT," +
-                    "iconResource TEXT," +
-                    "icon BLOB," +
-                    "appWidgetProvider TEXT," +
-                    "modified INTEGER NOT NULL DEFAULT 0," +
-                    "restored INTEGER NOT NULL DEFAULT 0," +
-                    "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," +
-                    "rank INTEGER NOT NULL DEFAULT 0," +
-                    "options INTEGER NOT NULL DEFAULT 0" +
-                    ");");
+            Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
         }
 
         private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
             String ifNotExists = optional ? " IF NOT EXISTS " : "";
-            db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" +
+            db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
                     LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
                     LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
                     LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
@@ -673,10 +674,10 @@
             // Delete items directly on the workspace who's screen id doesn't exist
             //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
             //   AND container = -100"
-            String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
+            String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME +
                     " WHERE " +
                     LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
-                    LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
+                    LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" +
                     " AND " +
                     LauncherSettings.Favorites.CONTAINER + " = " +
                     LauncherSettings.Favorites.CONTAINER_DESKTOP;
@@ -685,7 +686,7 @@
             // Delete items contained in folders which no longer exist (after above statement)
             //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
             //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
-            String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
+            String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME +
                     " WHERE " +
                     LauncherSettings.Favorites.CONTAINER + " <> " +
                     LauncherSettings.Favorites.CONTAINER_DESKTOP +
@@ -694,16 +695,12 @@
                     LauncherSettings.Favorites.CONTAINER_HOTSEAT +
                     " AND "
                     + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
-                    LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
+                    LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME +
                     " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
             db.execSQL(removeOrphanedFolderItems);
         }
 
-        private void setFlagJustLoadedOldDb() {
-            Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
-        }
-
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
@@ -823,8 +820,8 @@
          * Clears all the data for a fresh start.
          */
         public void createEmptyDB(SQLiteDatabase db) {
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
+            db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
+            db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
             onCreate(db);
         }
 
@@ -839,9 +836,8 @@
 
             try {
                 // Only consider the primary user as other users can't have a shortcut.
-                long userSerial = UserManagerCompat.getInstance(mContext)
-                        .getSerialNumberForUser(UserHandleCompat.myUserHandle());
-                c = db.query(TABLE_FAVORITES, new String[] {
+                long userSerial = getDefaultUserSerial();
+                c = db.query(Favorites.TABLE_NAME, new String[] {
                         Favorites._ID,
                         Favorites.INTENT,
                     }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
@@ -891,7 +887,7 @@
         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
             db.beginTransaction();
             try {
-                Cursor c = db.query(TABLE_WORKSPACE_SCREENS,
+                Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
                         new String[] {LauncherSettings.WorkspaceScreens._ID},
                         null, null, null, null,
                         LauncherSettings.WorkspaceScreens.SCREEN_RANK);
@@ -909,7 +905,7 @@
                     c.close();
                 }
 
-                db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
+                db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
                 addWorkspacesTable(db, false);
 
                 // Add all screen ids back
@@ -919,7 +915,7 @@
                     values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
                     values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
                     addModifiedTime(values);
-                    db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values);
+                    db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
                 }
                 db.setTransactionSuccessful();
                 mMaxScreenId = maxId;
@@ -966,12 +962,7 @@
         }
 
         private boolean addProfileColumn(SQLiteDatabase db) {
-            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
-            // Default to the serial number of this user, for older
-            // shortcuts.
-            long userSerialNumber = userManager.getSerialNumberForUser(
-                    UserHandleCompat.myUserHandle());
-            return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber);
+            return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial());
         }
 
         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
@@ -1005,12 +996,12 @@
 
         @Override
         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
-            return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
+            return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
         }
 
         public void checkId(String table, ContentValues values) {
             long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
-            if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
+            if (table == WorkspaceScreens.TABLE_NAME) {
                 mMaxScreenId = Math.max(id, mMaxScreenId);
             }  else {
                 mMaxItemId = Math.max(id, mMaxItemId);
@@ -1018,7 +1009,7 @@
         }
 
         private long initializeMaxItemId(SQLiteDatabase db) {
-            return getMaxId(db, TABLE_FAVORITES);
+            return getMaxId(db, Favorites.TABLE_NAME);
         }
 
         // Generates a new ID to use for an workspace screen in your database. This method
@@ -1035,94 +1026,7 @@
         }
 
         private long initializeMaxScreenId(SQLiteDatabase db) {
-            return getMaxId(db, TABLE_WORKSPACE_SCREENS);
-        }
-
-        @Thunk boolean initializeExternalAdd(ContentValues values) {
-            // 1. Ensure that externally added items have a valid item id
-            long id = generateNewItemId();
-            values.put(LauncherSettings.Favorites._ID, id);
-
-            // 2. In the case of an app widget, and if no app widget id is specified, we
-            // attempt allocate and bind the widget.
-            Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
-            if (itemType != null &&
-                    itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
-                    !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
-
-                final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
-                ComponentName cn = ComponentName.unflattenFromString(
-                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
-
-                if (cn != null) {
-                    try {
-                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
-                        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
-                        if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
-                            return false;
-                        }
-                    } catch (RuntimeException e) {
-                        Log.e(TAG, "Failed to initialize external widget", e);
-                        return false;
-                    }
-                } else {
-                    return false;
-                }
-            }
-
-            // Add screen id if not present
-            long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
-            if (!addScreenIdIfNecessary(screenId)) {
-                return false;
-            }
-            return true;
-        }
-
-        // Returns true of screen id exists, or if successfully added
-        private boolean addScreenIdIfNecessary(long screenId) {
-            if (!hasScreenId(screenId)) {
-                int rank = getMaxScreenRank() + 1;
-
-                ContentValues v = new ContentValues();
-                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
-                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
-                if (dbInsertAndCheck(this, getWritableDatabase(),
-                        TABLE_WORKSPACE_SCREENS, null, v) < 0) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        private boolean hasScreenId(long screenId) {
-            SQLiteDatabase db = getWritableDatabase();
-            Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
-                    + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
-            if (c != null) {
-                int count = c.getCount();
-                c.close();
-                return count > 0;
-            } else {
-                return false;
-            }
-        }
-
-        private int getMaxScreenRank() {
-            SQLiteDatabase db = getWritableDatabase();
-            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
-                    + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
-
-            // get the result
-            final int maxRankIndex = 0;
-            int rank = -1;
-            if (c != null && c.moveToNext()) {
-                rank = c.getInt(maxRankIndex);
-            }
-            if (c != null) {
-                c.close();
-            }
-
-            return rank;
+            return getMaxId(db, WorkspaceScreens.TABLE_NAME);
         }
 
         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
@@ -1138,7 +1042,7 @@
                 values.clear();
                 values.put(LauncherSettings.WorkspaceScreens._ID, id);
                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
-                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
+                if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) {
                     throw new RuntimeException("Failed initialize screen table"
                             + "from default layout");
                 }
@@ -1383,7 +1287,7 @@
                             try {
                                 for (ContentValues row: allItems) {
                                     if (row == null) continue;
-                                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
+                                    if (dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, row)
                                             < 0) {
                                         return;
                                     } else {
@@ -1402,7 +1306,7 @@
                                 final ContentValues values = new ContentValues();
                                 values.put(LauncherSettings.WorkspaceScreens._ID, i);
                                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
-                                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
+                                if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values)
                                         < 0) {
                                     return;
                                 }
@@ -1422,9 +1326,6 @@
             Log.d(TAG, "migrated " + count + " icons from Launcher2 into "
                     + (curScreen+1) + " screens");
 
-            // ensure that new screens are created to hold these icons
-            setFlagJustLoadedOldDb();
-
             // Update max IDs; very important since we just grabbed IDs from another database
             mMaxItemId = initializeMaxItemId(db);
             mMaxScreenId = initializeMaxScreenId(db);
@@ -1484,4 +1385,31 @@
             }
         }
     }
+
+    private static class ChangeListenerWrapper implements Handler.Callback {
+
+        private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
+        private static final int MSG_EXTRACTED_COLORS_CHANGED = 2;
+        private static final int MSG_APP_WIDGET_HOST_RESET = 3;
+
+        private LauncherProviderChangeListener mListener;
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            if (mListener != null) {
+                switch (msg.what) {
+                    case MSG_LAUNCHER_PROVIDER_CHANGED:
+                        mListener.onLauncherProviderChange();
+                        break;
+                    case MSG_EXTRACTED_COLORS_CHANGED:
+                        mListener.onExtractedColorsChanged();
+                        break;
+                    case MSG_APP_WIDGET_HOST_RESET:
+                        mListener.onAppWidgetHostReset();
+                        break;
+                }
+            }
+            return true;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 0e559a5..13e4547 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import android.content.ContentResolver;
+import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.BaseColumns;
@@ -256,6 +257,33 @@
          * <p>Type: INTEGER</p>
          */
         public static final String OPTIONS = "options";
+
+        public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
+            String ifNotExists = optional ? " IF NOT EXISTS " : "";
+            db.execSQL("CREATE TABLE " + ifNotExists + TABLE_NAME + " (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    "title TEXT," +
+                    "intent TEXT," +
+                    "container INTEGER," +
+                    "screen INTEGER," +
+                    "cellX INTEGER," +
+                    "cellY INTEGER," +
+                    "spanX INTEGER," +
+                    "spanY INTEGER," +
+                    "itemType INTEGER," +
+                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
+                    "iconType INTEGER," +
+                    "iconPackage TEXT," +
+                    "iconResource TEXT," +
+                    "icon BLOB," +
+                    "appWidgetProvider TEXT," +
+                    "modified INTEGER NOT NULL DEFAULT 0," +
+                    "restored INTEGER NOT NULL DEFAULT 0," +
+                    "profileId INTEGER DEFAULT " + myProfileId + "," +
+                    "rank INTEGER NOT NULL DEFAULT 0," +
+                    "options INTEGER NOT NULL DEFAULT 0" +
+                    ");");
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1acbfc1..c5f601d 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -46,7 +46,6 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.os.Build;
-import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.PowerManager;
 import android.text.Spannable;
@@ -63,9 +62,11 @@
 
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.util.IconNormalizer;
 
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -845,6 +846,18 @@
         return true;
     }
 
+    public static void closeSilently(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException e) {
+                if (ProviderConfig.IS_DOGFOOD_BUILD) {
+                    Log.d(TAG, "Error closing", e);
+                }
+            }
+        }
+    }
+
     /**
      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
      * This allows the badging to be done based on the action bitmap size rather than
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5711a3d..34c6663 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -18,27 +18,15 @@
 
 /**
  * Defines a set of flags used to control various launcher behaviors
- * All the flags must be defined as
- *   public static boolean LAUNCHER3_FLAG_NAME = true/false;
- *
- * Use LAUNCHER3_ prefix for prevent namespace conflicts.
  */
 public final class FeatureFlags {
     private FeatureFlags() {}
 
-    public static boolean IS_DEV_BUILD = false;
-    public static boolean IS_ALPHA_BUILD = false;
-    public static boolean IS_RELEASE_BUILD = true;
-
     // Custom flags go below this
     public static boolean LAUNCHER3_DISABLE_ICON_NORMALIZATION = false;
     // As opposed to the new spring-loaded workspace.
     public static boolean LAUNCHER3_LEGACY_WORKSPACE_DND = false;
     public static boolean LAUNCHER3_LEGACY_FOLDER_ICON = false;
-    public static boolean LAUNCHER3_LEGACY_LOGGING = false;
     public static boolean LAUNCHER3_USE_SYSTEM_DRAG_DRIVER = false;
     public static boolean LAUNCHER3_DISABLE_PINCH_TO_OVERVIEW = false;
-
-    // This flags is only defined to resolve some build issues.
-    public static boolean LAUNCHER3_ICON_NORMALIZATION = false;
 }
diff --git a/src/com/android/launcher3/config/ProviderConfig.java b/src/com/android/launcher3/config/ProviderConfig.java
index 825b434..1d964b1 100644
--- a/src/com/android/launcher3/config/ProviderConfig.java
+++ b/src/com/android/launcher3/config/ProviderConfig.java
@@ -20,5 +20,5 @@
 
     public static final String AUTHORITY = "com.android.launcher3.settings".intern();
 
-    public static boolean IS_DOGFOOD_BUILD = false;
+    public static boolean IS_DOGFOOD_BUILD = true;
 }
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
index 95a62b9..89594f4 100644
--- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java
+++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
@@ -32,6 +32,9 @@
  */
 public class ColorExtractionService extends IntentService {
 
+    /** The fraction of the wallpaper to extract colors for use on the hotseat. */
+    private static final float HOTSEAT_FRACTION = 1f / 4;
+
     public ColorExtractionService() {
         super("ColorExtractionService");
     }
@@ -44,10 +47,21 @@
         if (wallpaperManager.getWallpaperInfo() != null) {
             // We can't extract colors from live wallpapers, so just use the default color always.
             extractedColors.updatePalette(null);
+            extractedColors.updateHotseatPalette(null);
         } else {
             Bitmap wallpaper = ((BitmapDrawable) wallpaperManager.getDrawable()).getBitmap();
             Palette palette = Palette.from(wallpaper).generate();
             extractedColors.updatePalette(palette);
+            // We extract colors for the hotseat separately,
+            // since it only considers the lower part of the wallpaper.
+            // TODO(twickham): update Palette library to 23.3.1 or higher,
+            // which fixes a bug with using regions (b/28349435).
+            Palette hotseatPalette = Palette.from(wallpaper)
+                    .setRegion(0, (int) (wallpaper.getHeight() * (1f - HOTSEAT_FRACTION)),
+                            wallpaper.getWidth(), wallpaper.getHeight())
+                    .clearFilters()
+                    .generate();
+            extractedColors.updateHotseatPalette(hotseatPalette);
         }
 
         // Save the extracted colors and wallpaper id to LauncherProvider.
diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java
index 4d17ff7..e545288 100644
--- a/src/com/android/launcher3/dynamicui/ExtractedColors.java
+++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
 import android.support.v7.graphics.Palette;
 import android.util.Log;
 
@@ -35,26 +36,30 @@
 
     // These color profile indices should NOT be changed, since they are used when saving and
     // loading extracted colors. New colors should always be added at the end.
-    public static final int DEFAULT_INDEX = 0;
-    public static final int VIBRANT_INDEX = 1;
-    public static final int VIBRANT_DARK_INDEX = 2;
-    public static final int VIBRANT_LIGHT_INDEX = 3;
-    public static final int MUTED_INDEX = 4;
-    public static final int MUTED_DARK_INDEX = 5;
-    public static final int MUTED_LIGHT_INDEX = 6;
+    public static final int VERSION_INDEX = 0;
+    public static final int HOTSEAT_INDEX = 1;
+    // public static final int VIBRANT_INDEX = 2;
+    // public static final int VIBRANT_DARK_INDEX = 3;
+    // public static final int VIBRANT_LIGHT_INDEX = 4;
+    // public static final int MUTED_INDEX = 5;
+    // public static final int MUTED_DARK_INDEX = 6;
+    // public static final int MUTED_LIGHT_INDEX = 7;
 
-    public static final int NUM_COLOR_PROFILES = 7;
+    public static final int NUM_COLOR_PROFILES = 1;
+    private static final int VERSION = 1;
 
     private static final String COLOR_SEPARATOR = ",";
 
     private int[] mColors;
 
     public ExtractedColors() {
-        mColors = new int[NUM_COLOR_PROFILES];
+        // The first entry is reserved for the version number.
+        mColors = new int[NUM_COLOR_PROFILES + 1];
+        mColors[VERSION_INDEX] = VERSION;
     }
 
     public void setColorAtIndex(int index, int color) {
-        if (index >= 0 && index < mColors.length) {
+        if (index > VERSION_INDEX && index < mColors.length) {
             mColors[index] = color;
         } else {
             Log.e(TAG, "Attempted to set a color at an invalid index " + index);
@@ -89,17 +94,21 @@
      */
     public void load(Context context) {
         String encodedString = Utilities.getPrefs(context).getString(
-                ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, DEFAULT_COLOR + "");
+                ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, VERSION + "");
 
         decodeFromString(encodedString);
+
+        if (mColors[VERSION_INDEX] != VERSION) {
+            ExtractionUtils.startColorExtractionService(context);
+        }
     }
 
     /** @param index must be one of the index values defined at the top of this class. */
-    public int getColor(int index) {
-        if (index >= 0 && index < mColors.length) {
+    public int getColor(int index, int defaultColor) {
+        if (index > VERSION_INDEX && index < mColors.length) {
             return mColors[index];
         }
-        return DEFAULT_COLOR;
+        return defaultColor;
     }
 
     /**
@@ -112,20 +121,39 @@
                 setColorAtIndex(i, ExtractedColors.DEFAULT_COLOR);
             }
         } else {
-            setColorAtIndex(ExtractedColors.DEFAULT_INDEX,
-                    ExtractedColors.DEFAULT_COLOR);
-            setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
-                    palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
-            setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
-                    palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
-            setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
-                    palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
-            setColorAtIndex(ExtractedColors.MUTED_INDEX,
-                    palette.getMutedColor(ExtractedColors.DEFAULT_COLOR));
-            setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
-                    palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
-            setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
-                    palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+            // We currently don't use any of the colors defined by the Palette API,
+            // but this is how we would add them if we ever need them.
+
+            // setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
+                // palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
+            // setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
+                // palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
+            // setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
+                // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+            // setColorAtIndex(ExtractedColors.MUTED_INDEX,
+                // palette.getMutedColor(DEFAULT_COLOR));
+            // setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
+                // palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
+            // setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
+                // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
         }
     }
+
+    /**
+     * The hotseat's color is defined as follows:
+     * - 12% black for super light wallpaper
+     * - 18% white for super dark
+     * - 25% white otherwise
+     */
+    public void updateHotseatPalette(Palette hotseatPalette) {
+        int hotseatColor;
+        if (hotseatPalette != null && ExtractionUtils.isSuperLight(hotseatPalette)) {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.BLACK, (int) (0.12f * 255));
+        } else if (hotseatPalette != null && ExtractionUtils.isSuperDark(hotseatPalette)) {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.18f * 255));
+        } else {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.25f * 255));
+        }
+        setColorAtIndex(HOTSEAT_INDEX, hotseatColor);
+    }
 }
diff --git a/src/com/android/launcher3/dynamicui/ExtractionUtils.java b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
index 0b28ba6..6dc0035 100644
--- a/src/com/android/launcher3/dynamicui/ExtractionUtils.java
+++ b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
@@ -20,11 +20,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v7.graphics.Palette;
 
 import com.android.launcher3.Utilities;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.List;
 
 /**
  * Contains helper fields and methods related to extracting colors from the wallpaper.
@@ -34,6 +38,7 @@
     public static final String WALLPAPER_ID_PREFERENCE_KEY = "pref_wallpaperId";
 
     private static final int FLAG_SET_SYSTEM = 1 << 0; // TODO: use WallpaperManager.FLAG_SET_SYSTEM
+    private static final float MIN_CONTRAST_RATIO = 2f;
 
     /**
      * Extract colors in the :wallpaper-chooser process, if the wallpaper id has changed.
@@ -46,12 +51,17 @@
             @Override
             public void run() {
                 if (hasWallpaperIdChanged(context)) {
-                    context.startService(new Intent(context, ColorExtractionService.class));
+                    startColorExtractionService(context);
                 }
             }
         });
     }
 
+    /** Starts the {@link ColorExtractionService} without checking the wallpaper id */
+    public static void startColorExtractionService(Context context) {
+        context.startService(new Intent(context, ColorExtractionService.class));
+    }
+
     private static boolean hasWallpaperIdChanged(Context context) {
         if (!Utilities.isNycOrAbove()) {
             // TODO: update an id in sharedprefs in onWallpaperChanged broadcast, and read it here.
@@ -72,4 +82,36 @@
             return -1;
         }
     }
+
+    public static boolean isSuperLight(Palette p) {
+        return !isLegibleOnWallpaper(Color.WHITE, p.getSwatches());
+    }
+
+    public static boolean isSuperDark(Palette p) {
+        return !isLegibleOnWallpaper(Color.BLACK, p.getSwatches());
+    }
+
+    /**
+     * Given a color, returns true if that color is legible on
+     * the given wallpaper color swatches, else returns false.
+     */
+    private static boolean isLegibleOnWallpaper(int color, List<Palette.Swatch> wallpaperSwatches) {
+        int legiblePopulation = 0;
+        int illegiblePopulation = 0;
+        for (Palette.Swatch swatch : wallpaperSwatches) {
+            if (isLegible(color, swatch.getRgb())) {
+                legiblePopulation += swatch.getPopulation();
+            } else {
+                illegiblePopulation += swatch.getPopulation();
+            }
+        }
+        return legiblePopulation > illegiblePopulation;
+    }
+
+    /** @return Whether the foreground color is legible on the background color. */
+    private static boolean isLegible(int foreground, int background) {
+        background = ColorUtils.setAlphaComponent(background, 255);
+        return ColorUtils.calculateContrast(foreground, background) >= MIN_CONTRAST_RATIO;
+    }
+
 }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7dc8155..1ebe8fd 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -330,8 +330,13 @@
     }
 
     public void startEditingFolderName() {
-        mFolderName.setHint("");
-        mIsEditingName = true;
+        post(new Runnable() {
+            @Override
+            public void run() {
+                mFolderName.setHint("");
+                mIsEditingName = true;
+            }
+        });
     }
 
     public void dismissEditingName() {
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
new file mode 100644
index 0000000..68d9b8c
--- /dev/null
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -0,0 +1,216 @@
+package com.android.launcher3.logging;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.ProviderConfig;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around {@link Log} to allow writing to a file.
+ * This class can safely be called from main thread.
+ *
+ * Note: This should only be used for logging errors which have a persistent effect on user's data,
+ * but whose effect may not be visible immediately.
+ */
+public final class FileLog {
+
+    private static final String FILE_NAME_PREFIX = "log-";
+    private static final DateFormat DATE_FORMAT =
+            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
+
+    private static final long MAX_LOG_FILE_SIZE = 4 << 20;  // 4 mb
+
+    private static Handler sHandler = null;
+    private static File sLogsDirectory = null;
+
+    public static void setDir(File logsDir) {
+        sLogsDirectory = logsDir;
+    }
+
+    public static void d(String tag, String msg, Exception e) {
+        Log.d(tag, msg, e);
+        print(tag, msg, e);
+    }
+
+    public static void d(String tag, String msg) {
+        Log.d(tag, msg);
+        print(tag, msg);
+    }
+
+    public static void e(String tag, String msg, Exception e) {
+        Log.e(tag, msg, e);
+        print(tag, msg, e);
+    }
+
+    public static void e(String tag, String msg) {
+        Log.e(tag, msg);
+        print(tag, msg);
+    }
+
+    public static void print(String tag, String msg) {
+        print(tag, msg, null);
+    }
+
+    public static void print(String tag, String msg, Exception e) {
+        if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+            return;
+        }
+        String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
+        if (e != null) {
+            out += "\n" + Log.getStackTraceString(e);
+        }
+        Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
+    }
+
+    private static Handler getHandler() {
+        synchronized (DATE_FORMAT) {
+            if (sHandler == null) {
+                HandlerThread thread = new HandlerThread("file-logger");
+                thread.start();
+                sHandler = new Handler(thread.getLooper(), new LogWriterCallback());
+            }
+        }
+        return sHandler;
+    }
+
+    /**
+     * Blocks until all the pending logs are written to the disk
+     * @param out if not null, all the persisted logs are copied to the writer.
+     */
+    public static void flushAll(PrintWriter out) throws InterruptedException {
+        if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+            return;
+        }
+        CountDownLatch latch = new CountDownLatch(1);
+        Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
+                Pair.create(out, latch)).sendToTarget();
+
+        latch.await(2, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Writes logs to the file.
+     * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
+     * Logs older than 36 hours are purged.
+     */
+    private static class LogWriterCallback implements Handler.Callback {
+
+        private static final long CLOSE_DELAY = 5000;  // 5 seconds
+
+        private static final int MSG_WRITE = 1;
+        private static final int MSG_CLOSE = 2;
+        private static final int MSG_FLUSH = 3;
+
+        private String mCurrentFileName = null;
+        private PrintWriter mCurrentWriter = null;
+
+        private void closeWriter() {
+            Utilities.closeSilently(mCurrentWriter);
+            mCurrentWriter = null;
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            if (sLogsDirectory == null || !ProviderConfig.IS_DOGFOOD_BUILD) {
+                return true;
+            }
+            switch (msg.what) {
+                case MSG_WRITE: {
+                    Calendar cal = Calendar.getInstance();
+                    // suffix with 0 or 1 based on the day of the year.
+                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+
+                    if (!fileName.equals(mCurrentFileName)) {
+                        closeWriter();
+                    }
+
+                    try {
+                        if (mCurrentWriter == null) {
+                            mCurrentFileName = fileName;
+
+                            boolean append = false;
+                            File logFile = new File(sLogsDirectory, fileName);
+                            if (logFile.exists()) {
+                                Calendar modifiedTime = Calendar.getInstance();
+                                modifiedTime.setTimeInMillis(logFile.lastModified());
+
+                                // If the file was modified more that 36 hours ago, purge the file.
+                                // We use instead of 24 to account for day-365 followed by day-1
+                                modifiedTime.add(Calendar.HOUR, 36);
+                                append = cal.before(modifiedTime)
+                                        && logFile.length() < MAX_LOG_FILE_SIZE;
+                            }
+                            mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
+                        }
+
+                        mCurrentWriter.println((String) msg.obj);
+                        mCurrentWriter.flush();
+
+                        // Auto close file stream after some time.
+                        sHandler.removeMessages(MSG_CLOSE);
+                        sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
+                    } catch (Exception e) {
+                        Log.e("FileLog", "Error writing logs to file", e);
+                        // Close stream, will try reopening during next log
+                        closeWriter();
+                    }
+                    return true;
+                }
+                case MSG_CLOSE: {
+                    closeWriter();
+                    return true;
+                }
+                case MSG_FLUSH: {
+                    closeWriter();
+                    Pair<PrintWriter, CountDownLatch> p =
+                            (Pair<PrintWriter, CountDownLatch>) msg.obj;
+
+                    if (p.first != null) {
+                        dumpFile(p.first, FILE_NAME_PREFIX + 0);
+                        dumpFile(p.first, FILE_NAME_PREFIX + 1);
+                    }
+                    p.second.countDown();
+                    return true;
+                }
+            }
+            return true;
+        }
+    }
+
+    private static void dumpFile(PrintWriter out, String fileName) {
+        File logFile = new File(sLogsDirectory, fileName);
+        if (logFile.exists()) {
+
+            BufferedReader in = null;
+            try {
+                in = new BufferedReader(new FileReader(logFile));
+                out.println();
+                out.println("--- logfile: " + fileName + " ---");
+                String line;
+                while ((line = in.readLine()) != null) {
+                    out.println(line);
+                }
+            } catch (Exception e) {
+                // ignore
+            } finally {
+                Utilities.closeSilently(in);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/LauncherBackupAgentTest.java b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
new file mode 100644
index 0000000..020a557
--- /dev/null
+++ b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
@@ -0,0 +1,74 @@
+package com.android.launcher3;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+/**
+ * Tests for {@link LauncherBackupAgent}
+ */
+@MediumTest
+public class LauncherBackupAgentTest extends AndroidTestCase {
+
+    public void testGetProfileId() throws Exception {
+        SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
+        assertEquals(23, new LauncherBackupAgent().getDefaultProfileId(db));
+    }
+
+    public void testMigrateProfileId() throws Exception {
+        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+        // Add some dummy data
+        for (int i = 0; i < 5; i++) {
+            ContentValues values = new ContentValues();
+            values.put(Favorites._ID, i);
+            values.put(Favorites.TITLE, "item " + i);
+            db.insert(Favorites.TABLE_NAME, null, values);
+        }
+        // Verify item add
+        assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+
+        new LauncherBackupAgent().migrateProfileId(db, 33);
+
+        // verify data migrated
+        assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
+        assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+
+        // Verify default value changed
+        ContentValues values = new ContentValues();
+        values.put(Favorites._ID, 100);
+        values.put(Favorites.TITLE, "item 100");
+        db.insert(Favorites.TABLE_NAME, null, values);
+        assertEquals(6, getCount(db, "select * from favorites where profileId = 33"));
+    }
+
+    private int getCount(SQLiteDatabase db, String sql) {
+        Cursor c = db.rawQuery(sql, null);
+        try {
+            return c.getCount();
+        } finally {
+            c.getCount();
+        }
+    }
+
+    private class MyDatabaseHelper extends DatabaseHelper {
+
+        private final long mProfileId;
+
+        public MyDatabaseHelper(long profileId) {
+            super(getContext(), null, null);
+            mProfileId = profileId;
+        }
+
+        @Override
+        public long getDefaultUserSerial() {
+            return mProfileId;
+        }
+
+        protected void onEmptyDbCreated() { }
+    }
+}
diff --git a/tests/src/com/android/launcher3/logging/FileLogTest.java b/tests/src/com/android/launcher3/logging/FileLogTest.java
new file mode 100644
index 0000000..c24cc3f
--- /dev/null
+++ b/tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -0,0 +1,77 @@
+package com.android.launcher3.logging;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Calendar;
+
+/**
+ * Tests for {@link FileLog}
+ */
+@SmallTest
+public class FileLogTest extends AndroidTestCase {
+
+    private File mTempDir;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        int count = 0;
+        do {
+            mTempDir = new File(getContext().getCacheDir(), "log-test-" + (count++));
+        } while(!mTempDir.mkdir());
+
+        FileLog.setDir(mTempDir);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Clear existing logs
+        new File(mTempDir, "log-0").delete();
+        new File(mTempDir, "log-1").delete();
+        mTempDir.delete();
+        super.tearDown();
+    }
+
+    public void testPrintLog() throws Exception {
+        FileLog.print("Testing", "hoolalala");
+        StringWriter writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("hoolalala"));
+
+        FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+        writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("abracadabra"));
+        // Exception is also printed
+        assertTrue(writer.toString().contains("cat! cat!"));
+
+        // Old logs still present after flush
+        assertTrue(writer.toString().contains("hoolalala"));
+    }
+
+    public void testOldFileTruncated() throws Exception {
+        FileLog.print("Testing", "hoolalala");
+        StringWriter writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("hoolalala"));
+
+        Calendar threeDaysAgo = Calendar.getInstance();
+        threeDaysAgo.add(Calendar.HOUR, -72);
+        new File(mTempDir, "log-0").setLastModified(threeDaysAgo.getTimeInMillis());
+        new File(mTempDir, "log-1").setLastModified(threeDaysAgo.getTimeInMillis());
+
+        FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+        writer = new StringWriter();
+        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(writer.toString().contains("abracadabra"));
+        // Exception is also printed
+        assertTrue(writer.toString().contains("cat! cat!"));
+
+        // Old logs have been truncated
+        assertFalse(writer.toString().contains("hoolalala"));
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/TestLauncherProvider.java b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
index a11013f..bd3e86c 100644
--- a/tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
@@ -1,6 +1,7 @@
 package com.android.launcher3.util;
 
 import android.content.Context;
+import android.database.sqlite.SQLiteOpenHelper;
 
 import com.android.launcher3.LauncherProvider;
 
@@ -17,16 +18,21 @@
     @Override
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = new MyDatabaseHelper(getContext(), this);
+            mOpenHelper = new MyDatabaseHelper(getContext());
         }
     }
 
+    public SQLiteOpenHelper getHelper() {
+        createDbIfNotExists();
+        return mOpenHelper;
+    }
+
     @Override
     protected void notifyListeners() { }
 
     private static class MyDatabaseHelper extends DatabaseHelper {
-        public MyDatabaseHelper(Context context, LauncherProvider provider) {
-            super(context, provider, null, null);
+        public MyDatabaseHelper(Context context) {
+            super(context, null, null);
             initIds();
         }