[LivePicker 4/n] Deduplicate PreviewFragment logic

Consolidate "info page" set up into one method, so both live
and static wallpapers share the same UI format.
Similarly with the loading indicator.

Bug: 141391722
Change-Id: Ie8e1c94f147055ac471e262abee53da050abc286
diff --git a/res/layout/preview_page_settings.xml b/res/layout/preview_page_settings.xml
index 72e83a5..4faebb6 100644
--- a/res/layout/preview_page_settings.xml
+++ b/res/layout/preview_page_settings.xml
@@ -37,7 +37,7 @@
 
     <Button
         style="@style/ButtonStyle"
-        android:id="@+id/preview_attribution_pane_set_wallpaper_button"
+        android:id="@+id/preview_settings_pane_set_wallpaper_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/set_wallpaper_button_text"/>
diff --git a/src/com/android/wallpaper/picker/ImagePreviewFragment.java b/src/com/android/wallpaper/picker/ImagePreviewFragment.java
index 87e29c5..def680f 100755
--- a/src/com/android/wallpaper/picker/ImagePreviewFragment.java
+++ b/src/com/android/wallpaper/picker/ImagePreviewFragment.java
@@ -15,8 +15,6 @@
  */
 package com.android.wallpaper.picker;
 
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.Activity;
@@ -28,14 +26,11 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.view.ContextThemeWrapper;
 import android.view.Display;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
 import android.widget.ImageView;
-import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.fragment.app.FragmentActivity;
@@ -46,7 +41,6 @@
 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
 import com.android.wallpaper.util.ScreenSizeCalculator;
 import com.android.wallpaper.util.WallpaperCropUtils;
