Merge "Adding listener support for color extraction changes Changing the apps-search layout to use theme attribute instead of hard coded layout id" into ub-launcher3-dorval-polish
diff --git a/res/layout/deep_shortcut.xml b/res/layout/deep_shortcut.xml
index 7744906..85caba4 100644
--- a/res/layout/deep_shortcut.xml
+++ b/res/layout/deep_shortcut.xml
@@ -26,11 +26,12 @@
         android:background="?android:attr/selectableItemBackground"
         android:gravity="start|center_vertical"
         android:textAlignment="viewStart"
-        android:paddingStart="@dimen/bg_popup_item_height"
+        android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
         android:paddingEnd="@dimen/popup_padding_end"
         android:drawableEnd="@drawable/deep_shortcuts_drag_handle"
         android:drawablePadding="@dimen/deep_shortcut_drawable_padding"
         android:textSize="14sp"
+        android:textColor="?android:attr/textColorPrimary"
         android:fontFamily="sans-serif"
         launcher:layoutHorizontal="true"
         launcher:iconDisplay="shortcut_popup"
diff --git a/res/layout/notification.xml b/res/layout/notification.xml
index 4250e1e..f955c6b 100644
--- a/res/layout/notification.xml
+++ b/res/layout/notification.xml
@@ -35,7 +35,7 @@
             android:layout_height="@dimen/notification_header_height"
             android:paddingStart="@dimen/notification_padding_start"
             android:paddingEnd="@dimen/notification_padding_end"
-            android:background="@color/notification_header_background_color"
+            android:background="@color/popup_header_background_color"
             android:elevation="@dimen/notification_elevation">
             <TextView
                 android:id="@+id/notification_text"
@@ -67,7 +67,7 @@
             android:id="@+id/divider"
             android:layout_width="match_parent"
             android:layout_height="@dimen/popup_item_divider_height"
-            android:background="@color/divider_color"
+            android:background="?android:attr/listDivider"
             android:layout_below="@id/main_view"/>
 
         <include layout="@layout/notification_footer"
diff --git a/res/layout/notification_footer.xml b/res/layout/notification_footer.xml
index f1f5724..ed2212b 100644
--- a/res/layout/notification_footer.xml
+++ b/res/layout/notification_footer.xml
@@ -22,7 +22,7 @@
     android:elevation="@dimen/notification_elevation"
     android:clipChildren="false"
     android:layout_gravity="center_vertical"
-    android:background="@color/notification_background_color">
+    android:background="@color/popup_background_color">
 
     <LinearLayout
         android:id="@+id/icon_row"
diff --git a/res/layout/notification_main.xml b/res/layout/notification_main.xml
index 8fa1b68..ce4e137 100644
--- a/res/layout/notification_main.xml
+++ b/res/layout/notification_main.xml
@@ -28,7 +28,7 @@
         android:layout_height="match_parent"
         android:orientation="vertical"
         android:gravity="center_vertical"
-        android:background="@color/notification_background_color"
+        android:background="@color/popup_background_color"
         android:paddingStart="@dimen/notification_padding_start"
         android:paddingEnd="@dimen/notification_main_text_padding_end">
         <TextView
diff --git a/res/layout/system_shortcut.xml b/res/layout/system_shortcut.xml
index 26e7697..0952703 100644
--- a/res/layout/system_shortcut.xml
+++ b/res/layout/system_shortcut.xml
@@ -27,9 +27,10 @@
         android:background="?android:attr/selectableItemBackground"
         android:gravity="start|center_vertical"
         android:textAlignment="viewStart"
-        android:paddingStart="@dimen/bg_popup_item_height"
+        android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
         android:paddingEnd="@dimen/popup_padding_end"
         android:textSize="14sp"
+        android:textColor="?android:attr/textColorPrimary"
         android:fontFamily="sans-serif"
         launcher:iconDisplay="shortcut_popup"
         launcher:layoutHorizontal="true" />
diff --git a/res/layout/system_shortcut_icons.xml b/res/layout/system_shortcut_icons.xml
index 9dc56e4..676be8e 100644
--- a/res/layout/system_shortcut_icons.xml
+++ b/res/layout/system_shortcut_icons.xml
@@ -21,4 +21,4 @@
     android:layout_height="@dimen/system_shortcut_header_height"
     android:orientation="horizontal"
     android:gravity="end|center_vertical"
-    android:background="@color/notification_header_background_color" />
+    android:background="@color/popup_header_background_color" />
diff --git a/res/raw/downgrade_schema.json b/res/raw/downgrade_schema.json
new file mode 100644
index 0000000..3c1b64f
--- /dev/null
+++ b/res/raw/downgrade_schema.json
@@ -0,0 +1,20 @@
+{
+  // Note: Comments are not supported in JSON schema, but android parser is lenient.
+
+  // Maximum DB version supported by this schema
+  "version" : 27,
+
+  // Downgrade from 27 to 26. Empty array indicates, the DB is compatible
+  "downgrade_to_26" : [],
+  "downgrade_to_25" : [],
+  "downgrade_to_24" : [],
+  "downgrade_to_23" : [],
+  "downgrade_to_22" : [
+    "ALTER TABLE favorites RENAME TO temp_favorites;",
+    "CREATE TABLE favorites(_id INTEGER PRIMARY KEY, title TEXT, intent TEXT, container INTEGER, screen INTEGER, cellX INTEGER, cellY INTEGER, spanX INTEGER, spanY INTEGER, itemType INTEGER, appWidgetId INTEGER NOT NULL DEFAULT - 1, iconPackage TEXT, iconResource TEXT, icon BLOB, appWidgetProvider TEXT, modified INTEGER NOT NULL DEFAULT 0, restored INTEGER NOT NULL DEFAULT 0, profileId INTEGER DEFAULT 0, rank INTEGER NOT NULL DEFAULT 0);",
+    "INSERT INTO favorites SELECT _id, title, intent, container, screen, cellX, cellY, spanX, spanY, itemType, appWidgetId, iconPackage, iconResource, icon, appWidgetProvider, modified, restored, profileId, rank FROM temp_favorites;",
+    "DROP TABLE temp_favorites;"
+  ]
+
+  // Missing values indicate the DB is not compatible
+}
\ No newline at end of file
diff --git a/res/values-bs-rBA/strings.xml b/res/values-bs-rBA/strings.xml
index 8e1234e..63aaaba 100644
--- a/res/values-bs-rBA/strings.xml
+++ b/res/values-bs-rBA/strings.xml
@@ -43,7 +43,7 @@
     <string name="out_of_space" msgid="4691004494942118364">"Na ovom početnom ekranu nema više prostora."</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"Nema više prostora u ladici Favoriti"</string>
     <string name="all_apps_button_label" msgid="8130441508702294465">"Spisak aplikacija"</string>
