Refactor codes for AlbumsTabFragment

- Extract the TabFragment
- Rename BaseItemHolder to BaseViewHolder
- Modify the onItemClick for single select in PhotosTabFragment
  Add clearSelectedItems in PickerViewModel
- Add PhotosTabItemDecoration to adjust the gap of RecyclerView
- Support dark theme on date header
- Change the position of the check icon and the badges in RTL

Test: atest PickerViewModelTest
Test: screenshot on b/191746644
Bug: 191746644
Bug: 191127346
Bug: 191937323
Change-Id: Ie099a56104d7ee2f00acfac4566e898f4e2d8b29
Merged-In: Ie099a56104d7ee2f00acfac4566e898f4e2d8b29
(cherry picked from commit 34652dc00e7e2e917190a206530384f1a6d9e07f)
diff --git a/res/layout/fragment_photos_tab.xml b/res/layout/fragment_picker_tab.xml
similarity index 100%
rename from res/layout/fragment_photos_tab.xml
rename to res/layout/fragment_picker_tab.xml
diff --git a/res/layout/item_photo_grid.xml b/res/layout/item_photo_grid.xml
index f9ced81..392f294 100644
--- a/res/layout/item_photo_grid.xml
+++ b/res/layout/item_photo_grid.xml
@@ -19,7 +19,6 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_margin="1.5dp"
     android:background="@color/picker_highlight_color"
     android:focusable="true">
 
@@ -46,7 +45,7 @@
                 android:id="@+id/icon_gif"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="right|top"
+                android:layout_gravity="end|top"
                 android:layout_marginEnd="@dimen/picker_item_badge_margin"
                 android:layout_marginTop="@dimen/picker_item_badge_margin"
                 android:scaleType="fitCenter"
@@ -57,7 +56,7 @@
                 android:id="@+id/video_container"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="right|top"
+                android:layout_gravity="end|top"
                 android:layout_marginEnd="@dimen/picker_item_badge_margin"
                 android:layout_marginTop="@dimen/picker_item_badge_margin"
                 android:orientation="horizontal"
@@ -91,7 +90,7 @@
         android:layout_marginStart="@dimen/picker_item_check_margin"
         android:layout_marginTop="@dimen/picker_item_check_margin"
         android:src="@drawable/picker_item_check"
-        android:layout_gravity="top|left"
+        android:layout_gravity="top|start"
         android:scaleType="fitCenter"/>
 
 </FrameLayout>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index cec3a2b..80ff63b 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -23,4 +23,5 @@
     <color name="picker_primary_color">#8AB4F8</color>
     <color name="picker_background_color">#202124</color>
     <color name="picker_highlight_color">#3D8AB4F8</color>
+    <color name="picker_date_header_text_color">@color/picker_default_white</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 28a7b60..cee0809 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -22,18 +22,23 @@
 
     <!-- PhotoPicker -->
     <dimen name="picker_photo_size">118dp</dimen>
+
     <dimen name="picker_bottom_bar_size">56dp</dimen>
     <dimen name="picker_bottom_bar_horizontal_gap">16dp</dimen>
     <dimen name="picker_bottom_bar_vertical_gap">10dp</dimen>
     <dimen name="picker_bottom_bar_elevation">8dp</dimen>
+
     <dimen name="picker_item_check_size">24dp</dimen>
     <dimen name="picker_item_check_margin">6dp</dimen>
     <dimen name="picker_item_badge_margin">5dp</dimen>
     <dimen name="picker_item_badge_text_margin">3dp</dimen>
     <dimen name="picker_item_badge_text_size">10dp</dimen>
+
     <dimen name="picker_date_header_height">56dp</dimen>
     <dimen name="picker_date_header_padding">16dp</dimen>
 
+    <dimen name="picker_photo_item_spacing">3dp</dimen>
+
     <!-- PhotoPicker Preview -->
     <dimen name="preview_buttons_margin_horizontal">16dp</dimen>
     <dimen name="preview_buttons_margin_bottom">10dp</dimen>
