Add wallpaper support to theme bundles

Show wallpaper preview if available, show a checkbox to keep the
current wallpaper and apply the theme's wallpaper if needed.

Bug: 120559294

Change-Id: I229ab6e3372ace8218356d965e8d38f074e95061
diff --git a/res/layout/fragment_theme_picker.xml b/res/layout/fragment_theme_picker.xml
index 245a22c..e20c5f9 100644
--- a/res/layout/fragment_theme_picker.xml
+++ b/res/layout/fragment_theme_picker.xml
@@ -45,6 +45,13 @@
         <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
+            <CheckBox
+                android:id="@+id/use_my_wallpaper"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:text="@string/keep_my_wallpaper"/>
             <Button
                 android:id="@+id/apply_button"
                 style="@style/ActionPrimaryButton"
diff --git a/res/layout/preview_card_wallpaper_content.xml b/res/layout/preview_card_wallpaper_content.xml
new file mode 100644
index 0000000..0b43c2f
--- /dev/null
+++ b/res/layout/preview_card_wallpaper_content.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/preview_static_image"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    tools:showIn="@layout/theme_preview_card">
+    <TextView
+        style="@style/CardTitleTextAppearance"
+        android:id="@+id/wallpaper_description"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal|bottom"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/theme_preview_card.xml b/res/layout/theme_preview_card.xml
index fcb9e1e..18f2b15 100644
--- a/res/layout/theme_preview_card.xml
+++ b/res/layout/theme_preview_card.xml
@@ -16,27 +16,34 @@
 -->
 <androidx.cardview.widget.CardView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     style="@style/PreviewCard"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    app:contentPadding="0dp">
 
-    <LinearLayout
+    <FrameLayout
+        android:id="@+id/theme_preview_card_background"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-        <TextView
-            android:id="@+id/theme_preview_card_header"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:layout_marginBottom="8dp"
-            android:drawablePadding="10dp"
-            android:textAppearance="@style/CardTitleTextAppearance"/>
-        <FrameLayout
-            android:id="@+id/theme_preview_card_body_container"
+        android:layout_height="match_parent">
+        <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_marginHorizontal="8dp"/>
-    </LinearLayout>
-
+            android:padding="@dimen/preview_card_padding"
+            android:orientation="vertical">
+            <TextView
+                android:id="@+id/theme_preview_card_header"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginBottom="8dp"
+                android:drawablePadding="10dp"
+                android:textAppearance="@style/CardTitleTextAppearance"/>
+            <FrameLayout
+                android:id="@+id/theme_preview_card_body_container"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_marginHorizontal="8dp"/>
+        </LinearLayout>
+    </FrameLayout>
 </androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e491c39..e451795 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -38,6 +38,10 @@
         [CHAR LIMIT=20] -->
     <string name="apply_theme_btn">Apply</string>
 
+    <!-- Label for a checkbox to allow the user to use their currently set wallpaper instead of
+        the one bundled with selected Theme [CHAR LIMIT=35]-->
+    <string name="keep_my_wallpaper">Keep current wallpaper</string>
+
     <!-- Label for a button that allows the user to apply the currently selected customization option.
         [CHAR LIMIT=20] -->
     <string name="apply_btn">Apply</string>
@@ -81,4 +85,8 @@
 
     <!-- Message shown when a theme has been applied successfully in the system [CHAR LIMIT=NONE] -->
     <string name="applied_theme_msg">Style applied</string>
+
+    <!-- Message shown when a theme couldn't be applied in the system because of an error
+        [CHAR LIMIT=NONE] -->
+    <string name="apply_theme_error_msg">There was a problem applying the style</string>
 </resources>
diff --git a/src/com/android/customization/model/CustomizationManager.java b/src/com/android/customization/model/CustomizationManager.java
index 2a783a8..3f75be5 100644
--- a/src/com/android/customization/model/CustomizationManager.java
+++ b/src/com/android/customization/model/CustomizationManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.customization.model;
 
