ThemePicker: Implement the Clock section

Add a Fragment to show and apply available clockfaces.
A few more iterations are needed on the UI but this CL focuses
on the functionality.

Bug: 120560400

Change-Id: I809cb861cf4c03c4b6d84b94e839300dbea200a1
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dae680e..48a3941 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,7 +17,7 @@
         android:restoreAnyVersion="true"
         android:supportsRtl="true">
 
-        <activity android:name="com.android.theme.picker.ThemePickerActivity"
+        <activity android:name="com.android.customization.picker.CustomizationPickerActivity"
             android:label="@string/app_name"
             android:theme="@style/CustomizationTheme.NoActionBar"
             android:resizeableActivity="true">
diff --git a/res/layout/clock_option.xml b/res/layout/clock_option.xml
new file mode 100644
index 0000000..467263d
--- /dev/null
+++ b/res/layout/clock_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:paddingHorizontal="@dimen/option_tile_padding_horizontal"
+        android:paddingVertical="@dimen/option_tile_padding_vertical"
+        android:background="@drawable/option_border">
+        <ImageView
+            android:id="@+id/clock_option_thumbnail"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/clock_preview_card.xml b/res/layout/clock_preview_card.xml
new file mode 100644
index 0000000..63c55e1
--- /dev/null
+++ b/res/layout/clock_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/clock_preview_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/clockface_preview_background"/>
+
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/res/layout/fragment_clock_picker.xml b/res/layout/fragment_clock_picker.xml
new file mode 100644
index 0000000..b61c268
--- /dev/null
+++ b/res/layout/fragment_clock_picker.xml
@@ -0,0 +1,59 @@
+<?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/clock_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
+                android:id="@+id/apply_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/values/colors.xml b/res/values/colors.xml
index bbfb5df..4755c37 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -23,4 +23,6 @@
 
     <color name="shape_thumbnail_color">#b2b2b2</color>
     <color name="icon_thumbnail_color">@android:color/black</color>
+
+    <color name="clockface_preview_background">@android:color/black</color>
 </resources>
\ No newline at end of file
diff --git a/res/values/override.xml b/res/values/override.xml
index 82fc964..8d2079c 100644
--- a/res/values/override.xml
+++ b/res/values/override.xml
@@ -17,6 +17,7 @@
 -->
 <resources>
     <string name="themes_stub_package" translatable="false"/>
+    <string name="clocks_stub_package" translatable="false"/>
 
     <string name="grid_control_authority" translatable="false"/>
 </resources>
\ No newline at end of file
diff --git a/src/com/android/customization/model/CustomizationOption.java b/src/com/android/customization/model/CustomizationOption.java
index c89d12e..87c8690 100644
--- a/src/com/android/customization/model/CustomizationOption.java
+++ b/src/com/android/customization/model/CustomizationOption.java
@@ -15,6 +15,7 @@
  */
 package com.android.customization.model;
 
+import android.content.Context;
 import android.view.View;
 
 import androidx.annotation.LayoutRes;
@@ -43,7 +44,7 @@
     /**
      * Returns whether this option is the one currently set in the System.
      */