-    <string name="all_apps_home_button_label" msgid="252062713717058851">"Tipka za početak"</string>
+    <string name="all_apps_home_button_label" msgid="252062713717058851">"Početna"</string>
     <string name="remove_drop_target_label" msgid="7812859488053230776">"Ukloni"</string>
     <string name="uninstall_drop_target_label" msgid="4722034217958379417">"Deinstaliraj"</string>
     <string name="app_info_drop_target_label" msgid="692894985365717661">"Informacije o aplikaciji"</string>
@@ -69,7 +69,7 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Ime fascikle je promijenjeno u <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format" msgid="6629239338071103179">"Fascikla: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="widget_button_text" msgid="2880537293434387943">"Dodaci"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Pozadine"</string>
+    <string name="wallpaper_button_text" msgid="8404103075899945851">"Pozadinske slike"</string>
     <string name="settings_button_text" msgid="8119458837558863227">"Postavke"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogućio vaš administrator"</string>
     <string name="accessibility_action_overview" msgid="6257665857640347026">"Pregled"</string>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a3d26e3..47bc5b5 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -30,12 +30,11 @@
     <color name="spring_loaded_panel_color">#40FFFFFF</color>
     <color name="spring_loaded_highlighted_panel_border_color">#FFF</color>
 
-    <!-- Notifications -->
+    <!-- Popup container -->
+    <color name="popup_header_background_color">#EEEEEE</color> <!-- Gray 200 -->
+    <color name="popup_background_color">#FFF</color>
     <color name="notification_icon_default_color">#757575</color> <!-- Gray 600 -->
-    <color name="notification_header_background_color">#EEEEEE</color> <!-- Gray 200 -->
-    <color name="notification_background_color">#FFF</color>
     <color name="notification_color_beneath">#E0E0E0</color> <!-- Gray 300 -->
-    <color name="divider_color">@color/notification_color_beneath</color>
 
     <color name="icon_background">#E0E0E0</color> <!-- Gray 300 -->
     <color name="legacy_icon_background">#FFFFFF</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 7dd9e53..6add64d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -154,7 +154,7 @@
 
 <!-- Deep shortcuts -->
     <dimen name="deep_shortcuts_elevation">9dp</dimen>
-    <dimen name="bg_popup_item_width">208dp</dimen>
+    <dimen name="bg_popup_item_width">220dp</dimen>
     <dimen name="bg_popup_item_height">56dp</dimen>
     <dimen name="popup_items_spacing">4dp</dimen>
     <dimen name="pre_drag_view_scale">6dp</dimen>
@@ -163,23 +163,27 @@
     <dimen name="deep_shortcut_icon_size">36dp</dimen>
     <dimen name="deep_shortcut_drawable_padding">8dp</dimen>
     <dimen name="deep_shortcut_drag_handle_size">16dp</dimen>
-    <dimen name="popup_padding_start">6dp</dimen>
+    <dimen name="popup_padding_start">10dp</dimen>
     <dimen name="popup_padding_end">16dp</dimen>
     <dimen name="popup_arrow_width">10dp</dimen>
     <dimen name="popup_arrow_height">8dp</dimen>
     <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
     <!-- popup_padding_start + deep_shortcut_icon_size / 2 -->
-    <!-- Note that this works for right-aligned shortcuts, too, because
-         popup_padding_end + deep_shortcut_drag_handle_size / 2 also equals 24dp-->
-    <dimen name="popup_arrow_horizontal_center">24dp</dimen>
-    <!-- popup_arrow_center - popup_arrow_width / 2-->
-    <dimen name="popup_arrow_horizontal_offset">19dp</dimen>
+    <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
+    <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
+    <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
+    <!-- popup_arrow_center_start - popup_arrow_width / 2-->
+    <dimen name="popup_arrow_horizontal_offset_start">23dp</dimen>
+    <!-- popup_arrow_center_end - popup_arrow_width / 2-->
+    <dimen name="popup_arrow_horizontal_offset_end">19dp</dimen>
     <dimen name="popup_arrow_corner_radius">2dp</dimen>
-    <!-- popup_item_width - icon_size - padding_start - drawable_padding -->
-    <dimen name="deep_shortcuts_divider_width">158dp</dimen>
+    <!-- popup_padding_start + icon_size + 10dp -->
+    <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
+    <!-- popup_item_width - deep_shortcuts_text_padding_start -->
+    <dimen name="deep_shortcuts_divider_width">164dp</dimen>
     <dimen name="system_shortcut_icon_size">24dp</dimen>
-    <!-- popup_arrow_center - system_shortcut_icon_size / 2 -->
-    <dimen name="system_shortcut_margin_start">12dp</dimen>
+    <!-- popup_arrow_center_start - system_shortcut_icon_size / 2 -->
+    <dimen name="system_shortcut_margin_start">16dp</dimen>
     <dimen name="system_shortcut_header_height">40dp</dimen>
     <dimen name="system_shortcut_header_icon_touch_size">48dp</dimen>
     <!-- (touch_size - icon_size) / 2 -->
@@ -200,15 +204,15 @@
     <dimen name="notification_footer_height">32dp</dimen>
     <dimen name="notification_header_text_size">13sp</dimen>
     <dimen name="notification_header_count_text_size">12sp</dimen>
-    <dimen name="notification_main_text_size">15sp</dimen>
+    <dimen name="notification_main_text_size">14sp</dimen>
     <dimen name="notification_icon_size">24dp</dimen>
     <dimen name="notification_footer_icon_size">18dp</dimen>
     <!-- notification_icon_size + notification_padding_end + 16dp padding between icon and text -->
     <dimen name="notification_main_text_padding_end">52dp</dimen>
     <dimen name="notification_elevation">2dp</dimen>
     <dimen name="horizontal_ellipsis_size">18dp</dimen>
-    <!-- arrow_horizontal_offset - (ellipsis_size - arrow_width) / 2 -->
-    <dimen name="horizontal_ellipsis_offset">15dp</dimen>
+    <!-- arrow_horizontal_offset_start - (ellipsis_size - arrow_width) / 2 -->
+    <dimen name="horizontal_ellipsis_offset">19dp</dimen>
     <dimen name="popup_item_divider_height">0.5dp</dimen>
     <dimen name="swipe_helper_falsing_threshold">70dp</dimen>
 
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
index 7e833a0..299e92e 100644
--- a/res/xml/backupscheme.xml
+++ b/res/xml/backupscheme.xml
@@ -3,5 +3,6 @@
 
     <include domain="database" path="launcher.db" />
     <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