+import androidx.annotation.Nullable;
+
 import java.util.List;
 
 /**
@@ -24,6 +26,22 @@
 public interface CustomizationManager<T extends CustomizationOption> {
 
     /**
+     * Callback for applying a customization option.
+     */
+    interface Callback {
+        /**
+         * Called after an option was applied successfully.
+         */
+        void onSuccess();
+
+        /**
+         * Called if there was an error applying the customization
+         * @param throwable Exception thrown if available.
+         */
+        void onError(@Nullable Throwable throwable);
+    }
+
+    /**
      * Listener interface for fetching CustomizationOptions
      */
     interface OptionsFetchedListener<T extends CustomizationOption> {
@@ -41,7 +59,7 @@
     /**
      * Applies the given option into the system.
      */
-    void apply(T option);
+    void apply(T option, Callback callback);
 
     /**
      * Loads the available options for the type of Customization managed by this class, calling the
diff --git a/src/com/android/customization/model/clock/ClockManager.java b/src/com/android/customization/model/clock/ClockManager.java
index 4cfe64a..13c3530 100644
--- a/src/com/android/customization/model/clock/ClockManager.java
+++ b/src/com/android/customization/model/clock/ClockManager.java
@@ -16,7 +16,6 @@
 package com.android.customization.model.clock;
 
 import android.content.Context;
-import android.provider.Settings;
 import android.provider.Settings.Secure;
 
 import com.android.customization.model.CustomizationManager;
@@ -39,9 +38,14 @@
     }
 
     @Override
-    public void apply(Clockface option) {
-        Settings.Secure.putString(mContext.getContentResolver(),
+    public void apply(Clockface option, Callback callback) {
+        boolean stored = Secure.putString(mContext.getContentResolver(),
                 CLOCK_FACE_SETTING, option.getId());
+        if (stored) {
+            callback.onSuccess();
+        } else {
+            callback.onError(null);
+        }
     }
 
     @Override
diff --git a/src/com/android/customization/model/grid/GridOptionsManager.java b/src/com/android/customization/model/grid/GridOptionsManager.java
index 365a822..b411e10 100644
--- a/src/com/android/customization/model/grid/GridOptionsManager.java
+++ b/src/com/android/customization/model/grid/GridOptionsManager.java
@@ -41,8 +41,13 @@
     }
 
     @Override
-    public void apply(GridOption option) {
-        mProvider.applyGrid(option.name);
+    public void apply(GridOption option, Callback callback) {
+        int updated = mProvider.applyGrid(option.name);
+        if (updated == 1) {
+            callback.onSuccess();
+        } else {
+            callback.onError(null);
+        }
     }
 
     @Override
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
index 89c45bc..a824b28 100644
--- a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -124,7 +124,7 @@
         return mOptions;
     }
 
-    void applyGrid(String name) {
+    int applyGrid(String name) {
         Uri updateDefaultUri = new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(mProviderInfo.authority)
@@ -132,6 +132,6 @@
                 .build();
         ContentValues values = new ContentValues();
         values.put("name", name);
-        mContext.getContentResolver().update(updateDefaultUri, values, null, null);
+        return mContext.getContentResolver().update(updateDefaultUri, values, null, null);
     }
 }
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
index 911aa7c..a2cb95c 100644
--- a/src/com/android/customization/model/theme/DefaultThemeProvider.java
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -66,6 +66,10 @@
     private static final String ICON_PREVIEW_DRAWABLE_NAME = "ic_wifi_signal_3";
     private static final String PREVIEW_COLOR_PREFIX = "theme_preview_color_";
     private static final String PREVIEW_SHAPE_PREFIX = "theme_preview_shape_";
+    private static final String WALLPAPER_PREFIX = "theme_wallpaper_";
+    private static final String WALLPAPER_TITLE_PREFIX = "theme_wallpaper_title_";
+    private static final String WALLPAPER_ATTRIBUTION_PREFIX = "theme_wallpaper_attribution_";
+    private static final String WALLPAPER_ACTION_PREFIX = "theme_wallpaper_action_";
 
     private static final String DEFAULT_THEME_NAME= "default";
 
@@ -194,6 +198,27 @@
                             iconSettingsOverlayPackage);
                 }
 
+                try {
+                    String wallpaperResName = WALLPAPER_PREFIX + themeName;
+                    int wallpaperResId = mStubApkResources.getIdentifier(wallpaperResName,
+                            "drawable", mStubPackageName);
+                    if (wallpaperResId > 0) {
+                        builder.setWallpaperInfo(mStubPackageName, wallpaperResName,
+                                themeName, wallpaperResId,
+                                mStubApkResources.getIdentifier(WALLPAPER_TITLE_PREFIX + themeName,
+                                        "string", mStubPackageName),
+                                mStubApkResources.getIdentifier(
+                                        WALLPAPER_ATTRIBUTION_PREFIX + themeName, "string",
+                                        mStubPackageName),
+                                mStubApkResources.getIdentifier(WALLPAPER_ACTION_PREFIX + themeName,
+                                        "string", mStubPackageName))
+                                .setWallpaperAsset(
+                                        getDrawableResourceAsset(WALLPAPER_PREFIX, themeName));
+                    }
+                } catch (NotFoundException e) {
+                    // Nothing to do here, if there's no wallpaper we'll just omit wallpaper
+                }
+
                 mThemes.add(builder.build());
             } catch (NameNotFoundException | NotFoundException e) {
                 Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName),
@@ -310,6 +335,30 @@
             }
         }
 
+        try {
+            String wallpaperResName = WALLPAPER_PREFIX + DEFAULT_THEME_NAME;
+            int wallpaperResId = mStubApkResources.getIdentifier(wallpaperResName,
+                    "drawable", mStubPackageName);
+            if (wallpaperResId > 0) {
+                builder.setWallpaperInfo(mStubPackageName, wallpaperResName, DEFAULT_THEME_NAME,
+                        mStubApkResources.getIdentifier(
+                                wallpaperResName,
+                                "drawable", mStubPackageName),
+                        mStubApkResources.getIdentifier(WALLPAPER_TITLE_PREFIX + DEFAULT_THEME_NAME,
+                                "string", mStubPackageName),
+                        mStubApkResources.getIdentifier(
+                                WALLPAPER_ATTRIBUTION_PREFIX + DEFAULT_THEME_NAME, "string",
+                                mStubPackageName),
+                        mStubApkResources.getIdentifier(
+                                WALLPAPER_ACTION_PREFIX + DEFAULT_THEME_NAME,
+                                "string", mStubPackageName))
+                        .setWallpaperAsset(
+                                getDrawableResourceAsset(WALLPAPER_PREFIX, DEFAULT_THEME_NAME));
+            }
+        } catch (NotFoundException e) {
+            // Nothing to do here, if there's no wallpaper we'll just omit wallpaper
+        }
+
         mThemes.add(builder.build());
     }
 
diff --git a/src/com/android/customization/model/theme/ThemeBundle.java b/src/com/android/customization/model/theme/ThemeBundle.java
index fd57da9..aa28ad8 100644
--- a/src/com/android/customization/model/theme/ThemeBundle.java
+++ b/src/com/android/customization/model/theme/ThemeBundle.java
@@ -15,6 +15,7 @@
  */
 package com.android.customization.model.theme;
 
+import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Typeface;
@@ -29,12 +30,15 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 import androidx.core.graphics.PathParser;
 
 import com.android.customization.model.CustomizationManager;
 import com.android.customization.model.CustomizationOption;
 import com.android.wallpaper.R;
+import com.android.wallpaper.asset.Asset;
 import com.android.wallpaper.asset.ResourceAsset;
+import com.android.wallpaper.model.WallpaperInfo;
 
 import org.json.JSONObject;
 
@@ -56,13 +60,17 @@
     private final PreviewInfo mPreviewInfo;
     private final boolean mIsDefault;
     private final Map<String, String> mPackagesByCategory;
+    @Nullable private final WallpaperInfo mWallpaperInfo;
+    private WallpaperInfo mOverrideWallpaper;
 
     private ThemeBundle(String title, Map<String, String> overlayPackages,
-            boolean isDefault, PreviewInfo previewInfo) {
+            boolean isDefault, @Nullable WallpaperInfo wallpaperInfo,
+            PreviewInfo previewInfo) {
         mTitle = title;
         mIsDefault = isDefault;
         mPreviewInfo = previewInfo;
-        mPackagesByCategory = Collections.unmodifiableMap(overlayPackages);
+        mWallpaperInfo = wallpaperInfo;
+        mPackagesByCategory = Collections.unmodifiableMap(overlayPackages);        
     }
 
     @Override
@@ -114,6 +122,24 @@
         return mPreviewInfo;
     }
 
