ThemePicker: First iteration of Grid section

Create the Fragment and provider to interface with
Launcher.
Some polish is still pending, as well as applying the grid
to the launcher.

Bug: 120560197
Change-Id: I5fc35c901a33f2163d2864bfc9e177c74d49aa36
diff --git a/res/layout/fragment_grid_picker.xml b/res/layout/fragment_grid_picker.xml
new file mode 100644
index 0000000..536f1f7
--- /dev/null
+++ b/res/layout/fragment_grid_picker.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@color/category_picker_background_color">
+    <include layout="@layout/section_header"/>
+
+    <com.android.customization.widget.PreviewPager
+        android:id="@+id/grid_preview_pager"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:background="@color/secondary_color"/>
+
+    <LinearLayout
+        android:id="@+id/options_section"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:orientation="vertical">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/options_container"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/options_container_height"
+            android:layout_gravity="center_horizontal"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+            <Button
+                style="@style/ActionPrimaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:text="@string/apply_btn"/>
+        </RelativeLayout>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/grid_option.xml b/res/layout/grid_option.xml
new file mode 100644
index 0000000..0dac4fe
--- /dev/null
+++ b/res/layout/grid_option.xml
@@ -0,0 +1,42 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/option_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginBottom="@dimen/theme_option_label_margin"
+        android:textAppearance="@style/OptionTitleTextAppearance"/>
+    <FrameLayout
+        android:id="@+id/option_tile"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="@dimen/option_tile_width"
+        android:layout_gravity="center_horizontal"
+        android:padding="@dimen/option_tile_padding_vertical"
+        android:background="@drawable/option_border"
+        android:gravity="center">
+        <ImageView
+            android:id="@+id/grid_option_thumbnail"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/grid_preview_card.xml b/res/layout/grid_preview_card.xml
new file mode 100644
index 0000000..ae66b83
--- /dev/null
+++ b/res/layout/grid_preview_card.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+<androidx.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/FullContentPreviewCard"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ImageView
+        android:id="@+id/grid_preview_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/primary_color"/>
+
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/res/menu/bottom_navigation_menu.xml b/res/menu/bottom_navigation_menu.xml
index a856222..d477aaf 100644
--- a/res/menu/bottom_navigation_menu.xml
+++ b/res/menu/bottom_navigation_menu.xml
@@ -23,11 +23,11 @@
     <item
         android:id="@+id/nav_clock"
         android:title="@string/clock_title"
-        android:icon="@drawable/ic_nav_grid" />
+        android:icon="@drawable/ic_nav_clock" />
     <item
         android:id="@+id/nav_grid"
         android:title="@string/grid_title"
-        android:icon="@drawable/ic_nav_theme" />
+        android:icon="@drawable/ic_nav_grid" />
     <item
         android:id="@+id/nav_wallpaper"
         android:title="@string/wallpaper_title"
diff --git a/res/values/override.xml b/res/values/override.xml
index b40eec8..82fc964 100644
--- a/res/values/override.xml
+++ b/res/values/override.xml
@@ -17,4 +17,6 @@
 -->
 <resources>
     <string name="themes_stub_package" translatable="false"/>
+
+    <string name="grid_control_authority" translatable="false"/>
 </resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a09afaa..d87a354 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -38,6 +38,10 @@
         [CHAR LIMIT=20] -->
     <string name="apply_theme_btn">Apply Theme</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>
+
     <!-- Accessibility label for paging indicator in theme picker preview [CHAR LIMIT=NONE] -->
     <string name="accessibility_preview_pager">Page <xliff:g name="current_page" example="1">%1$d</xliff:g> of <xliff:g name="num_pages" example="2">%2$d</xliff:g></string>
     <!-- Content description of the next button to bring user to the next preview page.[CHAR LIMIT=NONE] -->
@@ -58,4 +62,7 @@
 
     <!-- Body text for previewing a font [CHAR LIMIT=160] -->
     <string name="font_card_body">My Style on every screen!\nOnce you install a theme, its design extends to every facet of the phone, from the home screen to icons, Quick Settings, etc.</string>
+
+    <!--Title for a grid option, describing the number of columns and rows, eg: 4x4 [CHAR LIMIT=10] -->
+    <string name="grid_title_pattern"><xliff:g name="num_cols" example="1">%1$d</xliff:g>x<xliff:g name="num_rows" example="1">%2$d</xliff:g></string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1e46cad..4e5103c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -63,6 +63,13 @@
         <item name="android:background">@color/primary_color</item>
     </style>
 