-    boolean isCurrentlySet();
+    boolean isActive(Context context);
 
     /**
      * Return the id of the layout used to show this option in the UI. It must contain a view with
diff --git a/src/com/android/customization/model/ResourcesApkProvider.java b/src/com/android/customization/model/ResourcesApkProvider.java
new file mode 100644
index 0000000..f770dbc
--- /dev/null
+++ b/src/com/android/customization/model/ResourcesApkProvider.java
@@ -0,0 +1,61 @@
+package com.android.customization.model;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+public abstract class ResourcesApkProvider {
+    private static final String TAG = "ResourcesApkProvider";
+
+    protected final Context mContext;
+    protected final String mStubPackageName;
+    protected final Resources mStubApkResources;
+
+    public ResourcesApkProvider(Context context, String stubPackageName) {
+        mContext = context;
+        mStubPackageName = stubPackageName;
+        if (TextUtils.isEmpty(mStubPackageName)) {
+            mStubApkResources = null;
+        } else {
+            Resources apkResources = null;
+            try {
+                PackageManager pm = mContext.getPackageManager();
+                ApplicationInfo stubAppInfo = pm.getApplicationInfo(
+                        mStubPackageName, PackageManager.GET_META_DATA);
+                if (stubAppInfo != null) {
+                    apkResources = pm.getResourcesForApplication(stubAppInfo);
+                }
+            } catch (NameNotFoundException e) {
+                Log.w(TAG, String.format("Stub APK for %s not found.", mStubPackageName));
+            } finally {
+                mStubApkResources = apkResources;
+            }
+        }
+    }
+
+    protected String[] getItemsFromStub(String arrayName) {
+        int themesListResId = mStubApkResources.getIdentifier(arrayName, "array",  mStubPackageName);
+        return mStubApkResources.getStringArray(themesListResId);
+    }
+
+    protected String getItemStringFromStub(String prefix, String itemName) {
+        int resourceId = mStubApkResources.getIdentifier(String.format("%s%s", prefix, itemName),
+                "string", mStubPackageName);
+        return mStubApkResources.getString(resourceId);
+    }
+
+    protected Drawable getItemDrawableFromStub(String prefix, String itemName) {
+        int resourceId = mStubApkResources.getIdentifier(String.format("%s%s", prefix, itemName),
+                "drawable", mStubPackageName);
+        return mStubApkResources.getDrawable(resourceId, null);
+    }
+
+    public boolean isAvailable() {
+        return mStubApkResources != null;
+    }
+}
diff --git a/src/com/android/customization/model/clock/ClockManager.java b/src/com/android/customization/model/clock/ClockManager.java
new file mode 100644
index 0000000..d516110
--- /dev/null
+++ b/src/com/android/customization/model/clock/ClockManager.java
@@ -0,0 +1,48 @@
+/*
+ * 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.clock;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import com.android.customization.model.CustomizationManager;
+
+public class ClockManager implements CustomizationManager<Clockface> {
+
+    private final ClockProvider mClockProvider;
+    private final Context mContext;
+
+    public ClockManager(Context context, ClockProvider provider) {
+        mClockProvider = provider;
+        mContext = context;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mClockProvider.isAvailable();
+    }
+
+    @Override
+    public void apply(Clockface option) {
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Clockface.CLOCK_FACE_SETTING, option.getId());
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<Clockface> callback) {
+        mClockProvider.fetch(callback, false);
+    }
+}
diff --git a/src/com/android/customization/model/clock/ClockProvider.java b/src/com/android/customization/model/clock/ClockProvider.java
new file mode 100644
index 0000000..d9a6779
--- /dev/null
+++ b/src/com/android/customization/model/clock/ClockProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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.clock;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+
+/**
+ * Interface for a class that can retrieve Themes from the system.
+ */
+public interface ClockProvider {
+    /**
+     * Returns whether clockfaces are available in the current setup.
+     */
+    boolean isAvailable();
+
+    /**
+     * Retrieve the available clockfaces.
+     * @param callback called when the clockfaces have been retrieved (or immediately if cached)
+     * @param reload whether to reload clockfaces if they're cached.
+     */
+    void fetch(OptionsFetchedListener<Clockface> callback, boolean reload);
+}
diff --git a/src/com/android/customization/model/clock/Clockface.java b/src/com/android/customization/model/clock/Clockface.java
new file mode 100644
index 0000000..4d81677
--- /dev/null
+++ b/src/com/android/customization/model/clock/Clockface.java
@@ -0,0 +1,106 @@
+/*
+ * 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.clock;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.customization.model.CustomizationOption;
+import com.android.wallpaper.R;
+
+public class Clockface implements CustomizationOption {
+
+    // TODO: use constant from Settings.Secure
+    static final String CLOCK_FACE_SETTING = "lock_screen_custom_clock_face";
+    private final String mTitle;
+    private final String mId;
+    private final Drawable mPreview;
+    private final Drawable mThumbnail;
+
+    private Clockface(String title, String id, Drawable preview, Drawable thumbnail) {
+        mTitle = title;
+        mId = id;
+        mPreview = preview;
+        mThumbnail = thumbnail;
+    }
+
+    @Override
+    public String getTitle() {
+        return mTitle;
+    }
+
+    @Override
+    public void bindThumbnailTile(View view) {
+        ((ImageView) view.findViewById(R.id.clock_option_thumbnail)).setImageDrawable(mThumbnail);
+    }
+
+    @Override
+    public boolean isActive(Context context) {
+        String currentClock = Secure.getString(context.getContentResolver(),
+                Clockface.CLOCK_FACE_SETTING);
+        // Empty clock Id is the default system clock
+        return (TextUtils.isEmpty(currentClock) && TextUtils.isEmpty(mId))
+                || (mId != null && mId.equals(currentClock));
+    }
+
+    @Override
+    public int getLayoutResId() {
+        return R.layout.clock_option;
+    }
+
+    public Drawable getPreviewDrawable() {
+        return mPreview;
+    }
+
+    String getId() {
+        return mId;
+    }
+
+    public static class Builder {
+        private String mTitle;
+        private String mId;
+        private Drawable mPreview;
+        private Drawable mThumbnail;
+
+        public Clockface build() {
+            return new Clockface(mTitle, mId, mPreview, mThumbnail);
+        }
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setId(String id) {
+            mId = id;
+            return this;
+        }
+
+        public Builder setPreview(Drawable preview) {
+            mPreview = preview;
+            return this;
+        }
+
+        public Builder setThumbnail(Drawable thumbnail) {
+            mThumbnail = thumbnail;
+            return this;
+        }
+    }
+}
diff --git a/src/com/android/customization/model/clock/ResourcesApkClockProvider.java b/src/com/android/customization/model/clock/ResourcesApkClockProvider.java
new file mode 100644
index 0000000..1533db9
--- /dev/null
+++ b/src/com/android/customization/model/clock/ResourcesApkClockProvider.java
@@ -0,0 +1,76 @@
+/*
+ * 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.clock;
+
+import android.content.Context;
+import android.content.res.Resources.NotFoundException;
+import android.util.Log;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.ResourcesApkProvider;
+import com.android.customization.model.clock.Clockface.Builder;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ResourcesApkClockProvider extends ResourcesApkProvider implements ClockProvider {
+
+    private static final String TAG = "ResourcesApkClockProvider";
+
+    private static final String CLOCKS_ARRAY = "clocks";
+    private static final String TITLE_PREFIX = "clock_title_";
+    private static final String ID_PREFIX = "clock_id_";
+    private static final String PREVIEW_PREFIX = "clock_preview_";
+    private static final String THUMBNAIL_PREFIX = "clock_thumbnail_";
+
+    private List<Clockface> mClocks;
+
+    public ResourcesApkClockProvider(Context context){
+        super(context, context.getString(R.string.clocks_stub_package));
+    }
+
+    @Override
+    public void fetch(OptionsFetchedListener<Clockface> callback, boolean reload) {
+        if (mClocks == null || reload) {
+            mClocks = new ArrayList<>();
+            loadAll();
+        }
+
+        if(callback != null) {
+            callback.onOptionsLoaded(mClocks);
+        }
+    }
+
+    private void loadAll() {
+        String[] clockNames = getItemsFromStub(CLOCKS_ARRAY);
+
+        for (String clockName : clockNames) {
+            try {
+                Builder builder = new Builder();
+
+                builder.setTitle(getItemStringFromStub(TITLE_PREFIX, clockName))
+                        .setId(getItemStringFromStub(ID_PREFIX, clockName))
+                        .setPreview(getItemDrawableFromStub(PREVIEW_PREFIX, clockName))
+                        .setThumbnail(getItemDrawableFromStub(THUMBNAIL_PREFIX, clockName));
+
+                mClocks.add(builder.build());
+            } catch (NotFoundException e) {
+                Log.i(TAG, "Resource not found, skipping clock", e);
+            }
+        }
+    }
+}
diff --git a/src/com/android/customization/model/grid/GridOption.java b/src/com/android/customization/model/grid/GridOption.java
index 1bc34d0..879a9c1 100644
--- a/src/com/android/customization/model/grid/GridOption.java
+++ b/src/com/android/customization/model/grid/GridOption.java
@@ -67,7 +67,7 @@
     }
 
     @Override
-    public boolean isCurrentlySet() {
+    public boolean isActive(Context context) {
         return mIsCurrent;
     }
 
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
index 6d67071..33e4ee4 100644
--- a/src/com/android/customization/model/theme/DefaultThemeProvider.java
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -19,8 +19,6 @@
 import static com.android.customization.model.ResourceConstants.CONFIG_ICON_MASK;
 
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
@@ -30,6 +28,7 @@
 import android.util.Log;
 
 import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.ResourcesApkProvider;
 import com.android.customization.model.theme.ThemeBundle.Builder;
 import com.android.wallpaper.R;
 
@@ -39,7 +38,7 @@
 /**
  * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK.
  */
