Improve error handling when loading customizations

Add a loading indicator and an error message if there's
an issue retrieving content for any of the customization
tabs.

Bug: 133326909
Change-Id: I2da6309f5c2b369caffeec4e21c3aa856ddb4e4a
diff --git a/res/layout/fragment_clock_picker.xml b/res/layout/fragment_clock_picker.xml
index 630d37a..1528666 100644
--- a/res/layout/fragment_clock_picker.xml
+++ b/res/layout/fragment_clock_picker.xml
@@ -23,60 +23,89 @@
     android:background="?android:colorPrimary">
     <include layout="@layout/section_header"/>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
-        <com.android.customization.widget.PreviewPager
-            android:id="@+id/clock_preview_pager"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/content_section"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
-            android:background="@color/secondary_color"
-            app:layout_constrainedHeight="true"
-            app:layout_constraintBottom_toTopOf="@id/options_container"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
-            app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
-            app:layout_constraintVertical_bias="0.0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintVertical_chainStyle="spread_inside"/>
+            android:layout_height="match_parent">
 
-        <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"
-            android:layout_marginTop="10dp"
-            app:layout_constraintBottom_toTopOf="@id/placeholder"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/clock_preview_pager"
-            app:layout_constraintVertical_bias="1.0"/>
+            <com.android.customization.widget.PreviewPager
+                android:id="@+id/clock_preview_pager"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/secondary_color"
+                app:layout_constrainedHeight="true"
+                app:layout_constraintBottom_toTopOf="@id/options_container"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+                app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
+                app:layout_constraintVertical_bias="0.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintVertical_chainStyle="spread_inside"/>
 
-        <Space
-            android:id="@+id/placeholder"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/min_taptarget_height"
-            app:layout_constraintBottom_toTopOf="@id/apply_button"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/options_container"
-            app:layout_constraintVertical_bias="1.0"/>
+            <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"
+                android:layout_marginTop="10dp"
+                app:layout_constraintBottom_toTopOf="@id/placeholder"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/clock_preview_pager"
+                app:layout_constraintVertical_bias="1.0"/>
 
-        <Button
-            android:id="@+id/apply_button"
-            style="@style/ActionPrimaryButton"
+            <Space
+                android:id="@+id/placeholder"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/min_taptarget_height"
+                app:layout_constraintBottom_toTopOf="@id/apply_button"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_min="@dimen/min_taptarget_height"
+                app:layout_constraintHorizontal_bias="0.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/options_container"
+                app:layout_constraintVertical_bias="1.0"/>
+
+            <Button
+                android:id="@+id/apply_button"
+                style="@style/ActionPrimaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:layout_marginEnd="10dp"
+                android:layout_marginVertical="10dp"
+                android:layout_weight="1"
+                android:text="@string/apply_theme_btn"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.core.widget.ContentLoadingProgressBar
+            android:id="@+id/loading_indicator"
+            style="@android:style/Widget.DeviceDefault.ProgressBar"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="end"
-            android:layout_marginEnd="10dp"
-            android:layout_marginVertical="10dp"
-            android:layout_weight="1"
-            android:text="@string/apply_theme_btn"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/placeholder"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+            android:layout_marginTop="200dp"
+            android:layout_gravity="center_horizontal|top"
+            android:indeterminate="true"/>
+        <FrameLayout
+            android:id="@+id/error_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <TextView
+                android:id="@+id/error_message"
+                style="@style/HeaderTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:text="@string/something_went_wrong"/>
+        </FrameLayout>
+    </FrameLayout>
 </LinearLayout>
diff --git a/res/layout/fragment_grid_picker.xml b/res/layout/fragment_grid_picker.xml
index d55b5c9..467a620 100644
--- a/res/layout/fragment_grid_picker.xml
+++ b/res/layout/fragment_grid_picker.xml
@@ -23,61 +23,88 @@
     android:background="?android:colorPrimary">
     <include layout="@layout/section_header"/>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