+    public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) {
+        mOverrideWallpaper = homeWallpaper;
+    }
+
+    public boolean useThemeWallpaper() {
+        return mOverrideWallpaper == null && mWallpaperInfo != null;
+    }
+
+    public Asset getWallpaperPreviewAsset(Context context) {
+        return mOverrideWallpaper != null ?
+                mOverrideWallpaper.getThumbAsset(context) :
+                getPreviewInfo().wallpaperAsset;
+    }
+
+    public WallpaperInfo getWallpaperInfo() {
+        return mWallpaperInfo;
+    }
+
     boolean isDefault() {
         return mIsDefault;
     }
@@ -122,7 +148,7 @@
         return mPackagesByCategory.values();
     }
 
-    String getSerializedPackages() {
+    public String getSerializedPackages() {
         if (isDefault()) {
             return "";
         }
@@ -130,6 +156,7 @@
         return new JSONObject(mPackagesByCategory).toString();
     }
 
+
     public static class PreviewInfo {
         public final Typeface bodyFontFamily;
         public final Typeface headlineFontFamily;
@@ -137,23 +164,24 @@
         @ColorInt public final int colorAccentDark;
         public final List<Drawable> icons;
         public final Drawable shapeDrawable;
-        @DrawableRes public final int wallpaperDrawableRes;
-        public final ResourceAsset colorPreviewDrawable;
-        public final ResourceAsset shapePreviewDrawable;
+        @Nullable public final ResourceAsset wallpaperAsset;
+        @Nullable public final ResourceAsset colorPreviewAsset;
+        @Nullable public final ResourceAsset shapePreviewAsset;
 
         private PreviewInfo(Typeface bodyFontFamily, Typeface headlineFontFamily,
                 int colorAccentLight, int colorAccentDark, List<Drawable> icons,
-                Drawable shapeDrawable, int wallpaperDrawableRes,
-                ResourceAsset colorPreviewDrawable, ResourceAsset shapePreviewDrawable) {
+                Drawable shapeDrawable, @Nullable ResourceAsset wallpaperAsset,
+                @Nullable ResourceAsset colorPreviewAsset,
+                @Nullable ResourceAsset shapePreviewAsset) {
             this.bodyFontFamily = bodyFontFamily;
             this.headlineFontFamily = headlineFontFamily;
             this.colorAccentLight = colorAccentLight;
             this.colorAccentDark = colorAccentDark;
             this.icons = icons;
             this.shapeDrawable = shapeDrawable;
-            this.wallpaperDrawableRes = wallpaperDrawableRes;
-            this.colorPreviewDrawable = colorPreviewDrawable;
-            this.shapePreviewDrawable = shapePreviewDrawable;
+            this.wallpaperAsset = wallpaperAsset;
+            this.colorPreviewAsset = colorPreviewAsset;
+            this.shapePreviewAsset = shapePreviewAsset;
         }
     }
 
@@ -167,9 +195,10 @@
         private List<Drawable> mIcons = new ArrayList<>();
         private String mShapePath;
         private boolean mIsDefault;
-        @DrawableRes private int mWallpaperDrawableResId;
+        private ResourceAsset mWallpaperAsset;
         private ResourceAsset mColorPreview;
         private ResourceAsset mShapePreview;
+        private WallpaperInfo mWallpaperInfo;
         private Map<String, String> mPackages = new HashMap<>();
 
         public ThemeBundle build() {
@@ -181,9 +210,9 @@
                 shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
                 shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
             }
-            return new ThemeBundle(mTitle, mPackages, mIsDefault,
+            return new ThemeBundle(mTitle, mPackages, mIsDefault, mWallpaperInfo,
                     new PreviewInfo(mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight,
-                            mColorAccentDark, mIcons, shapeDrawable, mWallpaperDrawableResId,
+                            mColorAccentDark, mIcons, shapeDrawable, mWallpaperAsset,
                             mColorPreview, mShapePreview));
         }
 
@@ -237,6 +266,19 @@
             return this;
         }
 