+    <include domain="file" path="downgrade_schema.json" />
 
 </full-backup-content>
\ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index bd53b4d..c84a431 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -56,6 +56,7 @@
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.DbDowngradeHelper;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -64,6 +65,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.reflect.Method;
@@ -77,18 +79,12 @@
     private static final String TAG = "LauncherProvider";
     private static final boolean LOGD = false;
 
+    private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
+
     /**
      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
      */
-    private static final int SCHEMA_VERSION = 27;
-    /**
-     * Represents the actual data. It could include additional validations and normalizations added
-     * overtime. These must be backwards compatible, else we risk breaking old devices during
-     * restore or binary version downgrade.
-     */
-    private static final int DATA_VERSION = 3;
-
-    private static final String PREF_KEY_DATA_VERISON = "provider_data_version";
+    public static final int SCHEMA_VERSION = 27;
 
     public static final String AUTHORITY = (BuildConfig.APPLICATION_ID + ".settings").intern();
 
@@ -703,47 +699,30 @@
         @Override
         public void onOpen(SQLiteDatabase db) {
             super.onOpen(db);
-            SharedPreferences prefs = mContext
-                    .getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, 0);
-            int oldVersion = prefs.getInt(PREF_KEY_DATA_VERISON, 0);
-            if (oldVersion != DATA_VERSION) {
-                // Only run the data upgrade path for an existing db.
-                if (!Utilities.getPrefs(mContext).getBoolean(EMPTY_DATABASE_CREATED, false)) {
-                    try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-                        onDataUpgrade(db, oldVersion);
-                        t.commit();
-                    } catch (Exception e) {
-                        Log.d(TAG, "Error updating data version, ignoring", e);
-                        return;
-                    }
-                }
-                prefs.edit().putInt(PREF_KEY_DATA_VERISON, DATA_VERSION).apply();
+
+            File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE);
+            if (!schemaFile.exists()) {
+                handleOneTimeDataUpgrade(db);
             }
+            DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext,
+                    R.raw.downgrade_schema);
         }
 
         /**
-         * Called when the data is updated as part of app update. It can be called multiple times
-         * with old version, even though it had been run before. The changes made here must be
-         * backwards compatible, else we risk breaking old devices during restore or binary
-         * version downgrade.
+         * One-time data updated before support of onDowngrade was added. This update is backwards
+         * compatible and can safely be run multiple times.
+         * Note: No new logic should be added here after release, as the new logic might not get
+         * executed on an existing device.
+         * TODO: Move this to db upgrade path, once the downgrade path is released.
          */
-        protected void onDataUpgrade(SQLiteDatabase db, int oldVersion) {
-            switch (oldVersion) {
-                case 0:
-                case 1: {
-                    // Remove "profile extra"
-                    UserManagerCompat um = UserManagerCompat.getInstance(mContext);
-                    for (UserHandle user : um.getUserProfiles()) {
-                        long serial = um.getSerialNumberForUser(user);
-                        String sql = "update favorites set intent = replace(intent, "
-                                + "';l.profile=" + serial + ";', ';') where itemType = 0;";
-                        db.execSQL(sql);
-                    }
-                }
-                case 2:
-                case 3:
-                    // data updated
-                    return;
+        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
+            // Remove "profile extra"
+            UserManagerCompat um = UserManagerCompat.getInstance(mContext);
+            for (UserHandle user : um.getUserProfiles()) {
+                long serial = um.getSerialNumberForUser(user);
+                String sql = "update favorites set intent = replace(intent, "
+                        + "';l.profile=" + serial + ";', ';') where itemType = 0;";
+                db.execSQL(sql);
             }
         }
 
@@ -850,15 +829,14 @@
 
         @Override
         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion == 28 && newVersion == 27) {
-                // TODO: remove this check. This is only applicable for internal development/testing
-                // and for any released version of Launcher.
-                return;
+            try {
+                DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE))
+                        .onDowngrade(db, oldVersion, newVersion);
+            } catch (Exception e) {
+                Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion +
+                        ". Wiping databse.", e);
+                createEmptyDB(db);
             }