-public class DefaultThemeProvider implements ThemeBundleProvider {
+public class DefaultThemeProvider extends ResourcesApkProvider implements ThemeBundleProvider {
 
     private static final String TAG = "DefaultThemeProvider";
 
@@ -60,37 +59,10 @@
     private static final String CONFIG_BODY_FONT_FAMILY = "config_bodyFontFamily";
     private static final String CONFIG_HEADLINE_FONT_FAMILY = "config_headlineFontFamily";
 
-    private final Context mContext;
-    private final String mStubPackageName;
-    private Resources mStubApkResources;
     private List<ThemeBundle> mThemes;
 
     public DefaultThemeProvider(Context context) {
-        mContext = context;
-        mStubPackageName = mContext.getString(R.string.themes_stub_package);
-        init();
-    }
-
-    private void init() {
-        if (TextUtils.isEmpty(mStubPackageName)) {
-            return;
-        }
-        mStubApkResources = null;
-        try {
-            PackageManager pm = mContext.getPackageManager();
-            ApplicationInfo stubAppInfo = pm.getApplicationInfo(
-                    mStubPackageName, PackageManager.GET_META_DATA);
-            if (stubAppInfo != null) {
-                mStubApkResources = pm.getResourcesForApplication(stubAppInfo);
-            }
-        } catch (NameNotFoundException e) {
-            Log.w(TAG, String.format("Themes stub APK for %s not found.", mStubPackageName));
-        }
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return mStubApkResources != null;
+        super(context, context.getString(R.string.themes_stub_package));
     }
 
     @Override
@@ -108,9 +80,7 @@
     private void loadAll() {
         addDefaultTheme();
 
-        int themesListResId = mStubApkResources.getIdentifier(THEMES_ARRAY, "array",
-                mStubPackageName);
-        String[] themeNames = mStubApkResources.getStringArray(themesListResId);
+        String[] themeNames = getItemsFromStub(THEMES_ARRAY);
 
         for (String themeName : themeNames) {
             // Default theme needs special treatment (see #addDefaultTheme())
@@ -187,7 +157,6 @@
         }
     }
 
-
     /**
      * Default theme requires different treatment: if there are overlay packages specified in the
      * stub apk, we'll use those, otherwise we'll get the System default values. But we cannot skip
@@ -270,9 +239,7 @@
     }
 
     private String getOverlayPackage(String prefix, String themeName) {
-        int overlayPackageResId = mStubApkResources.getIdentifier(prefix + themeName,
-                "string", mStubPackageName);
-        return mStubApkResources.getString(overlayPackageResId);
+        return getItemStringFromStub(prefix, themeName);
     }
 
     private Typeface loadTypeface(String configName, String fontOverlayPackage)
diff --git a/src/com/android/customization/model/theme/ThemeBundle.java b/src/com/android/customization/model/theme/ThemeBundle.java
index cf6055c..15b9efc 100644
--- a/src/com/android/customization/model/theme/ThemeBundle.java
+++ b/src/com/android/customization/model/theme/ThemeBundle.java
@@ -15,9 +15,9 @@
  */
 package com.android.customization.model.theme;
 
+import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
-import android.graphics.PorterDuff.Mode;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.ShapeDrawable;
@@ -83,7 +83,7 @@
     }
 
     @Override
-    public boolean isCurrentlySet() {
+    public boolean isActive(Context context) {
         return false;
     }
 
diff --git a/src/com/android/customization/picker/BasePreviewAdapter.java b/src/com/android/customization/picker/BasePreviewAdapter.java
new file mode 100644
index 0000000..e87fb71
--- /dev/null
+++ b/src/com/android/customization/picker/BasePreviewAdapter.java
@@ -0,0 +1,112 @@
+/*
+ * 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.picker;
+
+import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.cardview.widget.CardView;
+import androidx.core.view.ViewCompat;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.customization.picker.BasePreviewAdapter.PreviewPage;
+import com.android.customization.widget.PreviewPager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base adapter for {@link PreviewPager}.
+ * It can be used as-is by creating an extension of {@link PreviewPage} and adding pages via
+ * {@link #addPage(PreviewPage)}
+ * @param <T> the type of {@link PreviewPage} that this adapter will handle.
+ */
+public class BasePreviewAdapter<T extends PreviewPage> extends PagerAdapter {
+
+    private final int mPreviewCardResId;
+    private final Context mContext;
+    private final LayoutInflater mInflater;
+    protected final List<T> mPages = new ArrayList<>();
+
+    protected BasePreviewAdapter(Context context, @LayoutRes int previewCardResId) {
+        mContext = context;
+        mPreviewCardResId = previewCardResId;
+        mInflater = LayoutInflater.from(mContext);
+    }
+
+    protected void addPage(T p) {
+        mPages.add(p);
+    }
+
+    @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;
+        }
+        CardView card = (CardView) mInflater.inflate(mPreviewCardResId, container, false);
+        T 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) {
+        ((T) object).card = null;
+    }
+
+    /**
+     * Represents a possible page in a {@link PreviewPager}, based on a CardView container.
+     * Override {@link #bindPreviewContent()} to bind the contents of the page when instantiated.
+     */
+    public static abstract class PreviewPage {
+        protected final String title;
+        protected CardView card;
+
+        protected PreviewPage(String title) {
+            this.title = title;
+        }
+
+        public void setCard(CardView card) {
+            this.card = card;
+        }
+
+        public abstract void bindPreviewContent();
+    }
+}
diff --git a/src/com/android/customization/picker/CustomizationPickerActivity.java b/src/com/android/customization/picker/CustomizationPickerActivity.java
index f0db8a1..ab60166 100644
--- a/src/com/android/customization/picker/CustomizationPickerActivity.java
+++ b/src/com/android/customization/picker/CustomizationPickerActivity.java
@@ -30,12 +30,16 @@
 
 import com.android.customization.model.CustomizationManager;
 import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.clock.ClockManager;