+        public Builder setWallpaperInfo(String wallpaperPackageName, String wallpaperResName,
+                String themeId, @DrawableRes int wallpaperResId, @StringRes int titleResId,
+                @StringRes int attributionResId, @StringRes int actionUrlResId) {
+            mWallpaperInfo = new ThemeBundledWallpaperInfo(wallpaperPackageName, wallpaperResName,
+                    themeId, wallpaperResId, titleResId, attributionResId, actionUrlResId);
+            return this;
+        }
+
+        public Builder setWallpaperAsset(ResourceAsset wallpaperAsset) {
+            mWallpaperAsset = wallpaperAsset;
+            return this;
+        }
+
         public Builder asDefault() {
             mIsDefault = true;
             return this;
diff --git a/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java b/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java
new file mode 100644
index 0000000..4f6a29f
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import static com.google.android.apps.wallpaper.model.Action.getActionIconForType;
+import static com.google.android.apps.wallpaper.model.Action.getActionLabelForType;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.StringRes;
+
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.asset.ResourceAsset;
+import com.android.wallpaper.model.InlinePreviewIntentFactory;
+import com.android.wallpaper.model.WallpaperInfo;
+
+import com.google.android.apps.wallpaper.model.Action.ActionType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a wallpaper coming from the resources of the theme bundle container APK.
+ */
+public class ThemeBundledWallpaperInfo extends WallpaperInfo {
+    public static final Creator<ThemeBundledWallpaperInfo> CREATOR =
+            new Creator<ThemeBundledWallpaperInfo>() {
+                @Override
+                public ThemeBundledWallpaperInfo createFromParcel(Parcel in) {
+                    return new ThemeBundledWallpaperInfo(in);
+                }
+
+                @Override
+                public ThemeBundledWallpaperInfo[] newArray(int size) {
+                    return new ThemeBundledWallpaperInfo[size];
+                }
+            };
+
+    private static final String TAG = "ThemeBundledWallpaperInfo";
+
+    private final String mPackageName;
+    private final String mResName;
+    private final String mCollectionId;
+    @DrawableRes private final int mDrawableResId;
+    @StringRes private final int mTitleResId;
+    @StringRes private final int mAttributionResId;
+    @StringRes private final int mActionUrlResId;
+    private List<String> mAttributions;
+    private String mActionUrl;
+    private Resources mResources;
+    private Asset mAsset;
+
+    /**
+     * Constructs a new theme-bundled static wallpaper model object.
+     *
+     * @param drawableResId  Resource ID of the raw wallpaper image.
+     * @param resName        The unique name of the wallpaper resource, e.g. "z_wp001".
+     * @param themeName   Unique name of the collection this wallpaper belongs in; used for logging.
+     * @param titleResId     Resource ID of the string for the title attribution.
+     * @param attributionResId Resource ID of the string for the first subtitle attribution.
+     */
+    public ThemeBundledWallpaperInfo(String packageName, String resName, String themeName,
+            int drawableResId, int titleResId, int attributionResId, int actionUrlResId) {
+        mPackageName = packageName;
+        mResName = resName;
+        mCollectionId = themeName;
+        mDrawableResId = drawableResId;
+        mTitleResId = titleResId;
+        mAttributionResId = attributionResId;
+        mActionUrlResId = actionUrlResId;
+    }
+
+    private ThemeBundledWallpaperInfo(Parcel in) {
+        mPackageName = in.readString();
+        mResName = in.readString();
+        mCollectionId = in.readString();
+        mDrawableResId = in.readInt();
+        mTitleResId = in.readInt();
+        mAttributionResId = in.readInt();
+        mActionUrlResId = in.readInt();
+    }
+
+    @Override
+    public List<String> getAttributions(Context context) {
+        if (mAttributions == null) {
+            Resources res = getPackageResources(context);
+            mAttributions = new ArrayList<>();
+            if (mTitleResId != 0) {
+                mAttributions.add(res.getString(mTitleResId));
+            }
+            if (mAttributionResId != 0) {
+                mAttributions.add(res.getString(mAttributionResId));
+            }
+        }
+
+        return mAttributions;
+    }
+
+    @Override
+    public String getActionUrl(Context context) {
+        if (mActionUrl == null && mActionUrlResId != 0) {
+            mActionUrl = getPackageResources(context).getString(mActionUrlResId);
+        }
+        return mActionUrl;
+    }
+
+    @Override
+    public int getActionLabelRes(Context context) {
+        return getActionLabelForType(getActionType(context));
+    }
+
+    @Override
+    public Asset getAsset(Context context) {
+        if (mAsset == null) {
+            Resources res = getPackageResources(context);
+            mAsset = new ResourceAsset(res, mDrawableResId);
+        }
+
+        return mAsset;
+    }
+
+    @Override
+    public Asset getThumbAsset(Context context) {
+        return getAsset(context);
+    }
+
+    @Override
+    public int getActionIconRes(Context context) {
+        return getActionIconForType(getActionType(context));
+    }
+
+    @Override
+    public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory,
+            int requestCode) {
+        try {
+            srcActivity.startActivityForResult(factory.newIntent(srcActivity, this), requestCode);
+        } catch (ActivityNotFoundException |SecurityException e) {
+            Log.e(TAG, "App isn't installed or ThemePicker doesn't have permission to launch", e);
+        }
+
+    }
+
+    @Override
+    public String getCollectionId(Context unused) {
+        return mCollectionId;
+    }
+
+    @Override
+    public String getWallpaperId() {
+        return mResName;
+    }
+
+    public String getResName() {
+        return mResName;
+    }
+
+    private int getActionType(Context context) {
+        return ActionType.EXPLORE;
+    }
+
+    /**
+     * Returns the {@link Resources} instance for the theme bundles stub APK.
+     */
+    private Resources getPackageResources(Context context) {
+        if (mResources != null) {
+            return mResources;
+        }
+
+        try {
+            mResources = context.getPackageManager().getResourcesForApplication(mPackageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Could not get app resources for " + mPackageName);
+        }
+        return mResources;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mPackageName);
+        dest.writeString(mResName);
+        dest.writeString(mCollectionId);
+        dest.writeInt(mDrawableResId);
+        dest.writeInt(mTitleResId);
+        dest.writeInt(mAttributionResId);
+        dest.writeInt(mActionUrlResId);
+    }
+}
diff --git a/src/com/android/customization/model/theme/ThemeManager.java b/src/com/android/customization/model/theme/ThemeManager.java
index 2b0bcf8..e56503b 100644
--- a/src/com/android/customization/model/theme/ThemeManager.java
+++ b/src/com/android/customization/model/theme/ThemeManager.java
@@ -19,15 +19,21 @@
 import static com.android.customization.model.ResourceConstants.SETTINGS_PACKAGE;
 import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
 