-import com.android.wallpaper.widget.MaterialProgressDrawable;
 
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.MemoryCategory;
@@ -54,8 +48,6 @@
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 
-import java.util.List;
-
 /**
  * Fragment which displays the UI for previewing an individual static wallpaper and its attribution
  * information.
@@ -63,22 +55,16 @@
 public class ImagePreviewFragment extends PreviewFragment {
 
     private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f;
+    private static final int LOADING_TIME_MS = 500;
 
     private SubsamplingScaleImageView mFullResImageView;
     private Asset mWallpaperAsset;
-    private TextView mAttributionTitle;
-    private TextView mAttributionSubtitle1;
-    private TextView mAttributionSubtitle2;
-    private Button mExploreButton;
-    private Button mSetWallpaperButton;
-
     private Point mDefaultCropSurfaceSize;
     private Point mScreenSize;
     private Point mRawWallpaperSize; // Native size of wallpaper image.
-    private ImageView mLoadingIndicator;
-    private MaterialProgressDrawable mProgressDrawable;
     private ImageView mLowResImageView;
-    private View mSpacer;
+
+    private InfoPageController mInfoPageController;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -91,30 +77,29 @@
         return R.layout.fragment_image_preview;
     }
 
-    @Override
+
     protected int getBottomSheetResId() {
         return R.id.bottom_sheet;
     }
 
     @Override
+    protected int getLoadingIndicatorResId() {
+        return R.id.loading_indicator;
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View view = super.onCreateView(inflater, container, savedInstanceState);
 
         Activity activity = requireActivity();
-        // Set toolbar as the action bar.
 
         mFullResImageView = view.findViewById(R.id.full_res_image);
-        mLoadingIndicator = view.findViewById(R.id.loading_indicator);
 
-        mAttributionTitle = view.findViewById(R.id.preview_attribution_pane_title);
-        mAttributionSubtitle1 = view.findViewById(R.id.preview_attribution_pane_subtitle1);
-        mAttributionSubtitle2 = view.findViewById(R.id.preview_attribution_pane_subtitle2);
-        mExploreButton = view.findViewById(R.id.preview_attribution_pane_explore_button);
-        mSetWallpaperButton = view.findViewById(R.id.preview_attribution_pane_set_wallpaper_button);
+        mInfoPageController = new InfoPageController(view.findViewById(R.id.page_info),
+                mPreviewMode);
 
         mLowResImageView = view.findViewById(R.id.low_res_image);
-        mSpacer = view.findViewById(R.id.spacer);
 
         // Trim some memory from Glide to make room for the full-size image in this fragment.
         Glide.get(activity).setMemoryCategory(MemoryCategory.LOW);
@@ -148,8 +133,7 @@
             setUpExploreIntent(ImagePreviewFragment.this::initFullResView);
         });
 
-        // Configure loading indicator with a MaterialProgressDrawable.
-        setUpLoadingIndicator();
+        setUpLoadingIndicator(LOADING_TIME_MS);
 
         return view;
     }
@@ -159,32 +143,9 @@
         // Nothing needed here.
     }
 
-    private void setUpLoadingIndicator() {
-        Context context = requireContext();
-        mProgressDrawable = new MaterialProgressDrawable(context.getApplicationContext(),
-                mLoadingIndicator);
-        mProgressDrawable.setAlpha(255);
-        mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100,
-                context.getTheme()));
-        mProgressDrawable.setColorSchemeColors(getAttrColor(
-                new ContextThemeWrapper(context, getDeviceDefaultTheme()),
-                android.R.attr.colorAccent));
-        mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE);
-        mLoadingIndicator.setImageDrawable(mProgressDrawable);
-
-        // We don't want to show the spinner every time we load an image if it loads quickly;
-        // instead, only start showing the spinner if loading the image has taken longer than half
-        // of a second.
-        mLoadingIndicator.postDelayed(() -> {
-            if (mFullResImageView != null && !mFullResImageView.hasImage()
-                    && !mTestingModeEnabled) {
-                mLoadingIndicator.setVisibility(View.VISIBLE);
-                mLoadingIndicator.setAlpha(1f);
-                if (mProgressDrawable != null) {
-                    mProgressDrawable.start();
-                }
-            }
-        }, 500);
+    @Override
+    protected boolean isLoaded() {
+        return mFullResImageView != null && mFullResImageView.hasImage();
     }
 
     @Override
@@ -214,63 +175,12 @@
 
     @Override
     protected void setBottomSheetContentAlpha(float alpha) {
-        mExploreButton.setAlpha(alpha);
-        mAttributionTitle.setAlpha(alpha);
-        mAttributionSubtitle1.setAlpha(alpha);
-        mAttributionSubtitle2.setAlpha(alpha);
+        mInfoPageController.setContentAlpha(alpha);
     }
 
-    private void populateAttributionPane() {
-        final Context context = getContext();
-
-        final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
-
-        List<String> attributions = mWallpaper.getAttributions(context);
-        if (attributions.size() > 0 && attributions.get(0) != null) {
-            mAttributionTitle.setText(attributions.get(0));
-        }
-
-        if (attributions.size() > 1 && attributions.get(1) != null) {
-            mAttributionSubtitle1.setVisibility(View.VISIBLE);
-            mAttributionSubtitle1.setText(attributions.get(1));
-        }
-
-        if (attributions.size() > 2 && attributions.get(2) != null) {
-            mAttributionSubtitle2.setVisibility(View.VISIBLE);
-            mAttributionSubtitle2.setText(attributions.get(2));
-        }
-
-        setUpSetWallpaperButton(mSetWallpaperButton);
-
-        setUpExploreButton(mExploreButton);
-
-        if (mExploreButton.getVisibility() == View.VISIBLE
-                && mSetWallpaperButton.getVisibility() == View.VISIBLE) {
-            mSpacer.setVisibility(View.VISIBLE);
-        } else {
-            mSpacer.setVisibility(View.GONE);
-        }
-
-        mBottomSheet.setVisibility(View.VISIBLE);
-
-        // Initialize the state of the BottomSheet based on the current state because if the initial
-        // and current state are the same, the state change listener won't fire and set the correct
-        // arrow asset and text alpha.
-        if (bottomSheetBehavior.getState() == STATE_EXPANDED) {
-            setPreviewChecked(false);
-            mAttributionTitle.setAlpha(1f);
-            mAttributionSubtitle1.setAlpha(1f);
-            mAttributionSubtitle2.setAlpha(1f);
-        } else {
-            setPreviewChecked(true);
-            mAttributionTitle.setAlpha(0f);
-            mAttributionSubtitle1.setAlpha(0f);
-            mAttributionSubtitle2.setAlpha(0f);
-        }
-
-        // Let the state change listener take care of animating a state change to the initial state
-        // if there's a state change.
-        bottomSheetBehavior.setState(mBottomSheetInitialState);
+    @Override
+    protected CharSequence getExploreButtonLabel(Context context) {
+        return context.getString(mWallpaper.getActionLabelRes(context));
     }
 
     /**
@@ -320,7 +230,7 @@
                     }
                     getActivity().invalidateOptionsMenu();
 
-                    populateAttributionPane();
+                    populateInfoPage(mInfoPageController);
                 });
     }
 
diff --git a/src/com/android/wallpaper/picker/LivePreviewFragment.java b/src/com/android/wallpaper/picker/LivePreviewFragment.java
index fee60a8..e6390e6 100644
--- a/src/com/android/wallpaper/picker/LivePreviewFragment.java
+++ b/src/com/android/wallpaper/picker/LivePreviewFragment.java
@@ -15,12 +15,11 @@
  */
 package com.android.wallpaper.picker;
 
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
-
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.WallpaperColors;
+import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -53,9 +52,6 @@
 import android.view.ViewGroup;
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.AnimationUtils;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -70,9 +66,7 @@
 import com.android.wallpaper.compat.BuildCompat;
 import com.android.wallpaper.model.LiveWallpaperInfo;
 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