-        <com.android.customization.widget.PreviewPager
-            android:id="@+id/grid_preview_pager"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/content_section"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
-            android:background="@color/secondary_color"
-            app:card_style="screen_aspect_ratio"
-            app:layout_constrainedHeight="true"
-            app:layout_constraintBottom_toTopOf="@id/options_container"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
-            app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
-            app:layout_constraintVertical_bias="0.0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintVertical_chainStyle="spread_inside"/>
+            android:layout_height="match_parent">
 
-        <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"
-            android:layout_marginTop="10dp"
-            app:layout_constraintBottom_toTopOf="@id/placeholder"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/grid_preview_pager"
-            app:layout_constraintVertical_bias="1.0"/>
+            <com.android.customization.widget.PreviewPager
+                android:id="@+id/grid_preview_pager"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/secondary_color"
+                app:card_style="screen_aspect_ratio"
+                app:layout_constrainedHeight="true"
+                app:layout_constraintBottom_toTopOf="@id/options_container"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+                app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
+                app:layout_constraintVertical_bias="0.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintVertical_chainStyle="spread_inside"/>
 
-        <Space
-            android:id="@+id/placeholder"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/min_taptarget_height"
-            app:layout_constraintBottom_toTopOf="@id/apply_button"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/options_container"
-            app:layout_constraintVertical_bias="1.0"/>
+            <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"
+                android:layout_marginTop="10dp"
+                app:layout_constraintBottom_toTopOf="@id/placeholder"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/grid_preview_pager"
+                app:layout_constraintVertical_bias="1.0"/>
 
-        <Button
-            android:id="@+id/apply_button"
-            style="@style/ActionPrimaryButton"
+            <Space
+                android:id="@+id/placeholder"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/min_taptarget_height"
+                app:layout_constraintBottom_toTopOf="@id/apply_button"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/options_container"
+                app:layout_constraintVertical_bias="1.0"/>
+
+            <Button
+                android:id="@+id/apply_button"
+                style="@style/ActionPrimaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:layout_marginEnd="10dp"
+                android:layout_marginVertical="10dp"
+                android:layout_weight="1"
+                android:text="@string/apply_theme_btn"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.core.widget.ContentLoadingProgressBar
+            android:id="@+id/loading_indicator"
+            style="@android:style/Widget.DeviceDefault.ProgressBar"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="end"
-            android:layout_marginEnd="10dp"
-            android:layout_marginVertical="10dp"
-            android:layout_weight="1"
-            android:text="@string/apply_theme_btn"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/placeholder"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+            android:layout_marginTop="200dp"
+            android:layout_gravity="center_horizontal|top"
+            android:indeterminate="true"/>
+        <FrameLayout
+            android:id="@+id/error_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <TextView
+                android:id="@+id/error_message"
+                style="@style/HeaderTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:text="@string/something_went_wrong"/>
+        </FrameLayout>
+    </FrameLayout>
 </LinearLayout>
diff --git a/res/layout/fragment_theme_picker.xml b/res/layout/fragment_theme_picker.xml
index 84c5516..dbb633e 100644
--- a/res/layout/fragment_theme_picker.xml
+++ b/res/layout/fragment_theme_picker.xml
@@ -23,66 +23,95 @@
     android:background="?android:colorPrimary">
     <include layout="@layout/section_header"/>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
-        <com.android.customization.widget.PreviewPager
-            android:id="@+id/theme_preview_pager"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/content_section"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="@color/secondary_color"
-            app:layout_constrainedHeight="true"
-            app:layout_constraintBottom_toTopOf="@id/options_container"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
-            app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintVertical_bias="0.0"
-            app:layout_constraintVertical_chainStyle="spread_inside"/>
+            android:layout_height="match_parent">
 
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/options_container"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/options_container_height"
-            android:layout_gravity="bottom|center_horizontal"
-            android:layout_marginTop="10dp"
-            android:layout_weight="1"
-            app:layout_constraintBottom_toTopOf="@id/use_my_wallpaper"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/theme_preview_pager"
-            app:layout_constraintVertical_bias="1.0"/>
+            <com.android.customization.widget.PreviewPager
+                android:id="@+id/theme_preview_pager"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/secondary_color"
+                app:layout_constrainedHeight="true"
+                app:layout_constraintBottom_toTopOf="@id/options_container"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+                app:layout_constraintHeight_min="@dimen/preview_pager_min_height"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintVertical_bias="0.0"
+                app:layout_constraintVertical_chainStyle="spread_inside"/>
 