+    <style name="FullContentPreviewCard" parent="PreviewCard">
+        <item name="cardCornerRadius">@dimen/preview_card_corner_radius</item>
+        <item name="android:clipChildren">true</item>
+        <item name="contentPadding">0dp</item>
+        <item name="android:background">@color/primary_color</item>
+    </style>
+
     <style name="CardTitleTextAppearance" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
         <item name="android:textStyle">bold</item>
         <item name="android:textSize">@dimen/card_title_text_size</item>
diff --git a/src/com/android/customization/model/ResourceConstants.java b/src/com/android/customization/model/ResourceConstants.java
new file mode 100644
index 0000000..3158e60
--- /dev/null
+++ b/src/com/android/customization/model/ResourceConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Holds common strings used to reference system resources.
+ */
+public interface ResourceConstants {
+
+    /**
+     * Package name for android platform resources.
+     */
+    String ANDROID_PACKAGE = "android";
+
+    /**
+     * Name of the system resource for icon mask
+     */
+    String CONFIG_ICON_MASK = "config_icon_mask";
+
+}
diff --git a/src/com/android/customization/model/grid/GridOption.java b/src/com/android/customization/model/grid/GridOption.java
new file mode 100644
index 0000000..1bc34d0
--- /dev/null
+++ b/src/com/android/customization/model/grid/GridOption.java
@@ -0,0 +1,78 @@
+/*
+ * 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.grid;
+
+import android.content.Context;
+import android.graphics.PorterDuff.Mode;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.widget.GridTileDrawable;
+import com.android.wallpaper.R;
+
+/**
+ * Represents a grid layout option available in the current launcher.
+ */
+public class GridOption implements CustomizationOption {
+
+    private final String mTitle;
+    private final boolean mIsCurrent;
+    private final GridTileDrawable mTileDrawable;
+    public final String name;
+    public final int rows;
+    public final int cols;
+    public final Uri previewImageUri;
+    public final int previewPagesCount;
+
+    public GridOption(String title, String name, boolean isCurrent, int rows, int cols,
+            Uri previewImageUri, int previewPagesCount, String iconShapePath) {
+        mTitle = title;
+        mIsCurrent = isCurrent;
+        mTileDrawable = new GridTileDrawable(rows, cols, iconShapePath);
+        this.name = name;
+        this.rows = rows;
+        this.cols = cols;
+        this.previewImageUri = previewImageUri;
+        this.previewPagesCount = previewPagesCount;
+    }
+
+    @Override
+    public String getTitle() {
+        return mTitle;
+    }
+
+    @Override
+    public void bindThumbnailTile(View view) {
+        Context context = view.getContext();
+
+        mTileDrawable.setColorFilter(context.getResources().getColor(
+                R.color.material_grey500, null), Mode.ADD);
+        ((ImageView) view.findViewById(R.id.grid_option_thumbnail))
+                .setImageDrawable(mTileDrawable);
+    }
+
+    @Override
+    public boolean isCurrentlySet() {
+        return mIsCurrent;
+    }
+
+    @Override
+    public int getLayoutResId() {
+        return R.layout.grid_option;
+    }
+}
diff --git a/src/com/android/customization/model/grid/GridOptionsManager.java b/src/com/android/customization/model/grid/GridOptionsManager.java
new file mode 100644
index 0000000..365a822
--- /dev/null
+++ b/src/com/android/customization/model/grid/GridOptionsManager.java
@@ -0,0 +1,75 @@
+/*
+ * 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.grid;
+
+import android.os.AsyncTask;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager;
+
+import java.util.List;
+
+/**
+ * {@link CustomizationManager} for interfacing with the launcher to handle {@link GridOption}s.
+ */
+public class GridOptionsManager implements CustomizationManager<GridOption> {
+
+    private final LauncherGridOptionsProvider mProvider;
+
+    public GridOptionsManager(LauncherGridOptionsProvider provider) {
+        mProvider = provider;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mProvider.areGridsAvailable();
+    }
+
+    @Override
+    public void apply(GridOption option) {
+        mProvider.applyGrid(option.name);
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<GridOption> callback) {
+        new FetchTask(mProvider, callback).execute();
+    }
+
+    private static class FetchTask extends AsyncTask<Void, Void, List<GridOption>> {
+        private final LauncherGridOptionsProvider mProvider;
+        @Nullable private final OptionsFetchedListener<GridOption> mCallback;
+
+        private FetchTask(@NonNull LauncherGridOptionsProvider provider,
+                @Nullable OptionsFetchedListener<GridOption> callback) {
+            mCallback = callback;
+            mProvider = provider;
+        }
+
+        @Override
+        protected List<GridOption> doInBackground(Void[] params) {
+            return mProvider.fetch(false);
+        }
+
+        @Override
+        protected void onPostExecute(List<GridOption> gridOptions) {
+            if (mCallback != null) {
+                mCallback.onOptionsLoaded(gridOptions);
+            }
+        }
+    }
+}
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
new file mode 100644
index 0000000..d43cdc7
--- /dev/null
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -0,0 +1,116 @@
+/*
+ * 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.grid;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.wallpaper.R;
+
+import com.bumptech.glide.Glide;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstracts the logic to retrieve available grid options from the current Launcher.
+ */
+public class LauncherGridOptionsProvider {
+
+    private static final String LIST_OPTIONS = "list_options";
+    private static final String PREVIEW = "preview";
+
+    private static final String COL_NAME = "name";
+    private static final String COL_ROWS = "rows";
+    private static final String COL_COLS = "cols";
+    private static final String COL_PREVIEW_COUNT = "preview_count";
+    private static final String COL_IS_DEFAULT = "is_default";
+
+    private final Context mContext;
+    private final String mGridProviderAuthority;
+    private final ProviderInfo mProviderInfo;
+    private List<GridOption> mOptions;
+
+    public LauncherGridOptionsProvider(Context context) {
+        mContext = context;
+        // TODO: read this from Activity metadata instead
+        mGridProviderAuthority = mContext.getString(R.string.grid_control_authority);
+        // TODO: check permissions if needed
+        mProviderInfo = TextUtils.isEmpty(mGridProviderAuthority) ? null
+                : mContext.getPackageManager().resolveContentProvider(mGridProviderAuthority, 0);
+    }
+
+    boolean areGridsAvailable() {
+        return mProviderInfo != null;
+    }
+
+    /**
+     * Retrieve the available grids.
+     * @param reload whether to reload grid options if they're cached.
+     */
+    @WorkerThread
+    List<GridOption> fetch(boolean reload) {
+        if (!areGridsAvailable()) {
+            return null;
+        }
+        if (mOptions != null && !reload) {
+            return mOptions;
+        }
+        Uri optionsUri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(mProviderInfo.authority)
+                .appendPath(LIST_OPTIONS)
+                .build();
+        ContentResolver resolver = mContext.getContentResolver();
+        String iconPath = mContext.getResources().getString(Resources.getSystem().getIdentifier(
+                ResourceConstants.CONFIG_ICON_MASK, "string", ResourceConstants.ANDROID_PACKAGE));
+        try (Cursor c = resolver.query(optionsUri, null, null, null, null)) {
+            mOptions = new ArrayList<>();
+            while(c.moveToNext()) {
+                String name = c.getString(c.getColumnIndex(COL_NAME));
+                int rows = c.getInt(c.getColumnIndex(COL_ROWS));
+                int cols = c.getInt(c.getColumnIndex(COL_COLS));
+                int previewCount = c.getInt(c.getColumnIndex(COL_PREVIEW_COUNT));
+                boolean isSet = Boolean.valueOf(c.getString(c.getColumnIndex(COL_IS_DEFAULT)));
+                Uri preview = new Uri.Builder()
+                        .scheme(ContentResolver.SCHEME_CONTENT)
+                        .authority(mProviderInfo.authority)
+                        .appendPath(PREVIEW)
+                        .appendPath(name)
+                        .build();
+                String title = mContext.getString(R.string.grid_title_pattern, cols, rows);
+                mOptions.add(new GridOption(title, name, isSet, rows, cols, preview, previewCount,
+                        iconPath));
+            }
+            Glide.get(mContext).clearDiskCache();
+        } catch (Exception e) {
+            mOptions = null;
+        }
+        return mOptions;
+    }
+
+    void applyGrid(String name) {
+        //TODO: implement
+    }
+}
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
index 92c7946..6d67071 100644
--- a/src/com/android/customization/model/theme/DefaultThemeProvider.java
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -15,6 +15,9 @@
  */
 package com.android.customization.model.theme;
 
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.CONFIG_ICON_MASK;
+
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -56,8 +59,6 @@
     private static final String ACCENT_COLOR_DARK_NAME = "accent_device_default_dark";
     private static final String CONFIG_BODY_FONT_FAMILY = "config_bodyFontFamily";
     private static final String CONFIG_HEADLINE_FONT_FAMILY = "config_headlineFontFamily";