-import com.android.wallpaper.widget.MaterialProgressDrawable;
 
-import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.tabs.TabLayout;
 
 import java.util.ArrayList;
@@ -88,45 +82,37 @@
 
     private static final String KEY_ACTION_DELETE_LIVE_WALLPAPER = "action_delete_live_wallpaper";
     private static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info";
+    private static final int LOADING_TIME_MS = 200;
 
     /**
      * Instance of {@link WallpaperConnection} used to bind to the live wallpaper service to show
      * it in this preview fragment.
      * @see IWallpaperConnection
      */
-    private WallpaperConnection mWallpaperConnection;
+    protected WallpaperConnection mWallpaperConnection;
 
     private Intent mWallpaperIntent;
     private Intent mDeleteIntent;
     private Intent mSettingsIntent;
 
     private List<Pair<String, View>> mPages;
-    private ImageView mLoadingIndicator;
-    private TextView mAttributionTitle;
-    private TextView mAttributionSubtitle1;
-    private TextView mAttributionSubtitle2;
-    private Button mExploreButton;
-    private Button mSetWallpaperButton;
     private ViewPager mViewPager;
     private TabLayout mTabLayout;
     private SliceView mSettingsSliceView;
     private LiveData<Slice> mSettingsLiveData;
-    private View mSpacer;
     private View mLoadingScrim;
-    private MaterialProgressDrawable mProgressDrawable;
+    private InfoPageController mInfoPageController;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent();
-        mWallpaperIntent = new Intent(WallpaperService.SERVICE_INTERFACE)
-                .setClassName(info.getPackageName(), info.getServiceName());
+        mWallpaperIntent = getWallpaperIntent(info);
         setUpExploreIntent(null);
 
         android.app.WallpaperInfo currentWallpaper =
                 WallpaperManager.getInstance(requireContext()).getWallpaperInfo();
-        String deleteAction = getDeleteAction(info.getServiceInfo(),
-                (currentWallpaper == null) ? null : currentWallpaper.getServiceInfo());
+        String deleteAction = getDeleteAction(info, currentWallpaper);
 
         if (!TextUtils.isEmpty(deleteAction)) {
             mDeleteIntent = new Intent(deleteAction);
@@ -134,7 +120,7 @@
             mDeleteIntent.putExtra(EXTRA_LIVE_WALLPAPER_INFO, info);
         }
 
-        String settingsActivity = info.getSettingsActivity();
+        String settingsActivity = getSettingsActivity(info);
         if (settingsActivity != null) {
             mSettingsIntent = new Intent();
             mSettingsIntent.setComponent(new ComponentName(info.getPackageName(),
@@ -149,6 +135,16 @@
         }
     }
 
+    @Nullable
+    protected String getSettingsActivity(WallpaperInfo info) {
+        return info.getSettingsActivity();
+    }
+
+    protected Intent getWallpaperIntent(WallpaperInfo info) {
+        return new Intent(WallpaperService.SERVICE_INTERFACE)
+                .setClassName(info.getPackageName(), info.getServiceName());
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
@@ -161,10 +157,10 @@
         Activity activity = requireActivity();
 
         mLoadingScrim = view.findViewById(R.id.loading);
-        mLoadingIndicator = view.findViewById(R.id.loading_indicator);
-        setUpLoadingIndicator();
+        setUpLoadingIndicator(LOADING_TIME_MS);
 
-        mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity);
+        mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity,
+                getWallpaperConnectionListener());
         container.post(() -> {
             if (!mWallpaperConnection.connect()) {
                 mWallpaperConnection = null;
@@ -253,45 +249,18 @@
         mViewPager.setCurrentItem(0);
     }
 
-    private void setUpLoadingIndicator() {
-        Context context = requireContext();
-        mProgressDrawable = new MaterialProgressDrawable(context.getApplicationContext(),
-                mLoadingIndicator);
-        mProgressDrawable.setAlpha(255);
-        mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100,
-                context.getTheme()));
-        mProgressDrawable.setColorSchemeColors(getAttrColor(
-                new ContextThemeWrapper(context, getDeviceDefaultTheme()),
-                android.R.attr.colorAccent));
-        mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE);
-        mLoadingIndicator.setImageDrawable(mProgressDrawable);
+    protected WallpaperConnectionListener getWallpaperConnectionListener() {
+        return null;
+    }
 
