Add empty view in PhotoPicker
- Use the device default theme
- Support empty view in AutoFitRecyclerView
- Add empty view in TabFragment
Test: video on the bug
Test: atest NoItemsTest
Bug: 195913683
Change-Id: Ifedcc3a3deb529ce613e98fdbc8c8391e7fe57c6
diff --git a/res/drawable/ic_artwork_camera.xml b/res/drawable/ic_artwork_camera.xml
new file mode 100644
index 0000000..dc22c49
--- /dev/null
+++ b/res/drawable/ic_artwork_camera.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="120dp"
+ android:height="80dp"
+ android:viewportWidth="120"
+ android:viewportHeight="80">
+ <path
+ android:pathData="M96,14m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M8,14h104v64h-104z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M16,10h16v4h-16z"
+ android:fillColor="#5F6368"/>
+ <path
+ android:strokeWidth="1"
+ android:pathData="M60,48m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
+ android:fillColor="#DADCE0"
+ android:strokeColor="#5F6368"/>
+ <path
+ android:pathData="M60,48m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
+ android:fillColor="#BDC1C6"/>
+</vector>
diff --git a/res/layout/fragment_picker_tab.xml b/res/layout/fragment_picker_tab.xml
index f9e03a9..59cea08 100644
--- a/res/layout/fragment_picker_tab.xml
+++ b/res/layout/fragment_picker_tab.xml
@@ -20,6 +20,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <LinearLayout
+ android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="80dp"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_artwork_camera"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:id="@+id/empty_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/picker_empty_text_margin"
+ android:gravity="center_horizontal"
+ android:text="@string/picker_photos_empty_message"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/picker_empty_text_size"
+ style="?android:attr/textAppearanceListItem"/>
+
+ </LinearLayout>
+
<com.android.providers.media.photopicker.ui.AutoFitRecyclerView
android:id="@+id/picker_tab_recyclerview"
android:layout_width="match_parent"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index e85c2ba..88fbe53 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -59,6 +59,9 @@
<dimen name="picker_drag_margin_top">8dp</dimen>
<dimen name="picker_drag_margin_bottom">12dp</dimen>
+ <dimen name="picker_empty_text_margin">20dp</dimen>
+ <dimen name="picker_empty_text_size">18sp</dimen>
+
<!-- PhotoPicker Preview -->
<dimen name="preview_buttons_padding_horizontal">16dp</dimen>
<dimen name="preview_deselect_padding_start">2dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9258e71..288a54e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -97,6 +97,11 @@
<!-- Recent header for PhotoPicker. [CHAR LIMIT=50] -->
<string name="recent">Recent</string>
+ <!-- The message for empty message on Photos tab in PhotoPicker when the item count is zero. [CHAR LIMIT=NONE] -->
+ <string name="picker_photos_empty_message">No photos or videos</string>
+ <!-- The message for empty message on Albums tab in PhotoPicker when the item count is zero. [CHAR LIMIT=NONE] -->
+ <string name="picker_albums_empty_message">No albums</string>
+
<!-- PhotoPicker view selected action text. [CHAR LIMIT=80] -->
<string name="picker_view_selected">View selected</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index e421305..0a15288 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -86,17 +86,21 @@
<item name="android:layout_height">@dimen/picker_profile_dialog_icon_height</item>
</style>
- <style name="PickerDefaultTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
+ <style name="PickerDefaultTheme" parent="android:style/Theme.DeviceDefault.DayNight">
<!-- Color section -->
<item name="android:colorAccent">@color/picker_primary_color</item>
<item name="android:colorBackground">@color/picker_background_color</item>
<!-- System | Widget section -->
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:navigationBarColor">?android:attr/colorBackground</item>
<item name="android:statusBarColor">@android:color/transparent</item>
- <item name="android:navigationBarColor">?android:colorBackground</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
- <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <style name="PickerMaterialTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
</style>
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index c092d30..4b176dc 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -102,6 +102,11 @@
@Override
public void onCreate(Bundle savedInstanceState) {
+ // We use the device default theme as the base theme. Apply the material them for the
+ // material components. We use force "false" here, only values that are not already defined
+ // in the base theme will be copied.
+ getTheme().applyStyle(R.style.PickerMaterialTheme, /* force */ false);
+
super.onCreate(savedInstanceState);
if (!isPhotoPickerEnabled()) {
diff --git a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
index eafb874..8834f9d 100644
--- a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
@@ -82,6 +82,11 @@
return bottomBarSize + mBottomBarGap;
}
+ @Override
+ protected String getEmptyMessage() {
+ return getString(R.string.picker_albums_empty_message);
+ }
+
/**
* Create the albums tab fragment and add it into the FragmentManager
*
diff --git a/src/com/android/providers/media/photopicker/ui/AutoFitRecyclerView.java b/src/com/android/providers/media/photopicker/ui/AutoFitRecyclerView.java
index 526d090..81bd941 100644
--- a/src/com/android/providers/media/photopicker/ui/AutoFitRecyclerView.java
+++ b/src/com/android/providers/media/photopicker/ui/AutoFitRecyclerView.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.View;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
@@ -31,6 +32,50 @@
private int mColumnWidth = -1;
private int mMinimumSpanCount = 2;
private boolean mIsGridLayout;
+ private View mEmptyView;
+ private AdapterDataObserver mAdapterDataObserver = new AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ checkIsEmpty();
+ }
+
+ /**
+ * If the user triggers {@link RecyclerView.Adapter#notifyItemInserted(int)}, this method
+ * will be triggered. We also need to check whether the dataset is empty or not to decide
+ * the visibility of the empty view.
+ */
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ super.onItemRangeInserted(positionStart, itemCount);
+ checkIsEmpty();
+ }
+
+ /**
+ * If the user triggers {@link RecyclerView.Adapter#notifyItemRemoved(int)}, this method
+ * will be triggered. We also need to check whether the dataset is empty or not to decide
+ * the visibility of the empty view.
+ */
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ super.onItemRangeRemoved(positionStart, itemCount);
+ checkIsEmpty();
+ }
+
+ private void checkIsEmpty() {
+ if (mEmptyView == null) {
+ return;
+ }
+
+ if (getAdapter().getItemCount() == 0) {
+ mEmptyView.setVisibility(VISIBLE);
+ setVisibility(GONE);
+ } else {
+ mEmptyView.setVisibility(GONE);
+ setVisibility(VISIBLE);
+ }
+ }
+ };
public AutoFitRecyclerView(Context context) {
super(context);
@@ -62,6 +107,22 @@
}
}
+ @Override
+ public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
+ super.setAdapter(adapter);
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(mAdapterDataObserver);
+ }
+ mAdapterDataObserver.onChanged();
+ }
+
+ /**
+ * Set the empty view. If the empty view is not null, when the item count is zero, it is shown.
+ */
+ public void setEmptyView(@Nullable View emptyView) {
+ mEmptyView = emptyView;
+ }
+
public void setColumnWidth(int columnWidth) {
mColumnWidth = columnWidth;
}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index de45fe9..e9b37db 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -140,6 +140,11 @@
}
}
+ @Override
+ protected String getEmptyMessage() {
+ return getString(R.string.picker_photos_empty_message);
+ }
+
private void onItemClick(@NonNull View view) {
if (mSelection.canSelectMultiple()) {
final boolean isSelectedBefore = view.isSelected();
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index a35bb11..1ab6229 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -22,6 +22,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -53,6 +54,7 @@
private int mBottomBarSize;
private ExtendedFloatingActionButton mProfileButton;
+ private TextView mEmptyTextView;
private UserIdManager mUserIdManager;
private boolean mHideProfileButton;
@@ -70,6 +72,9 @@
mImageLoader = new ImageLoader(getContext());
mRecyclerView = view.findViewById(R.id.picker_tab_recyclerview);
+ View emptyView = view.findViewById(android.R.id.empty);
+ mRecyclerView.setEmptyView(emptyView);
+ mEmptyTextView = emptyView.findViewById(R.id.empty_text_view);
mRecyclerView.setHasFixedSize(true);
mPickerViewModel = new ViewModelProvider(requireActivity()).get(PickerViewModel.class);
mSelection = mPickerViewModel.getSelection();
@@ -120,6 +125,7 @@
public void onResume() {
super.onResume();
+ mEmptyTextView.setText(getEmptyMessage());
updateProfileButtonAsync();
}
@@ -232,6 +238,13 @@
return bottomBarSize;
}
+ /**
+ * Get the messages to show on empty view
+ */
+ protected String getEmptyMessage() {
+ return getString(R.string.picker_photos_empty_message);
+ }
+
protected void hideProfileButton(boolean hide) {
mHideProfileButton = hide;
if (hide) {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
new file mode 100644
index 0000000..7d78605
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.espresso;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withParent;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.not;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import com.android.providers.media.R;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class NoItemsTest extends PhotoPickerBaseTest {
+
+ @BeforeClass
+ public static void setupClass() throws Exception {
+ PhotoPickerBaseTest.setupClass();
+ deleteFiles(/* invalidateMediaStore */ true);
+ }
+
+ /**
+ * Simple test to check we are able to launch PhotoPickerActivity with no items
+ */
+ @Test
+ public void testNoItems_Simple() {
+ try (ActivityScenario<PhotoPickerTestActivity> scenario = ActivityScenario.launch(
+ PhotoPickerBaseTest.getSingleSelectionIntent())) {
+ final int pickerTabRecyclerViewId = R.id.picker_tab_recyclerview;
+
+ onView(withId(pickerTabRecyclerViewId)).check(matches(not(isDisplayed())));
+ onView(withId(android.R.id.empty)).check(matches(isDisplayed()));
+ onView(withText(R.string.picker_photos_empty_message)).check(matches(isDisplayed()));
+
+ // Goto Albums page
+ onView(allOf(withText(R.string.picker_albums), withParent(withId(R.id.chip_container))))
+ .perform(click());
+
+ onView(withId(pickerTabRecyclerViewId)).check(matches(not(isDisplayed())));
+ onView(withId(android.R.id.empty)).check(matches(isDisplayed()));
+ onView(withText(R.string.picker_albums_empty_message)).check(matches(isDisplayed()));
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
index cd6a6f1..cd995a8 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
@@ -35,6 +35,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.not;
import android.app.Activity;
@@ -61,6 +62,7 @@
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
onView(withId(R.id.fragment_container)).check(matches(isDisplayed()));
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ onView(withId(android.R.id.empty)).check(matches(not(isDisplayed())));
onView(withContentDescription("Navigate up")).perform(click());
assertThat(mRule.getScenario().getResult().getResultCode()).isEqualTo(
Activity.RESULT_CANCELED);
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index 14a4881..76f0c78 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -163,14 +163,28 @@
@AfterClass
public static void destroyClass() {
- IMAGE_1_FILE.delete();
- IMAGE_2_FILE.delete();
- VIDEO_FILE.delete();
+ deleteFiles(/* invalidateMediaStore */ false);
InstrumentationRegistry.getInstrumentation()
.getUiAutomation().dropShellPermissionIdentity();
}
+ protected static void deleteFiles(boolean invalidateMediaStore) {
+ deleteFile(IMAGE_1_FILE, invalidateMediaStore);
+ deleteFile(IMAGE_2_FILE, invalidateMediaStore);
+ deleteFile(VIDEO_FILE, invalidateMediaStore);
+ }
+
+ private static void deleteFile(File file, boolean invalidateMediaStore) {
+ file.delete();
+ if (invalidateMediaStore) {
+ final Uri uri = MediaStore.scanFile(getIsolatedContext().getContentResolver(), file);
+ assertThat(uri).isNull();
+ // Force picker db sync for that db operation
+ MediaStore.waitForIdle(getIsolatedContext().getContentResolver());
+ }
+ }
+
private static void createFiles() throws Exception {
long timeNow = System.currentTimeMillis();
// Create files and change dateModified so that we can predict the recyclerView item