-            // This shouldn't happen -- throw our hands up in the air and start over.
-            Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
-                    ". Wiping databse.");
-            createEmptyDB(db);
         }
 
         /**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index ead1a9f..25e5749 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -75,6 +75,7 @@
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -2262,6 +2263,8 @@
             int previewSize = grid.folderIconSizePx;
             dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
             dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
+        } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
+            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
         }
 
         // Clear the pressed state if necessary
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
index f6b02aa..349b4ff 100644
--- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java
+++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
@@ -66,7 +66,7 @@
             if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
                 extractedColors.updateWallpaperThemePalette(null);
                 if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                    extractedColors.updateAllAppsGradientPalette(null);
+                    extractedColors.updateAllAppsGradientPalette(this);
                 }
             }
         } else {
@@ -79,10 +79,9 @@
             }
 
             if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                Palette wallpaperPalette = getWallpaperPalette();
-                extractedColors.updateWallpaperThemePalette(wallpaperPalette);
+                extractedColors.updateWallpaperThemePalette(getWallpaperPalette());
                 if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                    extractedColors.updateAllAppsGradientPalette(wallpaperPalette);
+                    extractedColors.updateAllAppsGradientPalette(this);
                 }
             }
         }
diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java
index 108a21f..e60a1bd 100644
--- a/src/com/android/launcher3/dynamicui/ExtractedColors.java
+++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.dynamicui;
 
+import android.app.WallpaperManager;
 import android.content.Context;
 import android.graphics.Color;
 import android.support.annotation.Nullable;
@@ -25,6 +26,7 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -163,15 +165,18 @@
                 ? defaultColor : wallpaperPalette.getVibrantColor(defaultColor));
     }
 
-    public void updateAllAppsGradientPalette(@Nullable Palette wallpaperPalette) {
-        // TODO b/37089857 will be modified to take the system extracted colors into account
-        int idx;
-        idx = ALLAPPS_GRADIENT_MAIN_INDEX;
-        setColorAtIndex(idx, wallpaperPalette == null
-                ? DEFAULT_VALUES[idx] : wallpaperPalette.getDarkVibrantColor(DEFAULT_VALUES[idx]));
-        idx = ALLAPPS_GRADIENT_SECONDARY_INDEX;
-        setColorAtIndex(idx, wallpaperPalette == null
-                ? DEFAULT_VALUES[idx] : wallpaperPalette.getVibrantColor(DEFAULT_VALUES[idx]));
+    public void updateAllAppsGradientPalette(Context context) {
+        // TODO use isAtLeastO when available
+        try {
+            WallpaperManager.class.getDeclaredMethod("getWallpaperColors", int.class);
+            ColorExtractor extractor = new ColorExtractor(context);
+            ColorExtractor.GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM);
+            setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, colors.getMainColor());
+            setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, colors.getSecondaryColor());
+        } catch (NoSuchMethodException e) {
+            setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, Color.WHITE);
+            setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, Color.WHITE);
+        }
     }
 
     public void addOnChangeListener(OnChangeListener listener) {
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java
new file mode 100644
index 0000000..153b529
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java
@@ -0,0 +1,136 @@
+package com.android.launcher3.dynamicui.colorextraction;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.launcher3.dynamicui.colorextraction.types.ExtractionType;
+import com.android.launcher3.dynamicui.colorextraction.types.Tonal;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * Class to process wallpaper colors and generate a tonal palette based on them.
+ *
+ * TODO remove this class if available by platform
+ */
+public class ColorExtractor {
+    private static final String TAG = "ColorExtractor";
+    private static final int FALLBACK_COLOR = Color.WHITE;
+
+    private int mMainFallbackColor = FALLBACK_COLOR;
+    private int mSecondaryFallbackColor = FALLBACK_COLOR;
+    private final GradientColors mSystemColors;
+    private final GradientColors mLockColors;
+    private final Context mContext;
+    private final ExtractionType mExtractionType;
+
+    public ColorExtractor(Context context) {
+        mContext = context;
+        mSystemColors = new GradientColors();
+        mLockColors = new GradientColors();
+        mExtractionType = new Tonal();
+        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
+
+        if (wallpaperManager == null) {
+            Log.w(TAG, "Can't listen to color changes!");
+        } else {
+            Parcelable wallpaperColorsObj;
+            try {
+                Method method = WallpaperManager.class
+                        .getDeclaredMethod("getWallpaperColors", int.class);
+
+                wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
+                        WallpaperManager.FLAG_SYSTEM);
+                extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mSystemColors);
+                wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
+                        WallpaperManager.FLAG_LOCK);
+                extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mLockColors);
+            } catch (Exception e) {
+                Log.e(TAG, "reflection failed", e);
+            }
+        }
+    }
+
+    public GradientColors getColors(int which) {
+        if (which == WallpaperManager.FLAG_LOCK) {
+            return mLockColors;
+        } else if (which == WallpaperManager.FLAG_SYSTEM) {
+            return mSystemColors;
+        } else {
+            throw new IllegalArgumentException("which should be either FLAG_SYSTEM or FLAG_LOCK");
+        }
+    }
+
+    private void extractInto(WallpaperColorsCompat inWallpaperColors, GradientColors outGradientColors) {
+        applyFallback(outGradientColors);
+        if (inWallpaperColors == null) {
+            return;
+        }
+        mExtractionType.extractInto(inWallpaperColors, outGradientColors);
+    }
+
+    private void applyFallback(GradientColors outGradientColors) {
+        outGradientColors.setMainColor(mMainFallbackColor);
+        outGradientColors.setSecondaryColor(mSecondaryFallbackColor);
+    }
+
+    public static class GradientColors {
+        private int mMainColor = FALLBACK_COLOR;
+        private int mSecondaryColor = FALLBACK_COLOR;
+        private boolean mSupportsDarkText;
+
+        public void setMainColor(int mainColor) {
+            mMainColor = mainColor;
+        }
+
+        public void setSecondaryColor(int secondaryColor) {
+            mSecondaryColor = secondaryColor;
+        }
+
+        public void setSupportsDarkText(boolean supportsDarkText) {
+            mSupportsDarkText = supportsDarkText;
+        }
+
+        public void set(GradientColors other) {
+            mMainColor = other.mMainColor;
+            mSecondaryColor = other.mSecondaryColor;
+            mSupportsDarkText = other.mSupportsDarkText;
+        }
+
+        public int getMainColor() {
+            return mMainColor;
+        }
+
+        public int getSecondaryColor() {
+            return mSecondaryColor;
+        }
+
+        public boolean supportsDarkText() {
+            return mSupportsDarkText;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || o.getClass() != getClass()) {
+                return false;
+            }
+            GradientColors other = (GradientColors) o;
+            return other.mMainColor == mMainColor &&
+                    other.mSecondaryColor == mSecondaryColor &&
+                    other.mSupportsDarkText == mSupportsDarkText;
+        }
+
+        @Override
+        public int hashCode() {
+            int code = mMainColor;
+            code = 31 * code + mSecondaryColor;
+            code = 31 * code + (mSupportsDarkText ? 0 : 1);
+            return code;
+        }
+    }
+}
+
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java
new file mode 100644
index 0000000..f80a675
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java
@@ -0,0 +1,69 @@
+package com.android.launcher3.dynamicui.colorextraction;
+
+import android.graphics.Color;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.util.List;
+
+/**
+ * A wrapper around platform implementation of WallpaperColors until the
+ * updated SDK is available.
+ *
+ * TODO remove this class if available by platform
+ */
+public class WallpaperColorsCompat implements Parcelable {
+
+    private final Parcelable mObject;
+
+    public WallpaperColorsCompat(Parcelable object) {
+        mObject = object;
+    }
+
+    private Object invokeMethod(String methodName) {
+        try {
+            return mObject.getClass().getDeclaredMethod(methodName).invoke(mObject);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        parcel.writeParcelable(mObject, i);
+    }
+
+    public static final Parcelable.Creator<WallpaperColorsCompat> CREATOR =
+            new Parcelable.Creator<WallpaperColorsCompat>() {
+                public WallpaperColorsCompat createFromParcel(Parcel source) {
+                    Parcelable object = source.readParcelable(null);
+                    return new WallpaperColorsCompat(object);
+                }
+
+                public WallpaperColorsCompat[] newArray(int size) {
+                    return new WallpaperColorsCompat[size];
+                }
+            };
+
+    public List<Pair<Color, Integer>> getColors() {
+        try {
+            return (List<Pair<Color, Integer>>) invokeMethod("getColors");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public boolean supportsDarkText() {
+        try {
+            return (Boolean) invokeMethod("supportsDarkText");
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java
new file mode 100644
index 0000000..166c7c6
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java
@@ -0,0 +1,23 @@
+package com.android.launcher3.dynamicui.colorextraction.types;
+
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
+import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
+
+
+/**
+ * Interface to allow various color extraction implementations.
+ *
+ * TODO remove this class if available by platform
+ */
+public interface ExtractionType {
+
+    /**
+     * Executes color extraction by reading WallpaperColors and setting
+     * main and secondary colors on GradientColors.
+     *
+     * @param inWallpaperColors where to read from
+     * @param outGradientColors object that should receive the colors
+     */
+    void extractInto(WallpaperColorsCompat inWallpaperColors,
+                     ColorExtractor.GradientColors outGradientColors);
+}
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java
new file mode 100644
index 0000000..1e165a3
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java
@@ -0,0 +1,299 @@
+package com.android.launcher3.dynamicui.colorextraction.types;
+
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.ColorUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
+import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
+
+import java.util.Comparator;
+
+
+/**
+ * Implementation of tonal color extraction
+ *
+ * TODO remove this class if available by platform
+ */
+public class Tonal implements ExtractionType {
+    private static final String TAG = "Tonal";
+
+    // Used for tonal palette fitting
+    private static final float FIT_WEIGHT_H = 1.0f;
+    private static final float FIT_WEIGHT_S = 1.0f;
+    private static final float FIT_WEIGHT_L = 10.0f;
+
+    private static final float MIN_COLOR_OCCURRENCE = 0.1f;
+    private static final float MIN_LUMINOSITY = 0.5f;
+
+    public void extractInto(WallpaperColorsCompat wallpaperColors,
+                            ColorExtractor.GradientColors gradientColors) {
+        if (wallpaperColors.getColors().size() == 0) {
+            return;
+        }
+        // Tonal is not really a sort, it takes a color from the extracted
+        // palette and finds a best fit amongst a collection of pre-defined
+        // palettes. The best fit is tweaked to be closer to the source color
+        // and replaces the original palette
+
+        // First find the most representative color in the image
+        populationSort(wallpaperColors);
+        // Calculate total
+        int total = 0;
+        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
+            total += weightedColor.second;
+        }
+
+        // Get bright colors that occur often enough in this image
+        Pair<Color, Integer> bestColor = null;
+        float[] hsl = new float[3];
+        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
+            float colorOccurrence = weightedColor.second / (float) total;
+            if (colorOccurrence < MIN_COLOR_OCCURRENCE) {
+                break;
+            }
+
+            int colorValue = weightedColor.first.toArgb();
+            ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
+                    Color.blue(colorValue), hsl);
+            if (hsl[2] > MIN_LUMINOSITY) {
+                bestColor = weightedColor;
+            }
+        }
+
+        // Fallback to first color
+        if (bestColor == null) {
+            bestColor = wallpaperColors.getColors().get(0);
+        }
+
+        int colorValue = bestColor.first.toArgb();
+        ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
+                hsl);
+        hsl[0] /= 360.0f; // normalize
+
+        // TODO, we're finding a tonal palette for a hue, not all components
+        TonalPalette palette = findTonalPalette(hsl[0]);
+
+        // Fall back to population sort if we couldn't find a tonal palette
+        if (palette == null) {
+            Log.w(TAG, "Could not find a tonal palette!");
+            return;
+        }
+
+        int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
+        if (fitIndex == -1) {
+            Log.w(TAG, "Could not find best fit!");
+            return;
+        }
+        float[] h = fit(palette.h, hsl[0], fitIndex,
+                Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
+        float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
+        float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
+
+
+        hsl[0] = fract(h[0]) * 360.0f;
+        hsl[1] = s[0];
+        hsl[2] = l[0];
+        gradientColors.setMainColor(ColorUtils.HSLToColor(hsl));
+
+        hsl[0] = fract(h[1]) * 360.0f;
+        hsl[1] = s[1];
+        hsl[2] = l[1];
+        gradientColors.setSecondaryColor(ColorUtils.HSLToColor(hsl));
+    }
+
+    private static void populationSort(@NonNull WallpaperColorsCompat wallpaperColors) {
+        wallpaperColors.getColors().sort(new Comparator<Pair<Color, Integer>>() {
+            @Override
+            public int compare(Pair<Color, Integer> a, Pair<Color, Integer> b) {
+                return b.second - a.second;
+            }
+        });
+    }
+
+    /**
+     * Offsets all colors by a delta, clamping values that go beyond what's
+     * supported on the color space.
+     * @param data what you want to fit
+     * @param v how big should be the offset
+     * @param index which index to calculate the delta against
+     * @param min minimum accepted value (clamp)
+     * @param max maximum accepted value (clamp)
+     * @return
+     */
+    private static float[] fit(float[] data, float v, int index, float min, float max) {
+        float[] fitData = new float[data.length];
+        float delta = v - data[index];
+
+        for (int i = 0; i < data.length; i++) {
+            fitData[i] = constrain(data[i] + delta, min, max);
+        }
+
+        return fitData;
+    }
+
+    // TODO no MathUtils
+    private static float constrain(float x, float min, float max) {
+        x = Math.min(x, max);
+        x = Math.max(x, min);
+        return x;
+    }
+
+    /*function adjustSatLumForFit(val, points, fitIndex) {
+        var fitValue = lerpBetweenPoints(points, fitIndex);
+        var diff = val - fitValue;
+
+        var newPoints = [];
+        for (var ii=0; ii<points.length; ii++) {
+            var point = [points[ii][0], points[ii][1]];
+            point[1] += diff;
+            if (point[1] > 1) point[1] = 1;
+            if (point[1] < 0) point[1] = 0;
+            newPoints[ii] = point;
+        }
+        return newPoints;
+    }*/
+
+    /**
+     * Finds the closest color in a palette, given another HSL color
+     *
+     * @param palette where to search
+     * @param h hue
+     * @param s saturation
+     * @param l lightness
+     * @return closest index or -1 if palette is empty.
+     */
+    private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
+        int minErrorIndex = -1;
+        float minError = Float.POSITIVE_INFINITY;
+
+        for (int i = 0; i < palette.h.length; i++) {
+            float error =
+                    FIT_WEIGHT_H * Math.abs(h - palette.h[i])
+                            + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
+                            + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
+            if (error < minError) {
+                minError = error;
+                minErrorIndex = i;
+            }
+        }
+
+        return minErrorIndex;
+    }
+
+    @Nullable
+    private static TonalPalette findTonalPalette(float h) {
+        TonalPalette best = null;
+        float error = Float.POSITIVE_INFINITY;
+
+        for (TonalPalette candidate : TONAL_PALETTES) {
+            if (h >= candidate.minHue && h <= candidate.maxHue) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
+                best = candidate;
+                break;
+            }
+
+            if (h <= candidate.minHue && candidate.minHue - h < error) {
+                best = candidate;
+                error = candidate.minHue - h;
+            } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
+                best = candidate;
+                error = h - candidate.maxHue;
+            } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
+                    && h - fract(candidate.maxHue) < error) {
+                best = candidate;
+                error = h - fract(candidate.maxHue);
+            } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
+                    && fract(candidate.minHue) - h < error) {
+                best = candidate;
+                error = fract(candidate.minHue) - h;
+            }
+        }
+
+        return best;
+    }
+
+    private static float fract(float v) {
+        return v - (float) Math.floor(v);
+    }
+
+    static class TonalPalette {
+        final float[] h;
+        final float[] s;
+        final float[] l;
+        final float minHue;
+        final float maxHue;
+
+        TonalPalette(float[] h, float[] s, float[] l) {
+            this.h = h;
+            this.s = s;
+            this.l = l;
+
+            float minHue = Float.POSITIVE_INFINITY;
+            float maxHue = Float.NEGATIVE_INFINITY;
+
+            for (float v : h) {
+                minHue = Math.min(v, minHue);
+                maxHue = Math.max(v, maxHue);
+            }
+
+            this.minHue = minHue;
+            this.maxHue = maxHue;
+        }
+    }
+
+    // Data definition of Material Design tonal palettes
+    // When the sort type is set to TONAL, these palettes are used to find
+    // a best fist. Each palette is defined as 10 HSL colors
+    private static final TonalPalette[] TONAL_PALETTES = {
+            // Orange
+            new TonalPalette(
+                    new float[] { 0.028f, 0.042f, 0.053f, 0.061f, 0.078f, 0.1f, 0.111f, 0.111f, 0.111f, 0.111f },
+                    new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.5f, 0.53f, 0.54f, 0.55f, 0.535f, 0.52f, 0.5f, 0.63f, 0.75f, 0.85f }
+            ),
+            // Yellow
+            new TonalPalette(
+                    new float[] { 0.111f, 0.111f, 0.125f, 0.133f, 0.139f, 0.147f, 0.156f, 0.156f, 0.156f, 0.156f },
+                    new float[] { 1f, 0.942f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.43f, 0.484f, 0.535f, 0.555f, 0.57f, 0.575f, 0.595f, 0.715f, 0.78f, 0.885f }
+            ),
+            // Green
+            new TonalPalette(
+                    new float[] { 0.325f, 0.336f, 0.353f, 0.353f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f },
+                    new float[] { 1f, 1f, 0.852f, 0.754f, 0.639f, 0.667f, 0.379f, 0.542f, 1f, 1f },
+                    new float[] { 0.06f, 0.1f, 0.151f, 0.194f, 0.25f, 0.312f, 0.486f, 0.651f, 0.825f, 0.885f }
+            ),
+            // Blue
+            new TonalPalette(
+                    new float[] { 0.631f, 0.603f, 0.592f, 0.586f, 0.572f, 0.544f, 0.519f, 0.519f, 0.519f, 0.519f },
+                    new float[] { 0.852f, 1f, 0.887f, 0.852f, 0.871f, 0.907f, 0.949f, 0.934f, 0.903f, 0.815f },
+                    new float[] { 0.34f, 0.38f, 0.482f, 0.497f, 0.536f, 0.571f, 0.608f, 0.696f, 0.794f, 0.892f }
+            ),
+            // Purple
+            new TonalPalette(
+                    new float[] { 0.839f, 0.831f, 0.825f, 0.819f, 0.803f, 0.803f, 0.772f, 0.772f, 0.772f, 0.772f },
+                    new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 0.769f, 0.701f, 0.612f, 0.403f },
+                    new float[] { 0.125f, 0.15f, 0.2f, 0.245f, 0.31f, 0.36f, 0.567f, 0.666f, 0.743f, 0.833f }
+            ),
+            // Red
+            new TonalPalette(
+                    new float[] { 0.964f, 0.975f, 0.975f, 0.975f, 0.972f, 0.992f, 1.003f, 1.011f, 1.011f, 1.011f },
+                    new float[] { 0.869f, 0.802f, 0.739f, 0.903f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.241f, 0.316f, 0.46f, 0.586f, 0.655f, 0.7f, 0.75f, 0.8f, 0.84f, 0.88f }
+            )
+    };
+}
+
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 578921f..bee0bd4 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -139,6 +139,7 @@
         final Rect folderIconPos = new Rect();
         float scaleRelativeToDragLayer = mLauncher.getDragLayer()
                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