-        // We don't want to show the spinner every time we load a wallpaper if it loads quickly;
-        // instead, only start showing the spinner after 100 ms
-        mLoadingIndicator.postDelayed(() -> {
-            if ((mWallpaperConnection == null || !mWallpaperConnection.isEngineReady())
-                    && !mTestingModeEnabled) {
-                mLoadingIndicator.setVisibility(View.VISIBLE);
-                mLoadingIndicator.setAlpha(1f);
-                if (mProgressDrawable != null) {
-                    mProgressDrawable.start();
-                }
-            }
-        }, 100);
+    @Override
+    protected boolean isLoaded() {
+        return mWallpaperConnection != null && mWallpaperConnection.isEngineReady();
     }
 
     private void initInfoPage() {
-        View pageInfo = getLayoutInflater().inflate(R.layout.preview_page_info, null /* root */);
-
-        mAttributionTitle = pageInfo.findViewById(R.id.preview_attribution_pane_title);
-        mAttributionSubtitle1 = pageInfo.findViewById(R.id.preview_attribution_pane_subtitle1);
-        mAttributionSubtitle2 = pageInfo.findViewById(R.id.preview_attribution_pane_subtitle2);
-        mSpacer = pageInfo.findViewById(R.id.spacer);
-
-        mExploreButton = pageInfo.findViewById(R.id.preview_attribution_pane_explore_button);
-        mSetWallpaperButton = pageInfo.findViewById(
-                R.id.preview_attribution_pane_set_wallpaper_button);
-
+        View pageInfo = InfoPageController.createView(getLayoutInflater());
+        mInfoPageController = new InfoPageController(pageInfo, mPreviewMode);
         mPages.add(Pair.create(getString(R.string.tab_info), pageInfo));
     }
 
@@ -312,68 +281,23 @@
         mSettingsLiveData = SliceLiveData.fromUri(requireContext() /* context */, uriSettingsSlice);
         mSettingsLiveData.observeForever(mSettingsSliceView);
 
+        pageSettings.findViewById(R.id.preview_settings_pane_set_wallpaper_button)
+                .setOnClickListener(this::onSetWallpaperClicked);
+
         mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings));
     }
 
