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);
+ }
+}
+