diff --git a/src/com/android/providers/media/photopicker/data/model/Item.java b/src/com/android/providers/media/photopicker/data/model/Item.java
index b91efe6..cb4ad4f 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.providers.media.photopicker.data.ItemsProvider;
 import com.android.providers.media.util.MimeUtils;
@@ -75,6 +76,19 @@
         updateFromCursor(cursor, userId);
     }
 
+    @VisibleForTesting
+    public Item(long id, String mimeType, String displayName, String volumeName, long dateTaken,
+            long duration, Uri uri) {
+        mId = id;
+        mMimeType = mimeType;
+        mDisplayName = displayName;
+        mVolumeName = volumeName;
+        mDateTaken = dateTaken;
+        mDuration = duration;
+        mUri = uri;
+        parseMimeType();
+    }
+
     public long getId() {
         return mId;
     }
diff --git a/src/com/android/providers/media/photopicker/ui/BaseItemHolder.java b/src/com/android/providers/media/photopicker/ui/BaseViewHolder.java
similarity index 82%
rename from src/com/android/providers/media/photopicker/ui/BaseItemHolder.java
rename to src/com/android/providers/media/photopicker/ui/BaseViewHolder.java
index 8d11744..3ba04d1 100644
--- a/src/com/android/providers/media/photopicker/ui/BaseItemHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/BaseViewHolder.java
@@ -24,16 +24,16 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 /**
- * ViewHolder of a photo item within a RecyclerView.
+ * ViewHolder of a item within a {@link RecyclerView.Adapter}.
  */