-import android.content.Context;
+import android.app.Activity;
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayManager;
+import android.graphics.Point;
 import android.os.UserHandle;
 import android.provider.Settings;
 
 import androidx.annotation.Nullable;
 
 import com.android.customization.model.CustomizationManager;
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.module.WallpaperPersister;
+import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
+import com.android.wallpaper.module.WallpaperSetter;
+import com.android.wallpaper.util.WallpaperCropUtils;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -64,14 +70,17 @@
 
     private final ThemeBundleProvider mProvider;
     private final OverlayManager mOverlayManager;
-    private final Context mContext;
-    private boolean useThemeWallpaper;
+    private final WallpaperSetter mWallpaperSetter;
+    private final Activity mActivity;
+
     private Map<String, String> mCurrentOverlays;
 
-    public ThemeManager(ThemeBundleProvider provider, Context context) {
+    public ThemeManager(ThemeBundleProvider provider, Activity activity,
+            WallpaperSetter wallpaperSetter) {
         mProvider = provider;
-        mContext = context;
-        mOverlayManager = context.getSystemService(OverlayManager.class);
+        mActivity = activity;
+        mOverlayManager = activity.getSystemService(OverlayManager.class);
+        mWallpaperSetter = wallpaperSetter;
     }
 
     @Override
@@ -80,26 +89,70 @@
     }
 
     @Override
-    public void apply(ThemeBundle theme) {
+    public void apply(ThemeBundle theme, Callback callback) {
+        // Set wallpaper
+        if (theme.useThemeWallpaper()) {
+            applyWallpaper(theme, new SetWallpaperCallback() {
+                @Override
+                public void onSuccess() {
+                    applyOverlays(theme, callback);
+                }
+
+                @Override
+                public void onError(@Nullable Throwable throwable) {
+                    callback.onError(throwable);
+                }
+            });
+        } else {
+            applyOverlays(theme, callback);
+        }
+    }
+
+    private void applyWallpaper(ThemeBundle theme, SetWallpaperCallback callback) {
+        Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
+                mActivity.getResources(),
+                mActivity.getWindowManager().getDefaultDisplay());
+        Asset wallpaperAsset = theme.getWallpaperInfo().getAsset(mActivity);
+        wallpaperAsset.decodeRawDimensions(mActivity,
+                dimensions -> {
+                    float scale = 1f;
+                    // Calculate scale to fit the screen height
+                    if (dimensions != null && dimensions.y > 0) {
+                        scale = (float) defaultCropSurfaceSize.y / dimensions.y;
+                    }
+                    mWallpaperSetter.setCurrentWallpaper(mActivity,
+                            theme.getWallpaperInfo(),
+                            wallpaperAsset,
+                            WallpaperPersister.DEST_BOTH,
+                            scale, null, callback);
+                });
+    }
+
+    private void applyOverlays(ThemeBundle theme, Callback callback) {
+        boolean allApplied = true;
         if (theme.isDefault()) {
-            disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_COLOR);
-            disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_FONT);
-            disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_SHAPE);
-            disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_ICON_ANDROID);
-            disableCurrentOverlay(SYSUI_PACKAGE, OVERLAY_CATEGORY_ICON_SYSUI);
-            disableCurrentOverlay(SETTINGS_PACKAGE, OVERLAY_CATEGORY_ICON_SETTINGS);
+            allApplied &= disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_COLOR);
+            allApplied &= disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_FONT);
+            allApplied &= disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_SHAPE);
+            allApplied &= disableCurrentOverlay(ANDROID_PACKAGE, OVERLAY_CATEGORY_ICON_ANDROID);
+            allApplied &= disableCurrentOverlay(SYSUI_PACKAGE, OVERLAY_CATEGORY_ICON_SYSUI);
+            allApplied &= disableCurrentOverlay(SETTINGS_PACKAGE, OVERLAY_CATEGORY_ICON_SETTINGS);
         } else {
             for (String packageName : theme.getAllPackages()) {
                 if (packageName != null) {
-                    mOverlayManager.setEnabledExclusiveInCategory(packageName,
+                    allApplied &= mOverlayManager.setEnabledExclusiveInCategory(packageName,
                             UserHandle.myUserId());
                 }
             }
         }
-        Settings.Secure.putString(mContext.getContentResolver(), THEME_SETTING,
-                theme.getSerializedPackages());
+        allApplied &= Settings.Secure.putString(mActivity.getContentResolver(),
+                THEME_SETTING, theme.getSerializedPackages());
         mCurrentOverlays = null;
-        // TODO: set wallpaper
+        if (allApplied) {
+            callback.onSuccess();
+        } else {
+            callback.onError(null);
+        }
     }
 
     @Override
@@ -107,11 +160,12 @@
         mProvider.fetch(callback, false);
     }
 
-    private void disableCurrentOverlay(String packageName, String category) {
+    private boolean disableCurrentOverlay(String packageName, String category) {
         OverlayInfo current = getEnabledOverlayInfo(packageName, category);
         if (current != null) {
-            mOverlayManager.setEnabled(current.packageName, false, UserHandle.myUserId());
+           return mOverlayManager.setEnabled(current.packageName, false, UserHandle.myUserId());
         }
+        return true;
     }
 
     @Nullable
@@ -146,6 +200,6 @@
     }
 
     public String getStoredOverlays() {
-        return Settings.Secure.getString(mContext.getContentResolver(), THEME_SETTING);
+        return Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING);
     }
 }