+        float initialSize = (mFolderIcon.mBackground.getRadius() * 2) * scaleRelativeToDragLayer;
 
         // Match size/scale of icons in the preview
         float previewScale = rule.scaleForItem(0, itemsInPreview.size());
@@ -156,6 +157,9 @@
         // expected path to their final locations. ie. an icon should not move right, if it's final
         // location is to its left. This value is arbitrarily defined.
         int previewItemOffsetX = (int) (previewSize / 2);
+        if (Utilities.isRtl(mContext.getResources())) {
+            previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
+        }
 
         final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft())
                 * initialScale);
@@ -186,9 +190,6 @@
                 : finalTextColor);
 
         // Set up the reveal animation that clips the Folder.
-        float initialSize = (mFolderIcon.mBackground.getRadius() * 2
-                + mPreviewBackground.getStrokeWidth()) * scaleRelativeToDragLayer;
-
         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
         Rect startRect = new Rect(
                 Math.round(totalOffsetX / initialScale),
diff --git a/src/com/android/launcher3/model/DbDowngradeHelper.java b/src/com/android/launcher3/model/DbDowngradeHelper.java
new file mode 100644
index 0000000..cd86b72
--- /dev/null
+++ b/src/com/android/launcher3/model/DbDowngradeHelper.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.IOUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Utility class to handle DB downgrade
+ */
+public class DbDowngradeHelper {
+
+    private static final String TAG = "DbDowngradeHelper";
+
+    private static final String KEY_VERSION = "version";
+    private static final String KEY_DOWNGRADE_TO = "downgrade_to_";
+
+    private final SparseArray<String[]> mStatements = new SparseArray<>();
+    public final int version;
+
+    private DbDowngradeHelper(int version) {
+        this.version = version;
+    }
+
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        ArrayList<String> allCommands = new ArrayList<>();
+
+        for (int i = oldVersion - 1; i >= newVersion; i--) {
+            String[] commands = mStatements.get(i);
+            if (commands == null) {
+                throw new SQLiteException("Downgrade path not supported to version " + i);
+            }
+            Collections.addAll(allCommands, commands);
+        }
+
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            for (String sql : allCommands) {
+                db.execSQL(sql);
+            }
+            t.commit();
+        }
+    }
+
+    public static DbDowngradeHelper parse(File file) throws JSONException, IOException {
+        JSONObject obj = new JSONObject(new String(IOUtils.toByteArray(file)));
+        DbDowngradeHelper helper = new DbDowngradeHelper(obj.getInt(KEY_VERSION));
+        for (int version = helper.version - 1; version > 0; version--) {
+            if (obj.has(KEY_DOWNGRADE_TO + version)) {
+                JSONArray statements = obj.getJSONArray(KEY_DOWNGRADE_TO + version);
+                String[] parsed = new String[statements.length()];
+                for (int i = 0; i < parsed.length; i++) {
+                    parsed[i] = statements.getString(i);
+                }
+                helper.mStatements.put(version, parsed);
+            }
+        }
+        return helper;
+    }
+
+    public static void updateSchemaFile(File schemaFile, int expectedVersion,
+            Context context, int schemaResId) {
+        try {
+            if (DbDowngradeHelper.parse(schemaFile).version >= expectedVersion) {
+                return;
+            }
+        } catch (Exception e) {
+            // Schema error
+        }
+
+        // Write the updated schema
+        try (FileOutputStream fos = new FileOutputStream(schemaFile);
+            InputStream in = context.getResources().openRawResource(schemaResId)) {
+            IOUtils.copy(in, fos);
+        } catch (IOException e) {
+            Log.e(TAG, "Error writing schema file", e);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index e5bf35a..dd272b3 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
@@ -93,7 +94,7 @@
             if (mNotificationHeaderTextColor == Notification.COLOR_DEFAULT) {
                 mNotificationHeaderTextColor =
                         IconPalette.resolveContrastColor(getContext(), palette.dominantColor,
-                            getResources().getColor(R.color.notification_header_background_color));
+                            getResources().getColor(R.color.popup_header_background_color));
             }
             mHeaderCount.setTextColor(mNotificationHeaderTextColor);
         }