-    private static final String CONFIG_ICON_MASK = "config_icon_mask";
-    private static final String ANDROID_PACKAGE = "android";
 
     private final Context mContext;
     private final String mStubPackageName;
diff --git a/src/com/android/customization/picker/CustomizationPickerActivity.java b/src/com/android/customization/picker/CustomizationPickerActivity.java
index c27cb90..f0db8a1 100644
--- a/src/com/android/customization/picker/CustomizationPickerActivity.java
+++ b/src/com/android/customization/picker/CustomizationPickerActivity.java
@@ -28,6 +28,15 @@
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.grid.GridOption;
+import com.android.customization.model.grid.GridOptionsManager;
+import com.android.customization.model.grid.LauncherGridOptionsProvider;
+import com.android.customization.model.theme.DefaultThemeProvider;
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.picker.grid.GridFragment;
 import com.android.customization.picker.theme.ThemeFragment;
 import com.android.wallpaper.R;
 import com.android.wallpaper.model.WallpaperInfo;
@@ -103,7 +112,18 @@
     }
 
     private void initSections() {
-        mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme));
+        //Theme
+        ThemeManager themeManager = new ThemeManager(new DefaultThemeProvider(this));
+        if (themeManager.isAvailable()) {
+            mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme, themeManager));
+        }
+        // TODO: clock
+        //Grid
+        GridOptionsManager gridManager = new GridOptionsManager(
+                new LauncherGridOptionsProvider(this));
+        if (gridManager.isAvailable()) {
+            mSections.put(R.id.nav_grid, new GridSection(R.id.nav_grid, gridManager));
+        }
         mSections.put(R.id.nav_wallpaper, new WallpaperSection(R.id.nav_wallpaper));
         //TODO (santie): add other sections if supported by the device
     }