diff --git a/src/com/android/customization/picker/CustomizationPickerActivity.java b/src/com/android/customization/picker/CustomizationPickerActivity.java
index d86f5dc..2257946 100644
--- a/src/com/android/customization/picker/CustomizationPickerActivity.java
+++ b/src/com/android/customization/picker/CustomizationPickerActivity.java
@@ -54,6 +54,7 @@
 import com.android.wallpaper.module.Injector;
 import com.android.wallpaper.module.InjectorProvider;
 import com.android.wallpaper.module.UserEventLogger;
+import com.android.wallpaper.module.WallpaperSetter;
 import com.android.wallpaper.picker.CategoryFragment;
 import com.android.wallpaper.picker.CategoryFragment.CategoryFragmentHost;
 import com.android.wallpaper.picker.MyPhotosStarter;
@@ -84,6 +85,7 @@
 
     private static final Map<Integer, CustomizationSection> mSections = new HashMap<>();
     private CategoryFragment mWallpaperCategoryFragment;
+    private WallpaperSetter mWallpaperSetter;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -145,7 +147,11 @@
             return;
         }
         //Theme
-        ThemeManager themeManager = new ThemeManager(new DefaultThemeProvider(this), this);
+        Injector injector = InjectorProvider.getInjector();
+        mWallpaperSetter = new WallpaperSetter(injector.getWallpaperPersister(this),
+                injector.getPreferences(this), mUserEventLogger, false);
+        ThemeManager themeManager = new ThemeManager(new DefaultThemeProvider(this), this,
+                mWallpaperSetter);
         if (themeManager.isAvailable()) {
             mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme, themeManager));
         }