@@ -158,6 +159,13 @@
     }
 
     @Override
+    public int getArrowColor(boolean isArrowAttachedToBottom) {
+        return ContextCompat.getColor(getContext(), isArrowAttachedToBottom
+                ? R.color.popup_background_color
+                : R.color.popup_header_background_color);
+    }
+
+    @Override
     public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
             LauncherLogProto.Target targetParent) {
         target.itemType = LauncherLogProto.ItemType.NOTIFICATION;
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index fb7f80c..d4ee3b8 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -25,7 +25,6 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Color;
 import android.graphics.CornerPathEffect;
 import android.graphics.Paint;
 import android.graphics.PointF;
@@ -169,8 +168,6 @@
         final Resources resources = getResources();
         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
-        final int arrowHorizontalOffset = resources.getDimensionPixelSize(
-                R.dimen.popup_arrow_horizontal_offset);
         final int arrowVerticalOffset = resources.getDimensionPixelSize(
                 R.dimen.popup_arrow_vertical_offset);
 
@@ -208,6 +205,9 @@
         }
 
         // Add the arrow.
+        final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
+                R.dimen.popup_arrow_horizontal_offset_start :
+                R.dimen.popup_arrow_horizontal_offset_end);
         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
         mArrow.setPivotX(arrowWidth / 2);
         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