-        <CheckBox
-            android:id="@+id/use_my_wallpaper"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/min_taptarget_height"
-            android:layout_marginStart="10dp"
-            android:ellipsize="end"
-            android:gravity="start|center_vertical"
-            android:paddingLeft="4dp"
-            android:text="@string/keep_my_wallpaper"
-            app:layout_constraintBottom_toTopOf="@id/apply_button"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHeight_min="@dimen/min_taptarget_height"
-            app:layout_constraintHorizontal_bias="0.0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/options_container"
-            app:layout_constraintVertical_bias="1.0"/>
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/options_container"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/options_container_height"
+                android:layout_gravity="bottom|center_horizontal"
+                android:layout_marginTop="10dp"
+                android:layout_weight="1"
+                app:layout_constraintBottom_toTopOf="@id/use_my_wallpaper"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/theme_preview_pager"
+                app:layout_constraintVertical_bias="1.0"/>
 
-        <Button
-            android:id="@+id/apply_button"
-            style="@style/ActionPrimaryButton"
+            <CheckBox
+                android:id="@+id/use_my_wallpaper"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/min_taptarget_height"
+                android:layout_marginStart="10dp"
+                android:ellipsize="end"
+                android:gravity="start|center_vertical"
+                android:paddingLeft="4dp"
+                android:text="@string/keep_my_wallpaper"
+                app:layout_constraintBottom_toTopOf="@id/apply_button"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_min="@dimen/min_taptarget_height"
+                app:layout_constraintHorizontal_bias="0.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/options_container"
+                app:layout_constraintVertical_bias="1.0"/>
+
+            <Button
+                android:id="@+id/apply_button"
+                style="@style/ActionPrimaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:layout_marginEnd="10dp"
+                android:layout_marginBottom="10dp"
+                android:text="@string/apply_theme_btn"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.core.widget.ContentLoadingProgressBar
+            android:id="@+id/loading_indicator"
+            style="@android:style/Widget.DeviceDefault.ProgressBar"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="end"
-            android:layout_marginEnd="10dp"
-            android:layout_marginBottom="10dp"
-            android:text="@string/apply_theme_btn"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
+            android:layout_marginTop="200dp"
+            android:layout_gravity="center_horizontal|top"
+            android:indeterminate="true"/>
 
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        <FrameLayout
+            android:id="@+id/error_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <TextView
+                android:id="@+id/error_message"
+                style="@style/HeaderTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:text="@string/something_went_wrong"/>
+        </FrameLayout>
+    </FrameLayout>
 </LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 46de146..f571b6b 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -187,4 +187,7 @@
 
     <!-- Content description for a screen showing the preview of a clock face. [CHAR_LIMIT=NONE] -->
     <string name="clock_preview_content_description"><xliff:g name="clock_name">%1$s</xliff:g> clock preview</string>
+
+    <!-- Generic error message [CHAR_LIMIT=NONE] -->
+    <string name="something_went_wrong">Oops! Something went wrong.</string>
 </resources>
diff --git a/src/com/android/customization/model/CustomizationManager.java b/src/com/android/customization/model/CustomizationManager.java
index 3296ca1..7b9f463 100644
--- a/src/com/android/customization/model/CustomizationManager.java
+++ b/src/com/android/customization/model/CustomizationManager.java
@@ -15,6 +15,9 @@
  */
 package com.android.customization.model;
 
