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