-    private void populateAttributionPane() {
-        final Context context = getContext();
-
-        final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
-
-        List<String> attributions = mWallpaper.getAttributions(context);
-        if (attributions.size() > 0 && attributions.get(0) != null) {
-            mAttributionTitle.setText(attributions.get(0));
+    @Override
+    protected CharSequence getExploreButtonLabel(Context context) {
+        CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context);
+        if (TextUtils.isEmpty(exploreLabel)) {
+            exploreLabel = context.getString(mWallpaper.getActionLabelRes(context));
         }
-
-        if (mWallpaper.getWallpaperComponent().getShowMetadataInPreview()) {
-
-            if (attributions.size() > 1 && attributions.get(1) != null) {
-                mAttributionSubtitle1.setVisibility(View.VISIBLE);
-                mAttributionSubtitle1.setText(attributions.get(1));
-            }
-
-            if (attributions.size() > 2 && attributions.get(2) != null) {
-                mAttributionSubtitle2.setVisibility(View.VISIBLE);
-                mAttributionSubtitle2.setText(attributions.get(2));
-            }
-
-        } else {
-            mExploreIntent = null;
-        }
-
-        setUpSetWallpaperButton(mSetWallpaperButton);
-
-        setUpExploreButton(mExploreButton);
-
-        if (mExploreButton.getVisibility() == View.VISIBLE
-                && mSetWallpaperButton.getVisibility() == View.VISIBLE) {
-            mSpacer.setVisibility(View.VISIBLE);
-        } else {
-            mSpacer.setVisibility(View.GONE);
-        }
-
-        mBottomSheet.setVisibility(View.VISIBLE);
-
-        // Initialize the state of the BottomSheet based on the current state because if the initial
-        // and current state are the same, the state change listener won't fire and set the correct
-        // arrow asset and text alpha.
-        if (mBottomSheetInitialState == STATE_EXPANDED) {
-            setPreviewChecked(false);
-            mAttributionTitle.setAlpha(1f);
-            mAttributionSubtitle1.setAlpha(1f);
-            mAttributionSubtitle2.setAlpha(1f);
-        } else {
-            setPreviewChecked(true);
-            mAttributionTitle.setAlpha(0f);
-            mAttributionSubtitle1.setAlpha(0f);
-            mAttributionSubtitle2.setAlpha(0f);
-        }
-
-        bottomSheetBehavior.setState(mBottomSheetInitialState);
+        return exploreLabel;
     }
 
     @SuppressLint("NewApi") //Already checking with isAtLeastQ
-    private Uri getSettingsSliceUri(android.app.WallpaperInfo info) {
+    protected Uri getSettingsSliceUri(android.app.WallpaperInfo info) {
         if (BuildCompat.isAtLeastQ()) {
             return info.getSettingsSliceUri();
         }
@@ -391,6 +315,11 @@
     }
 
     @Override
+    protected int getLoadingIndicatorResId() {
+        return R.id.loading_indicator;
+    }
+
+    @Override
     protected void setCurrentWallpaper(int destination) {
         mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, null,
                 destination, 0, null, new SetWallpaperCallback() {
@@ -408,33 +337,20 @@
 
     @Override
     protected void setBottomSheetContentAlpha(float alpha) {
-        mExploreButton.setAlpha(alpha);
-        mAttributionTitle.setAlpha(alpha);
-        mAttributionSubtitle1.setAlpha(alpha);
-        mAttributionSubtitle2.setAlpha(alpha);
+        mInfoPageController.setContentAlpha(alpha);
     }
 
-    @Override
-    protected void setUpExploreButton(Button exploreButton) {
-        super.setUpExploreButton(exploreButton);
-        if (exploreButton.getVisibility() != View.VISIBLE) {
-            return;
-        }
-        Context context = requireContext();
-        CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context);
-        if (!TextUtils.isEmpty(exploreLabel)) {
-            exploreButton.setText(exploreLabel);
-        }
-    }
 
     @Nullable
-    private String getDeleteAction(ServiceInfo serviceInfo,
-            @Nullable ServiceInfo currentService) {
+    protected String getDeleteAction(android.app.WallpaperInfo wallpaperInfo,
+            @Nullable android.app.WallpaperInfo currentInfo) {
+        ServiceInfo serviceInfo = wallpaperInfo.getServiceInfo();
         if (!isPackagePreInstalled(serviceInfo.applicationInfo)) {
             Log.d(TAG, "This wallpaper is not pre-installed: " + serviceInfo.name);
             return null;
         }
 
+        ServiceInfo currentService = currentInfo == null ? null : currentInfo.getServiceInfo();
         // A currently set Live wallpaper should not be deleted.
         if (currentService != null && TextUtils.equals(serviceInfo.name, currentService.name)) {
             return null;
@@ -510,10 +426,27 @@
         return false;
     }
 
-    private class WallpaperConnection extends IWallpaperConnection.Stub
+    /**
+     * Interface to be notified of connect/disconnect events from {@link WallpaperConnection}
+     */
+    public interface WallpaperConnectionListener {
+        /**
+         * Called after the Wallpaper service has been bound.
+         */
+        void onConnected();
+
+        /**
+         * Called after the Wallpaper engine has been terminated and the service has been unbound.
+         */
+        void onDisconnected();
+    }
+
+    protected class WallpaperConnection extends IWallpaperConnection.Stub
             implements ServiceConnection {
+
         private final Activity mActivity;
         private final Intent mIntent;
+        private final WallpaperConnectionListener mListener;
         private IWallpaperService mService;
         private IWallpaperEngine mEngine;
         private boolean mConnected;
@@ -521,20 +454,26 @@
         private boolean mIsEngineVisible;
         private boolean mEngineReady;
 
-        WallpaperConnection(Intent intent, Activity activity) {
+        WallpaperConnection(Intent intent, Activity activity,
+                @Nullable WallpaperConnectionListener listener) {
             mActivity = activity;
             mIntent = intent;
+            mListener = listener;
         }
 
         public boolean connect() {
             synchronized (this) {
-                if (!mActivity.bindService(mIntent, this, Context.BIND_AUTO_CREATE)) {
+                if (!mActivity.bindService(mIntent, this,
+                        Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) {
                     return false;
                 }
 
                 mConnected = true;
-                return true;
             }
+            if (mListener != null) {
+                mListener.onConnected();
+            }
+            return true;
         }
 
         public void disconnect() {
@@ -556,6 +495,9 @@
                 }
                 mService = null;
             }
+            if (mListener != null) {
+                mListener.onDisconnected();
+            }
         }
 
         public void onServiceConnected(ComponentName name, IBinder service) {
@@ -599,6 +541,10 @@
             }
         }
 
+        public IWallpaperEngine getEngine() {
+            return mEngine;
+        }
+
         public ParcelFileDescriptor setWallpaper(String name) {
             return null;
         }
@@ -626,7 +572,7 @@
                                 mProgressDrawable.stop();
                             }
                             mLoadingScrim.setVisibility(View.INVISIBLE);
-                            populateAttributionPane();
+                            populateInfoPage(mInfoPageController);
                         });
             });
             mEngineReady = true;
diff --git a/src/com/android/wallpaper/picker/PreviewFragment.java b/src/com/android/wallpaper/picker/PreviewFragment.java
index f1a1625..25723c2 100755
--- a/src/com/android/wallpaper/picker/PreviewFragment.java
+++ b/src/com/android/wallpaper/picker/PreviewFragment.java
@@ -21,7 +21,6 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.res.Resources.NotFoundException;
 import android.content.res.TypedArray;
 import android.graphics.PorterDuff.Mode;
@@ -30,15 +29,19 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.Window;
 import android.widget.Button;
 import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import androidx.annotation.CallSuper;
@@ -63,6 +66,7 @@
 import com.android.wallpaper.module.WallpaperPersister.Destination;
 import com.android.wallpaper.module.WallpaperPreferences;
 import com.android.wallpaper.module.WallpaperSetter;
+import com.android.wallpaper.widget.MaterialProgressDrawable;
 
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.bottomsheet.BottomSheetBehavior.State;
@@ -78,7 +82,8 @@
         LoadWallpaperErrorDialogFragment.Listener {
 
     /**
-     * User can view wallpaper and attributions in full screen, but "Set wallpaper" button is hidden.
+     * User can view wallpaper and attributions in full screen, but "Set wallpaper" button is
+     * hidden.
      */
     static final int MODE_VIEW_ONLY = 0;
 
@@ -142,6 +147,8 @@
     protected WallpaperSetter mWallpaperSetter;
     protected UserEventLogger mUserEventLogger;
     protected ViewGroup mBottomSheet;
+    protected ImageView mLoadingIndicator;
+    protected MaterialProgressDrawable mProgressDrawable;
 
     protected CheckBox mPreview;
 
@@ -192,15 +199,12 @@
                         | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                         | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
 
-        List<String> attributions = mWallpaper.getAttributions(activity);
+        List<String> attributions = getAttributions(activity);
         if (attributions.size() > 0 && attributions.get(0) != null) {
             activity.setTitle(attributions.get(0));
         }
     }
 
-    @LayoutRes
-    protected abstract int getLayoutResId();
-
     @Override
     @CallSuper
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -236,6 +240,8 @@
                         R.dimen.preview_toolbar_set_wallpaper_button_end_padding),
         /* bottom */ 0);
 