@@ -190,15 +210,17 @@
      * There should be a concrete subclass per available section, providing the corresponding
      * Fragment to be displayed when switching to each section.
      */
-    static abstract class CustomizationSection {
+    static abstract class CustomizationSection<T extends CustomizationOption> {
 
         /**
          * IdRes used to identify this section in the BottomNavigationView menu.
          */
         @IdRes final int id;
+        protected final CustomizationManager<T> mCustomizationManager;
 
-        private CustomizationSection(@IdRes int id) {
+        private CustomizationSection(@IdRes int id, CustomizationManager<T> manager) {
             this.id = id;
+            this.mCustomizationManager = manager;
         }
 
         /**
@@ -216,7 +238,7 @@
         private boolean mForceCategoryRefresh;
 
         private WallpaperSection(int id) {
-            super(id);
+            super(id, null);
         }
 
         @Override
@@ -235,15 +257,39 @@
         }
     }
 
-    private class ThemeSection extends CustomizationSection {
+    private class ThemeSection extends CustomizationSection<ThemeBundle> {
 
-        private ThemeSection(int id) {
-            super(id);
+        private ThemeFragment mFragment;
+
+        private ThemeSection(int id, ThemeManager manager) {
+            super(id, manager);
         }
 
         @Override
         Fragment getFragment() {
-            return ThemeFragment.newInstance(getString(R.string.theme_title));
+            if (mFragment == null) {
+                mFragment = ThemeFragment.newInstance(getString(R.string.theme_title),
+                        (ThemeManager) mCustomizationManager);
+            }
+            return mFragment;
+        }
+    }
+
+    private class GridSection extends CustomizationSection<GridOption> {
+
+        private GridFragment mFragment;
+
+        private GridSection(int id, GridOptionsManager manager) {
+            super(id, manager);
+        }
+
+        @Override
+        Fragment getFragment() {
+            if (mFragment == null) {
+                mFragment = GridFragment.newInstance(getString(R.string.grid_title),
+                        (GridOptionsManager) mCustomizationManager);
+            }
+            return mFragment;
         }
     }
 }
diff --git a/src/com/android/customization/picker/grid/GridFragment.java b/src/com/android/customization/picker/grid/GridFragment.java
new file mode 100644
index 0000000..5864c8e
--- /dev/null
+++ b/src/com/android/customization/picker/grid/GridFragment.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2018 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.picker.grid;
+
+import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.customization.model.grid.GridOption;
+import com.android.customization.model.grid.GridOptionsManager;
+import com.android.customization.widget.OptionSelectorController;
+import com.android.customization.widget.PreviewPager;
+import com.android.wallpaper.R;
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.asset.ContentUriAsset;
+import com.android.wallpaper.picker.ToolbarFragment;
+
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment that contains the UI for selecting and applying a GridOption.
+ */
+public class GridFragment extends ToolbarFragment {
+
+    public static GridFragment newInstance(CharSequence title, GridOptionsManager manager) {
+        GridFragment fragment = new GridFragment();
+        fragment.setManager(manager);
+        fragment.setArguments(ToolbarFragment.createArguments(title));
+        return fragment;
+    }
+
+    private RecyclerView mOptionsContainer;
+    private OptionSelectorController mOptionsController;
+    private GridOptionsManager mGridManager;
+    private GridOption mSelectedOption;
+    private PreviewPager mPreviewPager;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(
+                R.layout.fragment_grid_picker, container, /* attachToRoot */ false);
+        setUpToolbar(view);
+        mPreviewPager = view.findViewById(R.id.grid_preview_pager);
+        mOptionsContainer = view.findViewById(R.id.options_container);
+        setUpOptions();
+
+        return view;
+    }
+
+    private void setManager(GridOptionsManager manager) {
+        mGridManager = manager;
+    }
+
+    private void createAdapter() {
+        mPreviewPager.setAdapter(new GridPreviewAdapter(mSelectedOption));
+    }
+
+    private void setUpOptions() {
+        mGridManager.fetchOptions(options -> {
+            mOptionsController = new OptionSelectorController(mOptionsContainer, options);
+
+            mOptionsController.addListener(selected -> {
+                mSelectedOption = (GridOption) selected;
+                createAdapter();
+            });
+            mOptionsController.initOptions();
+            for (GridOption option : options) {
+                if (option.isCurrentlySet()) {
+                    mSelectedOption = option;
+                }
+            }
+            // For development only, as there should always be a grid set.
+            if (mSelectedOption == null) {
+                mSelectedOption = options.get(0);
+            }
+            createAdapter();
+        });
+    }
+
+    /**
+     * Adapter class for mPreviewPager.
+     * This is a ViewPager as it allows for a nice pagination effect (ie, pages snap on swipe,
+     * we don't want to just scroll)
+     */
+    private class GridPreviewAdapter extends PagerAdapter {
+
+        private class PreviewPage {
+            final int pageId;
+            final Asset previewAsset;
+            final int cols;
+            final int rows;
+
+            CardView card;
+
+            private PreviewPage(Context context, int id, Uri previewUri, int rows, int cols) {
+                pageId = id;
+                previewAsset = new ContentUriAsset(context, previewUri,
+                        RequestOptions.fitCenterTransform());
+                this.rows = rows;
+                this.cols = cols;
+            }
+
+            public void setCard(CardView card) {
+                this.card = card;
+            }
+
+            void bindPreviewContent() {
+                previewAsset.loadDrawable(getActivity(), card.findViewById(R.id.grid_preview_image),
+                        card.getContext().getResources().getColor(R.color.primary_color, null));
+            }
+        }
+
+        private final List<PreviewPage> mPages = new ArrayList<>();
+
+        GridPreviewAdapter(GridOption gridOption) {
+            for (int i = 0; i < gridOption.previewPagesCount; i++) {
+                mPages.add(new PreviewPage(getContext(), i,
+                        gridOption.previewImageUri.buildUpon().appendPath("" + i).build(),
+                        gridOption.rows, gridOption.cols));
+            }
+        }
+
+        @Override
+        public int getCount() {
+            return mPages.size();
+        }
+
+        @Override
+        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+            return view == ((PreviewPage)object).card;
+        }
+
+        @NonNull
+        @Override
+        public Object instantiateItem(@NonNull ViewGroup container, int position) {
+            if (ViewCompat.getLayoutDirection(container) == LAYOUT_DIRECTION_RTL) {
+                position = mPages.size() - 1 - position;
+            }
+            LayoutInflater inflater = LayoutInflater.from(getContext());
+            CardView card = (CardView) inflater.inflate(R.layout.grid_preview_card,
+                    container, false);
+            PreviewPage page = mPages.get(position);
+
+            page.setCard(card);
+            page.bindPreviewContent();
+            if (card.getParent() != null) {
+                container.removeView(card);
+            }
+            container.addView(card);
+            return page;
+        }
+
+        @Override
+        public void destroyItem(@NonNull ViewGroup container, int position,
+                @NonNull Object object) {
+            ((PreviewPage) object).card = null;
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
index 2645576..179c08f 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -35,7 +35,6 @@
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.viewpager.widget.PagerAdapter;
 
-import com.android.customization.model.theme.DefaultThemeProvider;
 import com.android.customization.model.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeManager;
 import com.android.customization.widget.OptionSelectorController;
@@ -51,8 +50,9 @@
  */
 public class ThemeFragment extends ToolbarFragment {
 
-    public static ThemeFragment newInstance(CharSequence title) {
+    public static ThemeFragment newInstance(CharSequence title, ThemeManager manager) {
         ThemeFragment fragment = new ThemeFragment();
+        fragment.setManager(manager);
         fragment.setArguments(ToolbarFragment.createArguments(title));
         return fragment;
     }
@@ -72,13 +72,16 @@
                 R.layout.fragment_theme_picker, container, /* attachToRoot */ false);
         setUpToolbar(view);
         mPreviewPager = view.findViewById(R.id.theme_preview_pager);
-        mThemeManager = new ThemeManager(new DefaultThemeProvider(getContext()));
         mOptionsContainer = view.findViewById(R.id.options_container);
         setUpOptions();
 
         return view;
     }
 
+    private void setManager(ThemeManager manager) {
+        mThemeManager = manager;
+    }
+
     private void createAdapter() {
         mAdapter = new ThemePreviewAdapter(mSelectedTheme);
         mPreviewPager.setAdapter(mAdapter);
diff --git a/src/com/android/customization/widget/GridTileDrawable.java b/src/com/android/customization/widget/GridTileDrawable.java
new file mode 100644
index 0000000..c746aaf
--- /dev/null
+++ b/src/com/android/customization/widget/GridTileDrawable.java
@@ -0,0 +1,81 @@
+package com.android.customization.widget;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.graphics.PathParser;
+
+/**
+ * Drawable that draws a grid rows x cols of icon shapes adjusting their size to fit within its
+ * bounds.
+ */
+public class GridTileDrawable extends Drawable {
+
+    // Path is expected configuration in following dimension: [100 x 100]))
+    private static final float PATH_SIZE = 100f;
+    // We want each "icon" using 80% of the available size, so there's 10% padding on each side
+    private static final float ICON_SCALE = .8f;
+    private final int mCols;
+    private final int mRows;
+    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Path mShapePath;
+    private final Path mTransformedPath;
+    private final Matrix mScaleMatrix;
+    private float mCellSize = -1f;
+    private float mSpaceBetweenIcons;
+
+    public GridTileDrawable(int cols, int rows, String path) {
+        mCols = cols;
+        mRows = rows;
+
+        mShapePath = PathParser.createPathFromPathData(path);
+        mTransformedPath = new Path(mShapePath);
+        mScaleMatrix = new Matrix();
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        mCellSize = (float) bounds.height() / mRows;
+        mSpaceBetweenIcons = mCellSize * ((1 - ICON_SCALE) / 2);
+
+        float scaleFactor = (mCellSize * ICON_SCALE) / PATH_SIZE;
+        mScaleMatrix.setScale(scaleFactor, scaleFactor);
+        mShapePath.transform(mScaleMatrix, mTransformedPath);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        for (int r = 0; r < mRows; r++) {
+            for (int c = 0; c < mCols; c++) {
+                int saveCount = canvas.save();
+                float x = (c * mCellSize) + mSpaceBetweenIcons;
+                float y = (r * mCellSize) + mSpaceBetweenIcons;
+                canvas.translate(x, y);
+                canvas.drawPath(mTransformedPath, mPaint);
+                canvas.restoreToCount(saveCount);
+            }
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+}