+import android.util.Log;
+import android.widget.Toast;
+
 import androidx.annotation.Nullable;
 
 import java.util.List;
@@ -49,6 +52,15 @@
          * Called when the options have been retrieved.
          */
         void onOptionsLoaded(List<T> options);
+
+        /**
+         * Called if there was an error loading grid options
+         */
+        default void onError(@Nullable Throwable throwable) {
+            if (throwable != null) {
+                Log.e("OptionsFecthedListener", "Error loading options", throwable);
+            }
+        }
     }
 
     /**
diff --git a/src/com/android/customization/model/grid/GridOptionsManager.java b/src/com/android/customization/model/grid/GridOptionsManager.java
index ba5968e..1599dde 100644
--- a/src/com/android/customization/model/grid/GridOptionsManager.java
+++ b/src/com/android/customization/model/grid/GridOptionsManager.java
@@ -78,8 +78,19 @@
         @Override
         protected void onPostExecute(List<GridOption> gridOptions) {
             if (mCallback != null) {
-                mCallback.onOptionsLoaded(gridOptions != null ? gridOptions
-                        : Collections.emptyList());
+                if (gridOptions != null && !gridOptions.isEmpty()) {
+                    mCallback.onOptionsLoaded(gridOptions);
+                } else {
+                    mCallback.onError(null);
+                }
+            }
+        }
+
+        @Override
+        protected void onCancelled() {
+            super.onCancelled();
+            if (mCallback != null) {
+                mCallback.onError(null);
             }
         }
     }
diff --git a/src/com/android/customization/picker/clock/ClockFragment.java b/src/com/android/customization/picker/clock/ClockFragment.java
index c3e1c97..14dbc16 100644
--- a/src/com/android/customization/picker/clock/ClockFragment.java
+++ b/src/com/android/customization/picker/clock/ClockFragment.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -26,9 +27,11 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.widget.ContentLoadingProgressBar;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
 import com.android.customization.model.clock.BaseClockManager;
 import com.android.customization.model.clock.Clockface;
 import com.android.customization.module.ThemesUserEventLogger;
@@ -41,11 +44,15 @@
 import com.android.wallpaper.module.InjectorProvider;
 import com.android.wallpaper.picker.ToolbarFragment;
 
+import java.util.List;
+
 /**
  * Fragment that contains the main UI for selecting and applying a Clockface.
  */
 public class ClockFragment extends ToolbarFragment {
 
+    private static final String TAG = "ClockFragment";
+
     /**
      * Interface to be implemented by an Activity hosting a {@link ClockFragment}
      */
@@ -64,6 +71,9 @@
     private Clockface mSelectedOption;
     private BaseClockManager mClockManager;
     private PreviewPager mPreviewPager;
+    private ContentLoadingProgressBar mLoading;
+    private View mContent;
+    private View mError;
     private ThemesUserEventLogger mEventLogger;
 
     @Override
@@ -81,8 +91,11 @@
         View view = inflater.inflate(
                 R.layout.fragment_clock_picker, container, /* attachToRoot */ false);
         setUpToolbar(view);
+        mContent = view.findViewById(R.id.content_section);
         mPreviewPager = view.findViewById(R.id.clock_preview_pager);
         mOptionsContainer = view.findViewById(R.id.options_container);
+        mLoading = view.findViewById(R.id.loading_indicator);
+        mError = view.findViewById(R.id.error_section);
         setUpOptions();
         view.findViewById(R.id.apply_button).setOnClickListener(v -> {
             mClockManager.apply(mSelectedOption, new Callback() {
@@ -95,6 +108,9 @@
 
                 @Override
                 public void onError(@Nullable Throwable throwable) {
+                    if (throwable != null) {
+                        Log.e(TAG, "Error loading clockfaces", throwable);
+                    }
                     //TODO(santie): handle
                 }
             });
@@ -108,26 +124,50 @@
     }
 
     private void setUpOptions() {
-        mClockManager.fetchOptions(options -> {
-            mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
+        hideError();
+        mLoading.show();
+        mClockManager.fetchOptions(new OptionsFetchedListener<Clockface>() {
+           @Override
+           public void onOptionsLoaded(List<Clockface> options) {
+               mLoading.hide();
+               mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
 
-            mOptionsController.addListener(selected -> {
-                mSelectedOption = (Clockface) selected;
-                mEventLogger.logClockSelected(mSelectedOption);
-                createAdapter();
-            });
-            mOptionsController.initOptions(mClockManager);
-            for (Clockface option : options) {
-                if (option.isActive(mClockManager)) {
-                    mSelectedOption = option;
+               mOptionsController.addListener(selected -> {
+                   mSelectedOption = (Clockface) selected;
+                   mEventLogger.logClockSelected(mSelectedOption);
+                   createAdapter();
+               });
+               mOptionsController.initOptions(mClockManager);
+               for (Clockface option : options) {
+                   if (option.isActive(mClockManager)) {
+                       mSelectedOption = option;
+                   }
+               }
+               // For development only, as there should always be a grid set.
+               if (mSelectedOption == null) {
+                   mSelectedOption = options.get(0);
+               }
+               createAdapter();
+           }
+           @Override
+            public void onError(@Nullable Throwable throwable) {
+                if (throwable != null) {
+                   Log.e(TAG, "Error loading clockfaces", throwable);
                 }
+                showError();
             }
-            // For development only, as there should always be a grid set.
-            if (mSelectedOption == null) {
-                mSelectedOption = options.get(0);
-            }
-            createAdapter();
-        }, false);
+       }, false);
+    }
+
+    private void hideError() {
+        mContent.setVisibility(View.VISIBLE);
+        mError.setVisibility(View.GONE);
+    }
+
+    private void showError() {
+        mLoading.hide();
+        mContent.setVisibility(View.GONE);
+        mError.setVisibility(View.VISIBLE);
     }
 
     private static class ClockfacePreviewPage extends PreviewPage {
diff --git a/src/com/android/customization/picker/grid/GridFragment.java b/src/com/android/customization/picker/grid/GridFragment.java
index d50c7a8..3c395a1 100644
--- a/src/com/android/customization/picker/grid/GridFragment.java
+++ b/src/com/android/customization/picker/grid/GridFragment.java
@@ -22,6 +22,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
@@ -31,9 +32,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.cardview.widget.CardView;
+import androidx.core.widget.ContentLoadingProgressBar;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
 import com.android.customization.model.grid.GridOption;
 import com.android.customization.model.grid.GridOptionsManager;
 import com.android.customization.module.ThemesUserEventLogger;
@@ -51,6 +54,8 @@
 
 import com.bumptech.glide.request.RequestOptions;
 
+import java.util.List;
+
 /**
  * Fragment that contains the UI for selecting and applying a GridOption.
  */
@@ -58,6 +63,8 @@
 
     private static final int PREVIEW_FADE_DURATION_MS = 100;
 
+    private static final String TAG = "GridFragment";
+
     /**
      * Interface to be implemented by an Activity hosting a {@link GridFragment}
      */
@@ -82,6 +89,9 @@
     private GridOptionsManager mGridManager;
     private GridOption mSelectedOption;
     private PreviewPager mPreviewPager;
+    private ContentLoadingProgressBar mLoading;
+    private View mContent;
+    private View mError;
     private ThemesUserEventLogger mEventLogger;
 
     @Override
@@ -99,8 +109,11 @@
         View view = inflater.inflate(
                 R.layout.fragment_grid_picker, container, /* attachToRoot */ false);
         setUpToolbar(view);
+        mContent = view.findViewById(R.id.content_section);
         mPreviewPager = view.findViewById(R.id.grid_preview_pager);
         mOptionsContainer = view.findViewById(R.id.options_container);
+        mLoading = view.findViewById(R.id.loading_indicator);
+        mError = view.findViewById(R.id.error_section);
         final Resources res = getResources();
         DisplayMetrics dm = res.getDisplayMetrics();
         mScreenAspectRatio = (float) dm.heightPixels / dm.widthPixels;
@@ -161,28 +174,53 @@
     }
 
     private void setUpOptions() {
-        mGridManager.fetchOptions(options -> {
-            mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
+        hideError();
+        mLoading.show();
+        mGridManager.fetchOptions(new OptionsFetchedListener<GridOption>() {
+            @Override
+            public void onOptionsLoaded(List<GridOption> options) {
+                mLoading.hide();
+                mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
 
-            mOptionsController.addListener(selected -> {
-                mSelectedOption = (GridOption) selected;
-                mEventLogger.logGridSelected(mSelectedOption);
-                createAdapter();
-            });
-            mOptionsController.initOptions(mGridManager);
-            for (GridOption option : options) {
-                if (option.isActive(mGridManager)) {
-                    mSelectedOption = option;
+                mOptionsController.addListener(selected -> {
+                    mSelectedOption = (GridOption) selected;
+                    mEventLogger.logGridSelected(mSelectedOption);
+                    createAdapter();
+                });
+                mOptionsController.initOptions(mGridManager);
+                for (GridOption option : options) {
+                    if (option.isActive(mGridManager)) {
+                        mSelectedOption = option;
+                    }
                 }
+                // For development only, as there should always be a grid set.
+                if (mSelectedOption == null) {
+                    mSelectedOption = options.get(0);
+                }
+                createAdapter();
             }
-            // For development only, as there should always be a grid set.
-            if (mSelectedOption == null) {
-                mSelectedOption = options.get(0);
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                if (throwable != null) {
+                    Log.e(TAG, "Error loading grid options", throwable);
+                }
+                showError();
             }
-            createAdapter();
         }, false);
     }
 
+    private void hideError() {
+        mContent.setVisibility(View.VISIBLE);
+        mError.setVisibility(View.GONE);
+    }
+
+    private void showError() {
+        mLoading.hide();
+        mContent.setVisibility(View.GONE);
+        mError.setVisibility(View.VISIBLE);
+    }
+
     private class GridPreviewPage extends PreviewPage {
         private final int mPageId;
         private final Asset mPreviewAsset;
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
index 227dae5..bec3cf8 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -43,9 +43,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.widget.ContentLoadingProgressBar;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.grid.GridOption;
 import com.android.customization.model.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeBundle.PreviewInfo;
 import com.android.customization.model.theme.ThemeManager;
@@ -94,6 +97,9 @@
     private ThemeBundle mSelectedTheme;
     private ThemePreviewAdapter mAdapter;
     private PreviewPager mPreviewPager;
+    private ContentLoadingProgressBar mLoading;
+    private View mContent;
+    private View mError;
     private boolean mUseMyWallpaper;
     private WallpaperInfo mCurrentHomeWallpaper;
     private CurrentWallpaperInfoFactory mCurrentWallpaperFactory;
@@ -115,6 +121,9 @@
                 R.layout.fragment_theme_picker, container, /* attachToRoot */ false);
         setUpToolbar(view);
 
+        mContent = view.findViewById(R.id.content_section);
+        mLoading = view.findViewById(R.id.loading_indicator);
+        mError = view.findViewById(R.id.error_section);
         mCurrentWallpaperFactory = InjectorProvider.getInjector()
                 .getCurrentWallpaperFactory(getActivity().getApplicationContext());
         mPreviewPager = view.findViewById(R.id.theme_preview_pager);
@@ -237,45 +246,70 @@
                 ? View.INVISIBLE : View.VISIBLE);
     }
 
+    private void hideError() {
+        mContent.setVisibility(View.VISIBLE);
+        mError.setVisibility(View.GONE);
+    }
+
+    private void showError() {
+        mLoading.hide();
+        mContent.setVisibility(View.GONE);
+        mError.setVisibility(View.VISIBLE);
+    }
+
     private void setUpOptions(@Nullable Bundle savedInstanceState) {
-        mThemeManager.fetchOptions(options -> {
-            mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
-            mOptionsController.addListener(selected -> {
-                if (selected instanceof CustomTheme && !((CustomTheme) selected).isDefined()) {
-                    navigateToCustomTheme((CustomTheme) selected);
-                } else {
-                    mSelectedTheme = (ThemeBundle) selected;
-                    if (mUseMyWallpaper || mSelectedTheme instanceof CustomTheme) {
-                        mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper);
+        hideError();
+        mLoading.show();
+        mThemeManager.fetchOptions(new OptionsFetchedListener<ThemeBundle>() {
+            @Override
+            public void onOptionsLoaded(List<ThemeBundle> options) {
+                mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
+                mOptionsController.addListener(selected -> {
+                    mLoading.hide();
+                    if (selected instanceof CustomTheme && !((CustomTheme) selected).isDefined()) {
+                        navigateToCustomTheme((CustomTheme) selected);
                     } else {
-                        mSelectedTheme.setOverrideThemeWallpaper(null);
+                        mSelectedTheme = (ThemeBundle) selected;
+                        if (mUseMyWallpaper || mSelectedTheme instanceof CustomTheme) {
+                            mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper);
+                        } else {
+                            mSelectedTheme.setOverrideThemeWallpaper(null);
+                        }
+                        mEventLogger.logThemeSelected(mSelectedTheme,
+                                selected instanceof CustomTheme);
+                        createAdapter(options);
+                        updateButtonsVisibility();
                     }
-                    mEventLogger.logThemeSelected(mSelectedTheme, selected instanceof CustomTheme);
-                    createAdapter(options);
-                    updateButtonsVisibility();
+                });
+                mOptionsController.initOptions(mThemeManager);
+                String previouslySelected = savedInstanceState != null
+                        ? savedInstanceState.getString(KEY_SELECTED_THEME) : null;
+                for (ThemeBundle theme : options) {
+                    if (previouslySelected != null
+                            && previouslySelected.equals(theme.getSerializedPackages())) {
+                        mSelectedTheme = theme;
+                    } else if (theme.isActive(mThemeManager)) {
+                        mSelectedTheme = theme;
+                        break;
+                    }
                 }
-            });
-            mOptionsController.initOptions(mThemeManager);
-            String previouslySelected = savedInstanceState != null
-                    ? savedInstanceState.getString(KEY_SELECTED_THEME) : null;
-            for (ThemeBundle theme : options) {
-                if (previouslySelected != null
-                        && previouslySelected.equals(theme.getSerializedPackages())) {
-                    mSelectedTheme = theme;
-                } else if (theme.isActive(mThemeManager)) {
-                    mSelectedTheme = theme;
-                    break;
+                if (mSelectedTheme == null) {
+                    // Select the default theme if there is no matching custom enabled theme
+                    // TODO(b/124796742): default to custom if there is no matching theme bundle
+                    mSelectedTheme = options.get(0);
+                } else {
+                    // Only show show checkmark if we found a matching theme
+                    mOptionsController.setAppliedOption(mSelectedTheme);
                 }
+                mOptionsController.setSelectedOption(mSelectedTheme);
             }
-            if (mSelectedTheme == null) {
-                // Select the default theme if there is no matching custom enabled theme
-                // TODO(b/124796742): default to custom if there is no matching theme bundle
-                mSelectedTheme = options.get(0);
-            } else {
-                // Only show show checkmark if we found a matching theme
-                mOptionsController.setAppliedOption(mSelectedTheme);
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                if (throwable != null) {
+                    Log.e(TAG, "Error loading theme bundles", throwable);
+                }
+                showError();
             }
-            mOptionsController.setSelectedOption(mSelectedTheme);
         }, false);
     }