+import com.android.customization.model.clock.Clockface;
+import com.android.customization.model.clock.ResourcesApkClockProvider;
 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.clock.ClockFragment;
 import com.android.customization.picker.grid.GridFragment;
 import com.android.customization.picker.theme.ThemeFragment;
 import com.android.wallpaper.R;
@@ -117,7 +121,11 @@
         if (themeManager.isAvailable()) {
             mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme, themeManager));
         }
-        // TODO: clock
+        //Clock
+        ClockManager clockManager = new ClockManager(this, new ResourcesApkClockProvider(this));
+        if (clockManager.isAvailable()) {
+            mSections.put(R.id.nav_clock, new ClockSection(R.id.nav_clock, clockManager));
+        }
         //Grid
         GridOptionsManager gridManager = new GridOptionsManager(
                 new LauncherGridOptionsProvider(this));
@@ -292,4 +300,22 @@
             return mFragment;
         }
     }
+
+    private class ClockSection extends CustomizationSection<Clockface> {
+
+        private ClockFragment mFragment;
+
+        private ClockSection(int id, ClockManager manager) {
+            super(id, manager);
+        }
+
+        @Override
+        Fragment getFragment() {
+            if (mFragment == null) {
+                mFragment = ClockFragment.newInstance(getString(R.string.clock_title),
+                        (ClockManager) mCustomizationManager);
+            }
+            return mFragment;
+        }
+    }
 }