-public abstract class BaseItemHolder extends RecyclerView.ViewHolder {
+public abstract class BaseViewHolder extends RecyclerView.ViewHolder {
 
-    public BaseItemHolder(Context context, ViewGroup parent, int layout) {
+    public BaseViewHolder(Context context, ViewGroup parent, int layout) {
         this(context, inflateLayout(context, parent, layout));
     }
 
-    public BaseItemHolder(Context context, View item) {
-        super(item);
+    public BaseViewHolder(Context context, View view) {
+        super(view);
     }
 
     private static <V extends View> V inflateLayout(Context context, ViewGroup parent, int layout) {
diff --git a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
index b5122a1..e76938e 100644
--- a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
@@ -24,7 +24,7 @@
 /**
  * ViewHolder of a date header within a RecyclerView.
  */
-public class DateHeaderHolder extends BaseItemHolder {
+public class DateHeaderHolder extends BaseViewHolder {
     private TextView mTitle;
     public DateHeaderHolder(Context context, ViewGroup parent) {
         super(context, parent, R.layout.item_date_header);
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index dc4ac3e..1101444 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -44,7 +44,7 @@
         mContext = context;
     }
 
-    public void loadThumbanial(Item item, ImageView imageView) {
+    public void loadPhotoThumbnail(Item item, ImageView imageView) {
         int thumbSize = getThumbSize();
         final Size size = new Size(thumbSize, thumbSize);
         try {
diff --git a/src/com/android/providers/media/photopicker/ui/PhotoGridHolder.java b/src/com/android/providers/media/photopicker/ui/PhotoGridHolder.java
index bc1497e..6d7aa5c 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotoGridHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotoGridHolder.java
@@ -30,7 +30,7 @@
 /**
  * ViewHolder of a photo item within a RecyclerView.
  */
-public class PhotoGridHolder extends BaseItemHolder {
+public class PhotoGridHolder extends BaseViewHolder {
 
     private final Context mContext;
     private final ImageLoader mImageLoader;
@@ -62,7 +62,7 @@
     @Override
     public void bind() {
         final Item item = (Item) itemView.getTag();
-        mImageLoader.loadThumbanial(item, mIconThumb);
+        mImageLoader.loadPhotoThumbnail(item, mIconThumb);
 
         if (item.isGif()) {
             mIconGif.setVisibility(View.VISIBLE);
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index b239d52..b78bae9 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -32,9 +32,9 @@
 /**
  * Adapts from model to something RecyclerView understands.
  */
-public class PhotosTabAdapter extends RecyclerView.Adapter<BaseItemHolder> {
+public class PhotosTabAdapter extends RecyclerView.Adapter<BaseViewHolder> {
 
-    private static final int ITEM_TYPE_DATE_HEADER = 0;
+    public static final int ITEM_TYPE_DATE_HEADER = 0;
     private static final int ITEM_TYPE_PHOTO = 1;
 
     public static final int COLUMN_COUNT = 3;
@@ -53,7 +53,7 @@
 
     @NonNull
     @Override
-    public BaseItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
         if (viewType == ITEM_TYPE_DATE_HEADER) {
             return new DateHeaderHolder(viewGroup.getContext(), viewGroup);
         }
@@ -62,7 +62,7 @@
     }
 
     @Override
-    public void onBindViewHolder(@NonNull BaseItemHolder itemHolder, int position) {
+    public void onBindViewHolder(@NonNull BaseViewHolder itemHolder, int position) {
         final Item item = getItem(position);
         itemHolder.itemView.setTag(item);
 
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index 7c344a1..8c2cad3 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -18,109 +18,59 @@
 import static com.android.providers.media.photopicker.ui.PhotosTabAdapter.COLUMN_COUNT;
 
 import android.os.Bundle;
-import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.providers.media.R;
-import com.android.providers.media.photopicker.PhotoPickerActivity;
 import com.android.providers.media.photopicker.data.model.Item;
-import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
 /**
  * Photos tab fragment for showing the photos
  */
-public class PhotosTabFragment extends Fragment {
-
-    private PickerViewModel mPickerViewModel;
-    private ImageLoader mImageLoader;
-
-    @Override
-    @NonNull
-    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        super.onCreateView(inflater, container, savedInstanceState);
-        return inflater.inflate(R.layout.fragment_photos_tab, container, false);
-    }
+public class PhotosTabFragment extends TabFragment {
 
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        mImageLoader = new ImageLoader(getContext());
-        RecyclerView photosList = view.findViewById(R.id.photo_list);
-        photosList.setHasFixedSize(true);
-        mPickerViewModel = new ViewModelProvider(requireActivity()).get(PickerViewModel.class);
-        final boolean canSelectMultiple = mPickerViewModel.canSelectMultiple();
-        if (canSelectMultiple) {
-            final Button addButton = view.findViewById(R.id.button_add);
-            addButton.setOnClickListener(v -> {
-                ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
-            });
-
-            final Button viewSelectedButton = view.findViewById(R.id.button_view_selected);
-            // Transition to PreviewFragment on clicking "View Selected".
-            viewSelectedButton.setOnClickListener(this::launchPreview);
-            final int bottomBarSize = (int) getResources().getDimension(
-                    R.dimen.picker_bottom_bar_size);
-
-            mPickerViewModel.getSelectedItems().observe(this, selectedItemList -> {
-                final View bottomBar = view.findViewById(R.id.picker_bottom_bar);
-                final int size = selectedItemList.size();
-                int dimen = 0;
-                if (size == 0) {
-                    bottomBar.setVisibility(View.GONE);
-                } else {
-                    bottomBar.setVisibility(View.VISIBLE);
-                    addButton.setText(getString(R.string.add) + " (" + size + ")" );
-                    dimen = bottomBarSize;
-                }
-                photosList.setPadding(0, 0, 0, dimen);
-            });
-        }
 
         final PhotosTabAdapter adapter = new PhotosTabAdapter(mPickerViewModel, mImageLoader,
                 this::onItemClick);
+
         mPickerViewModel.getItems().observe(this, itemList -> {
             adapter.updateItemList(itemList);
         });
+
         final GridLayoutManager layoutManager = new GridLayoutManager(getContext(), COLUMN_COUNT);
         final GridLayoutManager.SpanSizeLookup lookup = adapter.createSpanSizeLookup();
         if (lookup != null) {
             layoutManager.setSpanSizeLookup(lookup);
         }
-        photosList.setLayoutManager(layoutManager);
-        photosList.setAdapter(adapter);
+        final PhotosTabItemDecoration itemDecoration = new PhotosTabItemDecoration(
+                view.getContext());
+
+        mRecyclerView.setLayoutManager(layoutManager);
+        mRecyclerView.setAdapter(adapter);
+        mRecyclerView.addItemDecoration(itemDecoration);
     }
 
     private void onItemClick(@NonNull View view) {
-        final boolean isSelectedBefore = view.isSelected();
-
-        if (isSelectedBefore) {
-            mPickerViewModel.deleteSelectedItem((Item) view.getTag());
-        } else {
-            mPickerViewModel.addSelectedItem((Item) view.getTag());
-        }
-
         if (mPickerViewModel.canSelectMultiple()) {
+            final boolean isSelectedBefore = view.isSelected();
+
+            if (isSelectedBefore) {
+                mPickerViewModel.deleteSelectedItem((Item) view.getTag());
+            } else {
+                mPickerViewModel.addSelectedItem((Item) view.getTag());
+            }
             view.setSelected(!isSelectedBefore);
         } else {
+            mPickerViewModel.clearSelectedItems();
+            mPickerViewModel.addSelectedItem((Item) view.getTag());
             // Transition to PreviewFragment.
-            launchPreview(view);
+            PreviewFragment.show(getActivity().getSupportFragmentManager());
         }
     }
-
-    private void launchPreview(View view) {
-        getActivity().getSupportFragmentManager().beginTransaction()
-                .setReorderingAllowed(true)
-                .replace(R.id.fragment_container, PreviewFragment.class, null)
-                .commitNow();
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabItemDecoration.java b/src/com/android/providers/media/photopicker/ui/PhotosTabItemDecoration.java
new file mode 100644
index 0000000..1e4f63b
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabItemDecoration.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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.providers.media.photopicker.ui;
+
+import static com.android.providers.media.photopicker.ui.PhotosTabAdapter.ITEM_TYPE_DATE_HEADER;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.appcompat.widget.ViewUtils;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.providers.media.R;
+
+/**
+ * The ItemDecoration layouts offset to specific item views from the adapter's data set
+ * for the {@link RecyclerView} on Photos tab.
+ */
+public class PhotosTabItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final int mSpacing;
+
+    public PhotosTabItemDecoration(Context context) {
+        mSpacing = context.getResources().getDimensionPixelSize(R.dimen.picker_photo_item_spacing);
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+            RecyclerView.State state) {
+        final GridLayoutManager.LayoutParams lp =
+                (GridLayoutManager.LayoutParams) view.getLayoutParams();
+        final GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
+        final int column = lp.getSpanIndex();
+        final int spanCount = layoutManager.getSpanCount();
+
+        // The date header doesn't have spacing
+        if (lp.getSpanSize() == spanCount) {
+            outRect.set(0, 0, 0, 0);
+            return;
+        }
+
+        final int adapterPosition = parent.getChildAdapterPosition(view);
+        if (adapterPosition > column) {
+            final int itemViewType = parent.getAdapter().getItemViewType(
+                    adapterPosition - column - 1);
+            // if the above item is not a date header, add the top spacing
+            if (itemViewType != ITEM_TYPE_DATE_HEADER) {
+                outRect.top = mSpacing;
+            }
+        }
+
+        // column * ((1f / spanCount) * spacing)
+        final int start = column * mSpacing / spanCount;
+        // spacing - (column + 1) * ((1f / spanCount) * spacing)
+        final int end = mSpacing - (column + 1) * mSpacing / spanCount;
+
+        if (ViewUtils.isLayoutRtl(parent)) {
+            outRect.left = end;
+            outRect.right = start;
+        } else {
+            outRect.left = start;
+            outRect.right = end;
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index 2138935..085deb4 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -29,7 +29,7 @@
 /**
  * Adapter for Preview RecyclerView to preview all images and videos.
  */
-public class PreviewAdapter extends RecyclerView.Adapter<BaseItemHolder> {
+public class PreviewAdapter extends RecyclerView.Adapter<BaseViewHolder> {
 
     private static final int ITEM_TYPE_PHOTO = 1;
 
@@ -42,12 +42,12 @@
 
     @NonNull
     @Override
-    public BaseItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
         return new PreviewImageHolder(viewGroup.getContext(), viewGroup, mImageLoader);
     }
 
     @Override
-    public void onBindViewHolder(@NonNull BaseItemHolder photoHolder, int position) {
+    public void onBindViewHolder(@NonNull BaseViewHolder photoHolder, int position) {
         final Item item = getItem(position);
         photoHolder.itemView.setTag(item);
         photoHolder.bind();
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
index c427207..5854279 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
@@ -17,6 +17,7 @@
 package com.android.providers.media.photopicker.ui;
 
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -26,6 +27,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.viewpager2.widget.ViewPager2;
 
@@ -41,6 +43,8 @@
  * Displays a selected items in one up view. Supports deselecting items.
  */
 public class PreviewFragment extends Fragment {
+    private static String TAG = "PreviewFragment";
+
     private PickerViewModel mPickerViewModel;
     private ViewPager2 mViewPager;
     private PreviewAdapter mAdapter;
@@ -152,4 +156,17 @@
         selectButton.setSelected(isSelected);
         selectButton.setText(isSelected ? R.string.deselect : R.string.select);
     }
+
+    public static void show(FragmentManager fm) {
+        if (fm.isStateSaved()) {
+            Log.d(TAG, "Skip show preview fragment because state saved");
+            return;
+        }
+
+        final PreviewFragment fragment = new PreviewFragment();
+        fm.beginTransaction()
+                .setReorderingAllowed(true)
+                .replace(R.id.fragment_container, fragment, TAG)
+                .commitNow();
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewImageHolder.java b/src/com/android/providers/media/photopicker/ui/PreviewImageHolder.java
index 679652c..2295b85 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewImageHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewImageHolder.java
@@ -27,7 +27,7 @@
 /**
  * ViewHolder of a photo item within a RecyclerView.
  */
-public class PreviewImageHolder extends BaseItemHolder {
+public class PreviewImageHolder extends BaseViewHolder {
     private final ImageLoader mImageLoader;
     private final ImageView mImageView;
 
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
new file mode 100644
index 0000000..be75eda
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.providers.media.photopicker.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.providers.media.R;
+import com.android.providers.media.photopicker.PhotoPickerActivity;
+import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
+
+/**
+ * The base abstract Tab fragment
+ */
+public abstract class TabFragment extends Fragment {
+
+    protected PickerViewModel mPickerViewModel;
+    protected ImageLoader mImageLoader;
+    protected RecyclerView mRecyclerView;
+    private int mBottomBarSize;
+
+    @Override
+    @NonNull
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        super.onCreateView(inflater, container, savedInstanceState);
+        return inflater.inflate(R.layout.fragment_picker_tab, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        mImageLoader = new ImageLoader(getContext());
+        mRecyclerView = view.findViewById(R.id.photo_list);
+        mRecyclerView.setHasFixedSize(true);
+        mPickerViewModel = new ViewModelProvider(requireActivity()).get(PickerViewModel.class);
+        final boolean canSelectMultiple = mPickerViewModel.canSelectMultiple();
+        if (canSelectMultiple) {
+            final Button addButton = view.findViewById(R.id.button_add);
+            addButton.setOnClickListener(v -> {
+                ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
+            });
+
+            final Button viewSelectedButton = view.findViewById(R.id.button_view_selected);
+            // Transition to PreviewFragment on clicking "View Selected".
+            viewSelectedButton.setOnClickListener(v -> {
+                PreviewFragment.show(getActivity().getSupportFragmentManager());
+            });
+            mBottomBarSize = (int) getResources().getDimension(R.dimen.picker_bottom_bar_size);
+
+            mPickerViewModel.getSelectedItems().observe(this, selectedItemList -> {
+                final View bottomBar = view.findViewById(R.id.picker_bottom_bar);
+                final int size = selectedItemList.size();
+                int dimen = 0;
+                if (size == 0) {
+                    bottomBar.setVisibility(View.GONE);
+                } else {
+                    bottomBar.setVisibility(View.VISIBLE);
+                    addButton.setText(getString(R.string.add) + " (" + size + ")" );
+                    dimen = getBottomGapForRecyclerView(mBottomBarSize);
+                }
+                mRecyclerView.setPadding(0, 0, 0, dimen);
+            });
+        }
+    }
+
+    protected int getBottomGapForRecyclerView(int bottomBarSize) {
+        return bottomBarSize;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index 93b34a6..1743bb7 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -71,7 +71,7 @@
     }
 
     /**
-     * Add the selected ItemInfo.
+     * Add the selected Item.
      */
     public void addSelectedItem(Item item) {
         if (mSelectedItemList.getValue() == null) {
@@ -83,7 +83,18 @@
     }
 
     /**
-     * Delete the selected ItemInfo.
+     * Clear the selected Item list.
+     */
+    public void clearSelectedItems() {
+        if (mSelectedItemList.getValue() == null) {
+            return;
+        }
+        mSelectedItemList.getValue().clear();
+        mSelectedItemList.postValue(mSelectedItemList.getValue());
+    }
+
+    /**
+     * Delete the selected Item.
      */
     public void deleteSelectedItem(Item item) {
         if (mSelectedItemList.getValue() == null) {
diff --git a/tests/Android.bp b/tests/Android.bp
index 338afec..693fe69 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -130,6 +130,7 @@
         "androidx.test.espresso.core",
         "androidx.test.espresso.contrib",
         "androidx.test.core",
+        "androidx.arch.core_core-runtime",
     ],
 
     certificate: "media",
diff --git a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
index 0048327..2d3a14c 100644
--- a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
@@ -22,6 +22,7 @@
 
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.provider.MediaStore;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -61,11 +62,8 @@
         final String displayName = "123.png";
         final String volumeName = "primary";
         final long duration = 1000;
-        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+        final Item item = generateItem(id, mimeType, displayName, volumeName,
                 dateTaken, duration);
-        cursor.moveToFirst();
-
-        final Item item = new Item(cursor, UserId.CURRENT_USER);
 
         assertThat(item.isImage()).isTrue();
     }
@@ -78,11 +76,8 @@
         final String displayName = "123.png";
         final String volumeName = "primary";
         final long duration = 1000;
-        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+        final Item item = generateItem(id, mimeType, displayName, volumeName,
                 dateTaken, duration);
-        cursor.moveToFirst();
-
-        final Item item = new Item(cursor, UserId.CURRENT_USER);
 
         assertThat(item.isVideo()).isTrue();
     }
@@ -95,11 +90,8 @@
         final String displayName = "123.png";
         final String volumeName = "primary";
         final long duration = 1000;
-        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+        final Item item = generateItem(id, mimeType, displayName, volumeName,
                 dateTaken, duration);
-        cursor.moveToFirst();
-
-        final Item item = new Item(cursor, UserId.CURRENT_USER);
 
         assertThat(item.isGif()).isTrue();
     }
@@ -121,4 +113,21 @@
         cursor.addRow(new Object[] {id, mimeType, displayName, volumeName, dateTaken, duration});
         return cursor;
     }
+
+    /**
+     * Generate the {@link Item}
+     * @param id the id
+     * @param mimeType the mime type
+     * @param displayName the display name
+     * @param volumeName the volume name
+     * @param dateTaken the time of date taken
+     * @param duration the duration
+     * @return the Item
+     */
+    public static Item generateItem(long id, String mimeType,
+            String displayName, String volumeName, long dateTaken, long duration) {
+
+        return new Item(id, mimeType, displayName, volumeName, dateTaken, duration,
+                MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL, id));
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/InstantTaskExecutorRule.java b/tests/src/com/android/providers/media/photopicker/viewmodel/InstantTaskExecutorRule.java
new file mode 100644
index 0000000..b35075e
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/InstantTaskExecutorRule.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 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.providers.media.photopicker.viewmodel;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which executes each task synchronously.
+ *
+ * We can't refer it in prebuilt androidX library.
+ * Copied it from androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
+ */
+public class InstantTaskExecutorRule extends TestWatcher {
+    @Override
+    protected void starting(Description description) {
+        super.starting(description);
+        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+            @Override
+            public void executeOnDiskIO(Runnable runnable) {
+                runnable.run();
+            }
+
+            @Override
+            public void postToMainThread(Runnable runnable) {
+                runnable.run();
+            }
+
+            @Override
+            public boolean isMainThread() {
+                return true;
+            }
+        });
+    }
+
+    @Override
+    protected void finished(Description description) {
+        super.finished(description);
+        ArchTaskExecutor.getInstance().setDelegate(null);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
new file mode 100644
index 0000000..da0a1ac
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 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.providers.media.photopicker.viewmodel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.data.model.ItemTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class PickerViewModelTest {
+
+    private static final String FAKE_IMAGE_MIME_TYPE = "image/jpg";
+    private static final String FAKE_DISPLAY_NAME = "testDisplayName";
+
+    @Rule
+    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+    @Mock
+    private Application mApplication;
+
+    private PickerViewModel mPickerViewModel;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        final Context context = InstrumentationRegistry.getTargetContext();
+        when(mApplication.getApplicationContext()).thenReturn(context);
+        mPickerViewModel = new PickerViewModel(mApplication);
+    }
+
+    @Test
+    public void testAddSelectedItem() throws Exception {
+        final long id = 1;
+        final Item item = generateFakeImageItem(id);
+
+        mPickerViewModel.addSelectedItem(item);
+
+        final Item selectedItem = mPickerViewModel.getSelectedItems().getValue().get(
+                item.getContentUri());
+
+        assertThat(selectedItem.getId()).isEqualTo(item.getId());
+        assertThat(selectedItem.getDateTaken()).isEqualTo(item.getDateTaken());
+        assertThat(selectedItem.getDisplayName()).isEqualTo(item.getDisplayName());
+        assertThat(selectedItem.getMimeType()).isEqualTo(item.getMimeType());
+        assertThat(selectedItem.getVolumeName()).isEqualTo(item.getVolumeName());
+        assertThat(selectedItem.getDuration()).isEqualTo(item.getDuration());
+    }
+
+    @Test
+    public void testDeleteSelectedItem() throws Exception {
+        final long id = 1;
+        final Item item = generateFakeImageItem(id);
+        Map<Uri, Item> selectedItems = mPickerViewModel.getSelectedItems().getValue();
+
+        assertThat(selectedItems.size()).isEqualTo(0);
+
+        mPickerViewModel.addSelectedItem(item);
+
+        selectedItems = mPickerViewModel.getSelectedItems().getValue();
+        assertThat(selectedItems.size()).isEqualTo(1);
+
+        mPickerViewModel.deleteSelectedItem(item);
+
+        selectedItems = mPickerViewModel.getSelectedItems().getValue();
+        assertThat(selectedItems.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testClearSelectedItem() throws Exception {
+        final long id1 = 1;
+        final Item item1 = generateFakeImageItem(id1);
+        final long id2 = 2;
+        final Item item2 = generateFakeImageItem(id2);
+        Map<Uri, Item> selectedItems = mPickerViewModel.getSelectedItems().getValue();
+
+        assertThat(selectedItems.size()).isEqualTo(0);
+
+        mPickerViewModel.addSelectedItem(item1);
+        mPickerViewModel.addSelectedItem(item2);
+
+        selectedItems = mPickerViewModel.getSelectedItems().getValue();
+        assertThat(selectedItems.size()).isEqualTo(2);
+
+        mPickerViewModel.clearSelectedItems();
+
+        selectedItems = mPickerViewModel.getSelectedItems().getValue();
+        assertThat(selectedItems.size()).isEqualTo(0);
+    }
+
+    private static Item generateFakeImageItem(long id) {
+        return ItemTest.generateItem(id, FAKE_IMAGE_MIME_TYPE, FAKE_DISPLAY_NAME + id,
+                MediaStore.VOLUME_EXTERNAL, 12345678l, 1000l);
+    }
+}
+