+        mLoadingIndicator = view.findViewById(getLoadingIndicatorResId());
+
         mBottomSheet = view.findViewById(getBottomSheetResId());
         setUpBottomSheetView(mBottomSheet);
 
@@ -257,11 +263,58 @@
         return view;
     }
 
+    protected void populateInfoPage(InfoPageController infoPage) {
+        Context context = requireContext();
+
+        BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
+
+        List<String> attributions = getAttributions(context);
+        boolean showMetadata = shouldShowMetadataInPreview();
+        CharSequence exploreLabel = getExploreButtonLabel(context);
+
+        infoPage.populate(attributions, showMetadata, this::onSetWallpaperClicked,
+                exploreLabel,
+                (showMetadata && mExploreIntent != null) ? this::onExploreClicked : null);
+
+        mBottomSheet.setVisibility(View.VISIBLE);
+
+        // Initialize the state of the BottomSheet based on the current state because if the initial
+        // and current state are the same, the state change listener won't fire and set the correct
+        // arrow asset and text alpha.
+        if (mBottomSheetInitialState == STATE_EXPANDED) {
+            setPreviewChecked(false);
+            infoPage.setContentAlpha(1f);
+        } else {
+            setPreviewChecked(true);
+            infoPage.setContentAlpha(0f);
+        }
+
+        bottomSheetBehavior.setState(mBottomSheetInitialState);
+    }
+
+    protected List<String> getAttributions(Context context) {
+        return mWallpaper.getAttributions(context);
+    }
+
+    protected boolean shouldShowMetadataInPreview() {
+        android.app.WallpaperInfo wallpaperComponent = mWallpaper.getWallpaperComponent();
+        return wallpaperComponent == null || wallpaperComponent.getShowMetadataInPreview();
+    }
+
+    @Nullable
+    protected abstract CharSequence getExploreButtonLabel(Context context);
+
+    @LayoutRes
+    protected abstract int getLayoutResId();
+
     protected abstract void setUpBottomSheetView(ViewGroup bottomSheet);
 
     @IdRes
     protected abstract int getBottomSheetResId();
 