@@ -493,7 +493,11 @@
             ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
                     width, height, !mIsAboveIcon));
             Paint arrowPaint = arrowDrawable.getPaint();
-            arrowPaint.setColor(Color.WHITE);
+            // Note that we have to use getChildAt() instead of getItemViewAt(),
+            // since the latter expects the arrow which hasn't been added yet.
+            PopupItemView itemAttachedToArrow = (PopupItemView)
+                    (getChildAt(mIsAboveIcon ? getChildCount() - 1 : 0));
+            arrowPaint.setColor(itemAttachedToArrow.getArrowColor(mIsAboveIcon));
             // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
             int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
             arrowPaint.setPathEffect(new CornerPathEffect(radius));
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
index 5ead971..b073def 100644
--- a/src/com/android/launcher3/popup/PopupItemView.java
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -48,7 +48,7 @@
 
     protected final Rect mPillRect;
     private float mOpenAnimationProgress;
-
+    protected final boolean mIsRtl;
     protected View mIconView;
 
     private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
@@ -75,6 +75,8 @@
         canvas.setBitmap(mRoundedCornerBitmap);
         canvas.drawArc(0, 0, radius*2, radius*2, 180, 90, true, mBackgroundClipPaint);
         mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+
+        mIsRtl = Utilities.isRtl(getResources());
     }
 
     @Override
@@ -120,7 +122,9 @@
      */
     public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
         Point center = getIconCenter();
-        int arrowCenter = getResources().getDimensionPixelSize(R.dimen.popup_arrow_horizontal_center);
+        int arrowCenter = getResources().getDimensionPixelSize(pivotLeft ^ mIsRtl ?
+                R.dimen.popup_arrow_horizontal_center_start:
+                R.dimen.popup_arrow_horizontal_center_end);
         ValueAnimator openAnimator =  new ZoomRevealOutlineProvider(center.x, center.y,
                 mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft, arrowCenter)
                         .createRevealAnimator(this, false);
@@ -144,7 +148,9 @@
     public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
             long duration) {
         Point center = getIconCenter();
-        int arrowCenter = getResources().getDimensionPixelSize(R.dimen.popup_arrow_horizontal_center);
+        int arrowCenter = getResources().getDimensionPixelSize(pivotLeft ^ mIsRtl ?
+                R.dimen.popup_arrow_horizontal_center_start :
+                R.dimen.popup_arrow_horizontal_center_end);
         ValueAnimator closeAnimator = new ZoomRevealOutlineProvider(center.x, center.y,
                 mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft, arrowCenter)
                         .createRevealAnimator(this, true);
@@ -177,6 +183,8 @@
         return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
     }
 
+    public abstract int getArrowColor(boolean isArrowAttachedToBottom);
+
     /**
      * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
      */
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 6325040..74373d3 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -52,6 +52,7 @@
 
             if (screenIds.isEmpty()) {
                 // No update needed
+                t.commit();
                 return true;
             }
             if (screenIds.get(0) != 0) {
@@ -71,6 +72,7 @@
             if (DatabaseUtils.queryNumEntries(db, Favorites.TABLE_NAME,
                     "container = -100 and screen = 0 and cellY = 0") == 0) {
                 // First row is empty, no need to migrate.
+                t.commit();
                 return true;
             }
 
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
index ee64b98..5b3b02e 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorSet;
 import android.content.Context;
 import android.graphics.Point;
+import android.support.v4.content.ContextCompat;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
@@ -109,7 +110,7 @@
         mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
         mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
 
-        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getBubbleText(),
+        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(),
                 (PopupContainerWithArrow) getParent(), sv.getFinalInfo(),
                 new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
         dv.animateShift(-mIconShift.x, -mIconShift.y);
@@ -246,6 +247,14 @@
     }
 
     @Override