@@ -260,6 +266,14 @@
         return section == null ? null : (ThemeManager) section.customizationManager;
     }
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mWallpaperSetter != null) {
+            mWallpaperSetter.cleanUp();
+        }
+    }
+
     /**
      * Represents a section of the Picker (eg "ThemeBundle", "Clock", etc).
      * There should be a concrete subclass per available section, providing the corresponding
diff --git a/src/com/android/customization/picker/clock/ClockFragment.java b/src/com/android/customization/picker/clock/ClockFragment.java
index ab38987..5e51dab 100644
--- a/src/com/android/customization/picker/clock/ClockFragment.java
+++ b/src/com/android/customization/picker/clock/ClockFragment.java
@@ -27,6 +27,7 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.customization.model.CustomizationManager.Callback;
 import com.android.customization.model.clock.ClockManager;
 import com.android.customization.model.clock.Clockface;
 import com.android.customization.picker.BasePreviewAdapter;
@@ -78,8 +79,18 @@
         mOptionsContainer = view.findViewById(R.id.options_container);
         setUpOptions();
         view.findViewById(R.id.apply_button).setOnClickListener(v -> {
-            mClockManager.apply(mSelectedOption);
-            getActivity().finish();
+            mClockManager.apply(mSelectedOption, new Callback() {
+                @Override
+                public void onSuccess() {
+                    getActivity().finish();
+                }
+
+                @Override
+                public void onError(@Nullable Throwable throwable) {
+                    //TODO(santie): handle
+                }
+            });
+
         });
         return view;
     }
diff --git a/src/com/android/customization/picker/grid/GridFragment.java b/src/com/android/customization/picker/grid/GridFragment.java
index 845d008..8d7a5c1 100644
--- a/src/com/android/customization/picker/grid/GridFragment.java
+++ b/src/com/android/customization/picker/grid/GridFragment.java
@@ -33,6 +33,7 @@
 import androidx.cardview.widget.CardView;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.customization.model.CustomizationManager.Callback;
 import com.android.customization.model.grid.GridOption;
 import com.android.customization.model.grid.GridOptionsManager;
 import com.android.customization.picker.BasePreviewAdapter;
@@ -101,8 +102,18 @@
         mScreenAspectRatio = (float) dm.heightPixels / dm.widthPixels;
         setUpOptions();
         view.findViewById(R.id.apply_button).setOnClickListener(v -> {
-            mGridManager.apply(mSelectedOption);
-            getActivity().finish();
+            mGridManager.apply(mSelectedOption,  new Callback() {
+                @Override
+                public void onSuccess() {
+                    getActivity().finish();
+                }
+
+                @Override
+                public void onError(@Nullable Throwable throwable) {
+                    //TODO(santie): handle
+                }
+            });
+
         });
         CurrentWallpaperInfoFactory factory = InjectorProvider.getInjector()
                 .getCurrentWallpaperFactory(getContext().getApplicationContext());
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
index b95746b..a7d8af2 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -19,10 +19,15 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
+import android.graphics.drawable.BitmapDrawable;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -34,8 +39,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager.widget.PagerAdapter;
 
+import com.android.customization.model.CustomizationManager.Callback;
 import com.android.customization.model.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeManager;
 import com.android.customization.picker.BasePreviewAdapter;
@@ -43,6 +48,9 @@
 import com.android.customization.widget.OptionSelectorController;
 import com.android.customization.widget.PreviewPager;
 import com.android.wallpaper.R;
+import com.android.wallpaper.model.WallpaperInfo;
+import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
+import com.android.wallpaper.module.InjectorProvider;
 import com.android.wallpaper.picker.ToolbarFragment;
 
 /**
@@ -50,13 +58,15 @@
  */
 public class ThemeFragment extends ToolbarFragment {
 
+    private static final String TAG = "ThemeFragment";
+    private static final String KEY_SELECTED_THEME = "ThemeFragment.SelectedThemeBundle";
+
     /**
      * Interface to be implemented by an Activity hosting a {@link ThemeFragment}
      */
     public interface ThemeFragmentHost {
         ThemeManager getThemeManager();
     }
-
     public static ThemeFragment newInstance(CharSequence title) {
         ThemeFragment fragment = new ThemeFragment();
         fragment.setArguments(ToolbarFragment.createArguments(title));
@@ -67,8 +77,11 @@
     private OptionSelectorController mOptionsController;
     private ThemeManager mThemeManager;
     private ThemeBundle mSelectedTheme;
-    private PagerAdapter mAdapter;
+    private ThemePreviewAdapter mAdapter;
     private PreviewPager mPreviewPager;
+    private boolean mUseMyWallpaper;
+    private WallpaperInfo mCurrentHomeWallpaper;
+    private CurrentWallpaperInfoFactory mCurrentWallpaperFactory;
 
     @Override
     public void onAttach(Context context) {
@@ -83,41 +96,105 @@
         View view = inflater.inflate(
                 R.layout.fragment_theme_picker, container, /* attachToRoot */ false);
         setUpToolbar(view);
+
+        mCurrentWallpaperFactory = InjectorProvider.getInjector()
+                .getCurrentWallpaperFactory(getActivity().getApplicationContext());
         mPreviewPager = view.findViewById(R.id.theme_preview_pager);
         mOptionsContainer = view.findViewById(R.id.options_container);
         view.findViewById(R.id.apply_button).setOnClickListener(v -> {
-            mThemeManager.apply(mSelectedTheme);
-            Toast.makeText(getContext(), R.string.applied_theme_msg, Toast.LENGTH_LONG).show();
-            getActivity().finish();
+            mThemeManager.apply(mSelectedTheme, new Callback() {
+                @Override
+                public void onSuccess() {
+                    Toast.makeText(getContext(), R.string.applied_theme_msg,
+                            Toast.LENGTH_LONG).show();
+                    getActivity().finish();
+                }
+
+                @Override
+                public void onError(@Nullable Throwable throwable) {
+                    Log.w(TAG, "Error applying theme", throwable);
+                    Toast.makeText(getContext(), R.string.apply_theme_error_msg,
+                            Toast.LENGTH_LONG).show();
+                }
+            });
+
         });
-        setUpOptions();
+        ((CheckBox)view.findViewById(R.id.use_my_wallpaper)).setOnCheckedChangeListener(
+                this::onUseMyWallpaperCheckChanged);
+
+        setUpOptions(savedInstanceState);
 
         return view;
     }
 
+    @Override
+    public void onResume() {
+        super.onResume();
+        reloadWallpaper();
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mSelectedTheme != null && !mSelectedTheme.isActive(mThemeManager)) {
+            outState.putString(KEY_SELECTED_THEME, mSelectedTheme.getSerializedPackages());
+        }
+    }
+
+    private void onUseMyWallpaperCheckChanged(CompoundButton checkbox, boolean checked) {
+        mUseMyWallpaper = checked;
+        reloadWallpaper();
+    }
+
+    private void reloadWallpaper() {
+        if (mUseMyWallpaper) {
+            mCurrentWallpaperFactory.createCurrentWallpaperInfos(
+                    (homeWallpaper, lockWallpaper, presentationMode) -> {
+                        if (mSelectedTheme != null) {
+                            mCurrentHomeWallpaper = homeWallpaper;
+                            mSelectedTheme.setOverrideThemeWallpaper(homeWallpaper);
+                            if (mAdapter != null) {
+                                mAdapter.rebindWallpaperIfAvailable();
+                            }
+                        }
+            }, false);
+        } else {
+            mCurrentHomeWallpaper = null;
+            if (mSelectedTheme != null) {
+                mSelectedTheme.setOverrideThemeWallpaper(null);
+                if (mAdapter != null) {
+                    mAdapter.rebindWallpaperIfAvailable();
+                }
+            }
+        }
+    }
+
     private void createAdapter() {
         mAdapter = new ThemePreviewAdapter(getActivity(), mSelectedTheme);
         mPreviewPager.setAdapter(mAdapter);
     }
 
-    private void setUpOptions() {
+    private void setUpOptions(@Nullable Bundle savedInstanceState) {
         mThemeManager.fetchOptions(options -> {
             mOptionsController = new OptionSelectorController(mOptionsContainer, options);
 
             mOptionsController.addListener(selected -> {
                 mSelectedTheme = (ThemeBundle) selected;
+                mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper);
                 createAdapter();
             });
             mOptionsController.initOptions(mThemeManager);
+            String previouslySelected = savedInstanceState != null
+                    ? savedInstanceState.getString(KEY_SELECTED_THEME) : null;
+
             for (ThemeBundle theme : options) {
-                if (theme.isActive(mThemeManager)) {
+                if (previouslySelected != null
+                        && previouslySelected.equals(theme.getSerializedPackages())) {
+                    mSelectedTheme = theme;
+                } else if (theme.isActive(mThemeManager)) {
                     mSelectedTheme = theme;
                 }
             }
-            // For development only, as there should always be a theme set.
-            if (mSelectedTheme == null) {
-                mSelectedTheme = options.get(0);
-            }
             mOptionsController.setSelectedOption(mSelectedTheme);
         });
         createAdapter();
@@ -130,8 +207,9 @@
         @ColorInt final int accentColor;
         private final LayoutInflater inflater;
 
-        private ThemePreviewPage(Context context, @StringRes int titleResId, @DrawableRes int iconSrc,
-                @LayoutRes int contentLayoutRes, @ColorInt int accentColor) {
+        private ThemePreviewPage(Context context, @StringRes int titleResId,
+                @DrawableRes int iconSrc, @LayoutRes int contentLayoutRes,
+                @ColorInt int accentColor) {
             super(null);
             this.nameResId = titleResId;
             this.iconSrc = iconSrc;
@@ -149,10 +227,14 @@
 
             ViewGroup body = card.findViewById(R.id.theme_preview_card_body_container);
             inflater.inflate(contentLayoutRes, body, true);
-            bindBody();
+            bindBody(false);
         }
 
-        protected abstract void bindBody();
+        protected boolean containsWallpaper() {
+            return false;
+        }
+
+        protected abstract void bindBody(boolean forceRebind);
     }
 
     /**
@@ -173,7 +255,7 @@
             addPage(new ThemePreviewPage(activity, R.string.preview_name_font, R.drawable.ic_font,
                     R.layout.preview_card_font_content, theme.getPreviewInfo().colorAccentLight) {
                 @Override
-                protected void bindBody() {
+                protected void bindBody(boolean forceRebind) {
                     TextView title = card.findViewById(R.id.font_card_title);
                     title.setTypeface(theme.getPreviewInfo().headlineFontFamily);
                     TextView body = card.findViewById(R.id.font_card_body);
@@ -185,7 +267,7 @@
                         R.drawable.ic_wifi_24px, R.layout.preview_card_icon_content,
                         theme.getPreviewInfo().colorAccentLight) {
                     @Override
-                    protected void bindBody() {
+                    protected void bindBody(boolean forceRebind) {
                         for (int i = 0; i < mIconIds.length; i++) {
                             ((ImageView) card.findViewById(mIconIds[i])).setImageDrawable(
                                     theme.getPreviewInfo().icons.get(i));
@@ -193,15 +275,14 @@
                     }
                 });
             }
-            if (theme.getPreviewInfo().colorPreviewDrawable != null) {
+            if (theme.getPreviewInfo().colorPreviewAsset != null) {
                 addPage(new ThemePreviewPage(activity, R.string.preview_name_color,
                         R.drawable.ic_colorize_24px, R.layout.preview_card_static_content,
                         theme.getPreviewInfo().colorAccentLight) {
                     @Override
-                    protected void bindBody() {
+                    protected void bindBody(boolean forceRebind) {
                         ImageView staticImage = card.findViewById(R.id.preview_static_image);
-
-                        theme.getPreviewInfo().colorPreviewDrawable.loadDrawable(activity,
+                        theme.getPreviewInfo().colorPreviewAsset.loadDrawable(activity,
                                 staticImage, card.getCardBackgroundColor().getDefaultColor());
                         staticImage.getLayoutParams().width = res.getDimensionPixelSize(
                                 R.dimen.color_preview_image_width);
@@ -210,14 +291,14 @@
                     }
                 });
             }
-            if (theme.getPreviewInfo().shapePreviewDrawable != null) {
+            if (theme.getPreviewInfo().shapePreviewAsset != null) {
                 addPage(new ThemePreviewPage(activity, R.string.preview_name_shape,
                         R.drawable.ic_shapes_24px, R.layout.preview_card_static_content,
                         theme.getPreviewInfo().colorAccentLight) {
                     @Override
-                    protected void bindBody() {
+                    protected void bindBody(boolean forceRebind) {
                         ImageView staticImage = card.findViewById(R.id.preview_static_image);
-                        theme.getPreviewInfo().shapePreviewDrawable.loadDrawable(activity,
+                        theme.getPreviewInfo().shapePreviewAsset.loadDrawable(activity,
                                 staticImage, card.getCardBackgroundColor().getDefaultColor());
 
                         staticImage.getLayoutParams().width = res.getDimensionPixelSize(
@@ -227,6 +308,63 @@
                     }
                 });
             }
+            if (theme.getPreviewInfo().wallpaperAsset != null) {
+                addPage(new ThemePreviewPage(activity, R.string.preview_name_wallpaper,
+                        R.drawable.ic_wallpaper_24px, R.layout.preview_card_wallpaper_content,
+                        theme.getPreviewInfo().colorAccentLight) {
+
+                    private final WallpaperPreviewLayoutListener  mListener =
+                            new WallpaperPreviewLayoutListener(theme);
+
+                    @Override
+                    protected boolean containsWallpaper() {
+                        return true;
+                    }
+
+                    @Override
+                    protected void bindBody(boolean forceRebind) {
+                        if (card == null) {
+                            return;
+                        }
+                        card.addOnLayoutChangeListener(mListener);
+                        if (forceRebind) {
+                            card.requestLayout();
+                        }
+                    }
+                });
+            }
+        }
+
+        public void rebindWallpaperIfAvailable() {
+            for (ThemePreviewPage page : mPages) {
+                if (page.containsWallpaper()) {
+                    page.bindBody(true);
+                }
+            }
+        }
+
+        private static class WallpaperPreviewLayoutListener implements OnLayoutChangeListener {
+            private final ThemeBundle mTheme;
+
+            public WallpaperPreviewLayoutListener(ThemeBundle theme) {
+                mTheme = theme;
+            }
+
+            @Override
+            public void onLayoutChange(View view, int left, int top, int right,
+                    int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                int targetWidth = right - left;
+                int targetHeight = bottom - top;
+                if (targetWidth > 0 && targetHeight > 0) {
+                    mTheme.getWallpaperPreviewAsset(view.getContext()).decodeBitmap(
+                            targetWidth, targetHeight, bitmap -> {
+                                Resources res = view.getContext().getResources();
+                                view.findViewById(R.id.theme_preview_card_background)
+                                        .setBackground(new BitmapDrawable(res, bitmap));
+                            });
+                    view.removeOnLayoutChangeListener(this);
+                }
+            }
         }
     }
 }