+    @IdRes
+    protected abstract int getLoadingIndicatorResId();
+
     protected int getDeviceDefaultTheme() {
         return android.R.style.Theme_DeviceDefault;
     }
@@ -342,33 +395,6 @@
         }
     }
 
-    protected void setUpSetWallpaperButton(Button setWallpaperButton) {
-        if (mPreviewMode == MODE_VIEW_ONLY) {
-            setWallpaperButton.setVisibility(View.GONE);
-        } else {
-            setWallpaperButton.setVisibility(View.VISIBLE);
-            setWallpaperButton.setOnClickListener(this::onSetWallpaperClicked);
-        }
-    }
-
-    protected void setUpExploreButton(Button exploreButton) {
-        exploreButton.setVisibility(View.GONE);
-        if (mExploreIntent == null) {
-            return;
-        }
-        Context context = requireContext();
-        exploreButton.setVisibility(View.VISIBLE);
-        exploreButton.setText(context.getString(
-                mWallpaper.getActionLabelRes(context)));
-
-        exploreButton.setOnClickListener(view -> {
-            mUserEventLogger.logActionClicked(mWallpaper.getCollectionId(context),
-                    mWallpaper.getActionLabelRes(context));
-
-            startActivity(mExploreIntent);
-        });
-    }
-
     protected void setUpExploreIntent(@Nullable Runnable callback) {
         Context context = getContext();
         if (context == null) {
@@ -397,6 +423,40 @@
         }
     }
 
+    /**
+     * Configure loading indicator with a MaterialProgressDrawable.
+     * We don't want to show the spinner every time we load an image if it loads quickly;
+     * instead, only start showing the spinner if loading the image has taken longer than the
+     * provided delayMs
+     * @param delayMs ms to wait before actually showing the loading indicator
+     */
+    protected void setUpLoadingIndicator(int delayMs) {
+        Context context = requireContext();
+        mProgressDrawable = new MaterialProgressDrawable(context.getApplicationContext(),
+                mLoadingIndicator);
+        mProgressDrawable.setAlpha(255);
+        mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100,
+                context.getTheme()));
+        mProgressDrawable.setColorSchemeColors(getAttrColor(
+                new ContextThemeWrapper(context, getDeviceDefaultTheme()),
+                android.R.attr.colorAccent));
+        mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE);
+        mLoadingIndicator.setImageDrawable(mProgressDrawable);
+
+
+        mLoadingIndicator.postDelayed(() -> {
+            if (!isLoaded() && !mTestingModeEnabled) {
+                mLoadingIndicator.setVisibility(View.VISIBLE);
+                mLoadingIndicator.setAlpha(1f);
+                if (mProgressDrawable != null) {
+                    mProgressDrawable.start();
+                }
+            }
+        }, delayMs);
+    }
+
+    protected abstract boolean isLoaded();
+
     @Override
     public void onSet(int destination) {
         setCurrentWallpaper(destination);
@@ -429,11 +489,22 @@
         outState.putInt(KEY_BOTTOM_SHEET_STATE, bottomSheetBehavior.getState());
     }
 
-    private void onSetWallpaperClicked(View button) {
+    protected void onSetWallpaperClicked(View button) {
         mWallpaperSetter.requestDestination(getContext(), getFragmentManager(), this,
                 mWallpaper instanceof LiveWallpaperInfo);
     }
 
+    private void onExploreClicked(View button) {
+        if (getContext() == null) {
+            return;
+        }
+        Context context = getContext();
+        mUserEventLogger.logActionClicked(mWallpaper.getCollectionId(context),
+                mWallpaper.getActionLabelRes(context));
+
+        startActivity(mExploreIntent);
+    }
+
     private void setUpBottomSheetListeners() {
         final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
 
@@ -530,14 +601,6 @@
         }
     }
 