+    public int getArrowColor(boolean isArrowAttachedToBottom) {
+        return ContextCompat.getColor(getContext(),
+                isArrowAttachedToBottom || mSystemShortcutIcons == null
+                        ? R.color.popup_background_color
+                        : R.color.popup_header_background_color);
+    }
+
+    @Override
     public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
             LauncherLogProto.Target targetParent) {
         target.itemType = LauncherLogProto.ItemType.DEEPSHORTCUT;
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
new file mode 100644
index 0000000..77c21fe
--- /dev/null
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Supports various IO utility functions
+ */
+public class IOUtils {
+
+    private static final int BUF_SIZE = 0x1000; // 4K
+
+    public static byte[] toByteArray(File file) throws IOException {
+        try (InputStream in = new FileInputStream(file)) {
+            return toByteArray(in);
+        }
+    }
+
+    public static byte[] toByteArray(InputStream in) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        copy(in, out);
+        return out.toByteArray();
+    }
+
+    public static long copy(InputStream from, OutputStream to) throws IOException {
+        byte[] buf = new byte[BUF_SIZE];
+        long total = 0;
+        int r;
+        while ((r = from.read(buf)) != -1) {
+            to.write(buf, 0, r);
+            total += r;
+        }
+        return total;
+    }
+}
diff --git a/tests/res/raw/db_schema_v10.json b/tests/res/raw/db_schema_v10.json
new file mode 100644
index 0000000..a5e290e
--- /dev/null
+++ b/tests/res/raw/db_schema_v10.json
@@ -0,0 +1,4 @@
+{
+  "version" : 10,
+  "downgrade_to_9" : []
+}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
new file mode 100644
index 0000000..1d9148b
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Tests for {@link DbDowngradeHelper}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DbDowngradeHelperTest {
+
+    private static final String SCHEMA_FILE = "test_schema.json";
+    private static final String DB_FILE = "test.db";
+
+    private Context mContext;
+    private File mSchemaFile;
+    private File mDbFile;
+
+    @Before
+    public void setup() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mSchemaFile = mContext.getFileStreamPath(SCHEMA_FILE);
+        mDbFile = mContext.getDatabasePath(DB_FILE);
+    }
+
+    @Test
+    public void testUpdateSchemaFile() throws Exception {
+        Context myContext = InstrumentationRegistry.getContext();
+        int testResId = myContext.getResources().getIdentifier(
+                "db_schema_v10", "raw", myContext.getPackageName());
+        mSchemaFile.delete();
+        assertFalse(mSchemaFile.exists());
+
+        DbDowngradeHelper.updateSchemaFile(mSchemaFile, 10, myContext, testResId);
+        assertTrue(mSchemaFile.exists());
+        assertEquals(10, DbDowngradeHelper.parse(mSchemaFile).version);
+
+        // Schema is updated on version upgrade
+        assertTrue(mSchemaFile.setLastModified(0));
+        DbDowngradeHelper.updateSchemaFile(mSchemaFile, 11, myContext, testResId);
+        assertNotSame(0, mSchemaFile.lastModified());
+
+        // Schema is not updated when version is same
+        assertTrue(mSchemaFile.setLastModified(0));
+        DbDowngradeHelper.updateSchemaFile(mSchemaFile, 10, myContext, testResId);
+        assertEquals(0, mSchemaFile.lastModified());
+
+        // Schema is not updated on version downgrade
+        DbDowngradeHelper.updateSchemaFile(mSchemaFile, 3, myContext, testResId);
+        assertEquals(0, mSchemaFile.lastModified());
+    }
+
+    @Test
+    public void testDowngrade_success_v24() throws Exception {
+        setupTestDb();
+
+        TestOpenHelper helper = new TestOpenHelper(24);
+        assertEquals(24, helper.getReadableDatabase().getVersion());
+        helper.close();
+    }
+
+    @Test
+    public void testDowngrade_success_v22() throws Exception {
+        setupTestDb();
+
+        SQLiteOpenHelper helper = new TestOpenHelper(22);
+        assertEquals(22, helper.getWritableDatabase().getVersion());
+
+        // Check column does not exist
+        try (Cursor c = helper.getWritableDatabase().query(Favorites.TABLE_NAME,
+                null, null, null, null, null, null)) {
+            assertEquals(-1, c.getColumnIndex(Favorites.OPTIONS));
+
+            // Check data is present
+            assertEquals(10, c.getCount());
+        }
+        helper.close();
+
+        helper = new DatabaseHelper(mContext, null, DB_FILE) {
+            @Override
+            public void onOpen(SQLiteDatabase db) { }
+        };
+        assertEquals(LauncherProvider.SCHEMA_VERSION, helper.getWritableDatabase().getVersion());
+
+        try (Cursor c = helper.getWritableDatabase().query(Favorites.TABLE_NAME,
+                null, null, null, null, null, null)) {
+            // Check column exists
+            assertNotSame(-1, c.getColumnIndex(Favorites.OPTIONS));
+
+            // Check data is present
+            assertEquals(10, c.getCount());
+        }
+        helper.close();
+    }
+
+    @Test(expected = DowngradeFailException.class)
+    public void testDowngrade_fail_v20() throws Exception {
+        setupTestDb();
+
+        TestOpenHelper helper = new TestOpenHelper(20);
+        helper.getReadableDatabase().getVersion();
+    }
+
+    private void setupTestDb() throws Exception {
+        mSchemaFile.delete();
+        mDbFile.delete();
+
+        DbDowngradeHelper.updateSchemaFile(mSchemaFile, LauncherProvider.SCHEMA_VERSION, mContext,
+                R.raw.downgrade_schema);
+
+        DatabaseHelper dbHelper = new DatabaseHelper(mContext, null, DB_FILE) {
+            @Override
+            public void onOpen(SQLiteDatabase db) { }
+        };
+        // Insert dummy data
+        for (int i = 0; i < 10; i++) {
+            ContentValues values = new ContentValues();
+            values.put(Favorites._ID, i);
+            values.put(Favorites.TITLE, "title " + i);
+            dbHelper.getWritableDatabase().insert(Favorites.TABLE_NAME, null, values);
+        }
+        dbHelper.close();
+    }
+
+    private class TestOpenHelper extends SQLiteOpenHelper {
+
+        public TestOpenHelper(int version) {
+            super(mContext, DB_FILE, null, version);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase sqLiteDatabase) {
+            throw new RuntimeException("DB should already be created");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            throw new RuntimeException("Only downgrade supported");
+        }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            try {
+                DbDowngradeHelper.parse(mSchemaFile).onDowngrade(db, oldVersion, newVersion);
+            } catch (Exception e) {
+                throw new DowngradeFailException(e);
+            }
+        }
+    }
+
+    private static class DowngradeFailException extends RuntimeException {
+        public DowngradeFailException(Exception e) {
+            super(e);
+        }
+    }
+}