diff --git a/src/com/android/customization/picker/clock/ClockFragment.java b/src/com/android/customization/picker/clock/ClockFragment.java
new file mode 100644
index 0000000..3436c22
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ClockFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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.picker.clock;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.customization.model.clock.ClockManager;
+import com.android.customization.model.clock.Clockface;
+import com.android.customization.picker.BasePreviewAdapter;
+import com.android.customization.picker.BasePreviewAdapter.PreviewPage;
+import com.android.customization.widget.OptionSelectorController;
+import com.android.customization.widget.PreviewPager;
+import com.android.wallpaper.R;
+import com.android.wallpaper.picker.ToolbarFragment;
+
+public class ClockFragment extends ToolbarFragment {
+
+    public static ClockFragment newInstance(CharSequence title, ClockManager manager) {
+        ClockFragment fragment = new ClockFragment();
+        fragment.setManager(manager);
+        fragment.setArguments(ToolbarFragment.createArguments(title));
+        return fragment;
+    }
+
+    private RecyclerView mOptionsContainer;
+    private OptionSelectorController mOptionsController;
+    private Clockface mSelectedOption;
+    private ClockManager mClockManager;
+    private PreviewPager mPreviewPager;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(
+                R.layout.fragment_clock_picker, container, /* attachToRoot */ false);
+        setUpToolbar(view);
+        mPreviewPager = view.findViewById(R.id.clock_preview_pager);
+        mOptionsContainer = view.findViewById(R.id.options_container);
+        setUpOptions();
+        view.findViewById(R.id.apply_button).setOnClickListener(v -> {
+            mClockManager.apply(mSelectedOption);
+            getActivity().finish();
+        });
+        return view;
+    }
+
+    private void setManager(ClockManager manager) {
+        mClockManager = manager;
+    }
+
+    private void createAdapter() {
+        mPreviewPager.setAdapter(new ClockPreviewAdapter(getContext(), mSelectedOption));
+    }
+
+    private void setUpOptions() {
+        mClockManager.fetchOptions(options -> {
+            mOptionsController = new OptionSelectorController(mOptionsContainer, options);
+
+            mOptionsController.addListener(selected -> {
+                mSelectedOption = (Clockface) selected;
+                createAdapter();
+            });
+            mOptionsController.initOptions();
+            for (Clockface option : options) {
+                if (option.isActive(getContext())) {
+                    mSelectedOption = option;
+                }
+            }
+            // For development only, as there should always be a grid set.
+            if (mSelectedOption == null) {
+                mSelectedOption = options.get(0);
+            }
+            createAdapter();
+        });
+    }
+
+    private static class ClockfacePreviewPage extends PreviewPage {
+
+        private final Drawable mPreview;
+
+        public ClockfacePreviewPage(String title, Drawable previewDrawable) {
+            super(title);
+            mPreview = previewDrawable;
+        }
+
+        @Override
+        public void bindPreviewContent() {
+            ((ImageView) card.findViewById(R.id.clock_preview_image))
+                    .setImageDrawable(mPreview);
+        }
+    }
+
+    /**
+     * 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 static class ClockPreviewAdapter extends BasePreviewAdapter<ClockfacePreviewPage> {
+        ClockPreviewAdapter(Context context, Clockface clockface) {
+            super(context, R.layout.clock_preview_card);
+            addPage(new ClockfacePreviewPage(clockface.getTitle(), clockface.getPreviewDrawable()));
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/grid/GridFragment.java b/src/com/android/customization/picker/grid/GridFragment.java
index 5864c8e..6733cb5 100644
--- a/src/com/android/customization/picker/grid/GridFragment.java
+++ b/src/com/android/customization/picker/grid/GridFragment.java
@@ -15,9 +15,7 @@
  */
 package com.android.customization.picker.grid;
 