-    @IntDef({
-            ActivityInfo.SCREEN_ORIENTATION_PORTRAIT,
-            ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
-            ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT,
-            ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE})
-    private @interface ActivityInfoScreenOrientation {
-    }
-
     /**
      * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support
      * was added in API 17, returns false for versions lower than 17.
@@ -546,4 +609,94 @@
         return getResources().getConfiguration().getLayoutDirection()
                     == View.LAYOUT_DIRECTION_RTL;
     }
+
+    protected static class InfoPageController {
+
+        public static View createView(LayoutInflater inflater) {
+            return inflater.inflate(R.layout.preview_page_info, null /* root */);
+        }
+
+        private final int mPreviewMode;
+        private final View mInfoPage;
+        private final TextView mAttributionTitle;
+        private final TextView mAttributionSubtitle1;
+        private final TextView mAttributionSubtitle2;
+        private final Button mExploreButton;
+        private final Button mSetWallpaperButton;
+        private final View mSpacer;
+
+        public InfoPageController(View infoPage, int previewMode) {
+            mInfoPage = infoPage;
+            mPreviewMode = previewMode;
+
+            mAttributionTitle = mInfoPage.findViewById(R.id.preview_attribution_pane_title);
+            mAttributionSubtitle1 = mInfoPage.findViewById(R.id.preview_attribution_pane_subtitle1);
+            mAttributionSubtitle2 = mInfoPage.findViewById(R.id.preview_attribution_pane_subtitle2);
+            mSpacer = mInfoPage.findViewById(R.id.spacer);
+
+            mExploreButton = mInfoPage.findViewById(R.id.preview_attribution_pane_explore_button);
+            mSetWallpaperButton = mInfoPage.findViewById(
+                    R.id.preview_attribution_pane_set_wallpaper_button);
+        }
+
+        public void populate(List<String> attributions, boolean showMetadata,
+                OnClickListener setWallpaperOnClickListener,
+                CharSequence exploreButtonLabel,
+                @Nullable OnClickListener exploreOnClickListener) {
+            if (attributions.size() > 0 && attributions.get(0) != null) {
+                mAttributionTitle.setText(attributions.get(0));
+            }
+
+            if (showMetadata) {
+                if (attributions.size() > 1 && attributions.get(1) != null) {
+                    mAttributionSubtitle1.setVisibility(View.VISIBLE);
+                    mAttributionSubtitle1.setText(attributions.get(1));
+                }
+
+                if (attributions.size() > 2 && attributions.get(2) != null) {
+                    mAttributionSubtitle2.setVisibility(View.VISIBLE);
+                    mAttributionSubtitle2.setText(attributions.get(2));
+                }
+            }
+            setUpSetWallpaperButton(setWallpaperOnClickListener);
+
+            setUpExploreButton(exploreButtonLabel, exploreOnClickListener);
+
+            if (mExploreButton.getVisibility() == View.VISIBLE
+                    && mSetWallpaperButton.getVisibility() == View.VISIBLE) {
+                mSpacer.setVisibility(View.VISIBLE);
+            } else {
+                mSpacer.setVisibility(View.GONE);
+            }
+        }
+
+        public void setContentAlpha(float alpha) {
+            mSetWallpaperButton.setAlpha(alpha);
+            mExploreButton.setAlpha(alpha);
+            mAttributionTitle.setAlpha(alpha);
+            mAttributionSubtitle1.setAlpha(alpha);
+            mAttributionSubtitle2.setAlpha(alpha);
+        }
+
+        private void setUpSetWallpaperButton(OnClickListener setWallpaperOnClickListener) {
+            if (mPreviewMode == MODE_VIEW_ONLY) {
+                mSetWallpaperButton.setVisibility(View.GONE);
+            } else {
+                mSetWallpaperButton.setVisibility(View.VISIBLE);
+                mSetWallpaperButton.setOnClickListener(setWallpaperOnClickListener);
+            }
+        }
+
+        private void setUpExploreButton(CharSequence label,
+                @Nullable OnClickListener exploreOnClickListener) {
+            mExploreButton.setVisibility(View.GONE);
+            if (exploreOnClickListener == null) {
+                return;
+            }
+            mExploreButton.setVisibility(View.VISIBLE);
+            mExploreButton.setText(label);
+
+            mExploreButton.setOnClickListener(exploreOnClickListener);
+        }
+    }
 }
\ No newline at end of file