-import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
-
-import android.content.Context;
+import android.app.Activity;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -26,13 +24,12 @@
 
 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.picker.BasePreviewAdapter;
+import com.android.customization.picker.BasePreviewAdapter.PreviewPage;
 import com.android.customization.widget.OptionSelectorController;
 import com.android.customization.widget.PreviewPager;
 import com.android.wallpaper.R;
@@ -42,9 +39,6 @@
 
 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.
  */
@@ -95,7 +89,7 @@
             });
             mOptionsController.initOptions();
             for (GridOption option : options) {
-                if (option.isCurrentlySet()) {
+                if (option.isActive(getContext())) {
                     mSelectedOption = option;
                 }
             }
@@ -107,83 +101,42 @@
         });
     }
 
+    private static class GridPreviewPage extends PreviewPage {
+        private final int mPageId;
+        private final Asset mPreviewAsset;
+        private final int mCols;
+        private final int mRows;
+        private final Activity mActivity;
+
+        private GridPreviewPage(Activity activity, int id, Uri previewUri, int rows, int cols) {
+            super(null);
+            mPageId = id;
+            mPreviewAsset = new ContentUriAsset(activity, previewUri,
+                    RequestOptions.fitCenterTransform());
+            mRows = rows;
+            mCols = cols;
+            mActivity = activity;
+        }
+
+        public void bindPreviewContent() {
+            mPreviewAsset.loadDrawable(mActivity, card.findViewById(R.id.grid_preview_image),
+                    card.getContext().getResources().getColor(R.color.primary_color, null));
+        }
+    }
     /**
      * 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<>();
+    class GridPreviewAdapter extends BasePreviewAdapter<GridPreviewPage> {
 
         GridPreviewAdapter(GridOption gridOption) {
+            super(getContext(), R.layout.grid_preview_card);
             for (int i = 0; i < gridOption.previewPagesCount; i++) {
-                mPages.add(new PreviewPage(getContext(), i,
+                addPage(new GridPreviewPage(getActivity(), 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 179c08f..aad4542 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -15,8 +15,7 @@
  */
 package com.android.customization.picker.theme;
 
-import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
-
+import android.content.Context;
 import android.content.res.ColorStateList;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -30,21 +29,18 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
-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.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.picker.BasePreviewAdapter;
+import com.android.customization.picker.BasePreviewAdapter.PreviewPage;
 import com.android.customization.widget.OptionSelectorController;
 import com.android.customization.widget.PreviewPager;
 import com.android.wallpaper.R;
 import com.android.wallpaper.picker.ToolbarFragment;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * Fragment that contains the main UI for selecting and applying a ThemeBundle.
  */
@@ -83,7 +79,7 @@
     }
 
     private void createAdapter() {
-        mAdapter = new ThemePreviewAdapter(mSelectedTheme);
+        mAdapter = new ThemePreviewAdapter(getContext(), mSelectedTheme);
         mPreviewPager.setAdapter(mAdapter);
     }
 
@@ -97,7 +93,7 @@
             });
             mOptionsController.initOptions();
             for (ThemeBundle theme : options) {
-                if (theme.isCurrentlySet()) {
+                if (theme.isActive(getContext())) {
                     mSelectedTheme = theme;
                 }
             }
@@ -110,43 +106,51 @@
         createAdapter();
     }
 
+    private static abstract class ThemePreviewPage extends PreviewPage {
+        @StringRes final int nameResId;
+        @DrawableRes final int iconSrc;
+        @LayoutRes final int contentLayoutRes;
+        @ColorInt final int accentColor;
+        private final LayoutInflater inflater;
+
+        private ThemePreviewPage(Context context, @StringRes int titleResId, @DrawableRes int iconSrc,
+                @LayoutRes int contentLayoutRes, @ColorInt int accentColor) {
+            super(null);
+            this.nameResId = titleResId;
+            this.iconSrc = iconSrc;
+            this.contentLayoutRes = contentLayoutRes;
+            this.accentColor = accentColor;
+            this.inflater = LayoutInflater.from(context);
+        }
+
+        @Override
+        public void bindPreviewContent() {
+            TextView header = card.findViewById(R.id.theme_preview_card_header);
+            header.setText(nameResId);
+            header.setCompoundDrawablesWithIntrinsicBounds(0, iconSrc, 0, 0);
+            header.setCompoundDrawableTintList(ColorStateList.valueOf(accentColor));
+
+            ViewGroup body = card.findViewById(R.id.theme_preview_card_body_container);
+            inflater.inflate(contentLayoutRes, body, true);
+            bindBody();
+        }
+
+        protected abstract void bindBody();
+    }
+
     /**
      * 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 ThemePreviewAdapter extends PagerAdapter {
+    private static class ThemePreviewAdapter extends BasePreviewAdapter<ThemePreviewPage> {
 
-        private abstract class PreviewPage {
-            @StringRes final int nameResId;
-            @DrawableRes final int iconSrc;
-            @LayoutRes final int contentLayoutRes;
-            @ColorInt final int accentColor;
-
-            CardView card;
-
-            private PreviewPage(@StringRes int titleResId, @DrawableRes int iconSrc,
-                    @LayoutRes int contentLayoutRes, @ColorInt int accentColor) {
-                this.nameResId = titleResId;
-                this.iconSrc = iconSrc;
-                this.contentLayoutRes = contentLayoutRes;
-                this.accentColor = accentColor;
-            }
-
-            public void setCard(CardView card) {
-                this.card = card;
-            }
-
-            abstract void bindPreviewContent();
-        }
-
-        List<PreviewPage> mPages = new ArrayList<>();
-
-        ThemePreviewAdapter(ThemeBundle theme) {
-            mPages.add(new PreviewPage(R.string.preview_name_font, R.drawable.ic_font,
+        ThemePreviewAdapter(Context context, ThemeBundle theme) {
+            super(context, R.layout.theme_preview_card);
+            addPage(new ThemePreviewPage(context, R.string.preview_name_font, R.drawable.ic_font,
                     R.layout.preview_card_font_content, theme.getPreviewInfo().colorAccentLight) {
                 @Override
-                void bindPreviewContent() {
+                protected void bindBody() {
                     TextView title = card.findViewById(R.id.font_card_title);
                     title.setTypeface(theme.getPreviewInfo().headlineFontFamily);
                     TextView body = card.findViewById(R.id.font_card_body);
@@ -154,49 +158,5 @@
                 }
             });
         }
-
-        @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.theme_preview_card,
-                    container, false);
-            PreviewPage page = mPages.get(position);
-
-            TextView header = card.findViewById(R.id.theme_preview_card_header);
-            header.setText(page.nameResId);
-            header.setCompoundDrawablesWithIntrinsicBounds(0, page.iconSrc, 0, 0);
-            header.setCompoundDrawableTintList(ColorStateList.valueOf(page.accentColor));
-
-            ViewGroup body = card.findViewById(R.id.theme_preview_card_body_container);
-            inflater.inflate(page.contentLayoutRes, body, true);
-
-            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/widget/OptionSelectorController.java b/src/com/android/customization/widget/OptionSelectorController.java
index dc14cfc..8c17cad 100644
--- a/src/com/android/customization/widget/OptionSelectorController.java
+++ b/src/com/android/customization/widget/OptionSelectorController.java
@@ -100,7 +100,7 @@
             @Override
             public void onBindViewHolder(@NonNull TileViewHolder holder, int position) {
                 CustomizationOption option = mOptions.get(position);
-                if (mSelectedOption == null && option.isCurrentlySet()) {
+                if (mSelectedOption == null && option.isActive(mContainer.getContext())) {
                     mSelectedOption = option;
                 }
                 if (holder.labelView != null) {