Support date header and Recent in PhotosTabFragment

- Add DateHeaderHolder
- Update items for date header and recent
- Support ITEM_TYPE_DATE_HEADER in PhotosTabAdapter
- Add DateTimeUtils

Bug: 189732217
Test: Manual. videos on the bug.
Test: atest DateTimeUtilsTest
Test: atest ItemTest
Change-Id: I3e8b7582c743e12548fd44e71e8a5beb079f561f
Merged-In: I3e8b7582c743e12548fd44e71e8a5beb079f561f
(cherry picked from commit 1b6db2d497508fece1bfd4a194af7577bb8940ad)
diff --git a/res/layout/item_date_header.xml b/res/layout/item_date_header.xml
new file mode 100644
index 0000000..fd6f826
--- /dev/null
+++ b/res/layout/item_date_header.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 20121 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/date_header_title"
+          android:layout_width="match_parent"
+          android:layout_height="@dimen/picker_date_header_height"
+          android:padding="@dimen/picker_date_header_padding"
+          android:textAppearance="@style/PickerDateHeader"/>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index e593c25..8714efc 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -25,6 +25,7 @@
     <color name="picker_primary_color">#1A73E8</color>
     <color name="picker_background_color">@android:color/white</color>
     <color name="picker_highlight_color">#E8F0FE</color>
+    <color name="picker_date_header_text_color">#3C4043</color>
 
     <!-- PhotoPicker Preview -->
     <color name="preview_default_blue">#8AB4F8</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 92812ee..28a7b60 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -31,6 +31,8 @@
     <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>
 
     <!-- PhotoPicker Preview -->
     <dimen name="preview_buttons_margin_horizontal">16dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index be3c0a5..22998c6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -88,6 +88,9 @@
     <!-- Select button for PhotoPicker. [CHAR LIMIT=30] -->
     <string name="select">Select</string>
 
+    <!-- Recent header for PhotoPicker. [CHAR LIMIT=50] -->
+    <string name="recent">Recent</string>
+
     <!-- PhotoPicker view selected action text. [CHAR LIMIT=80] -->
     <string name="picker_view_selected">View selected</string>
 
diff --git a/res/values/styles_text.xml b/res/values/styles_text.xml
new file mode 100644
index 0000000..710631a
--- /dev/null
+++ b/res/values/styles_text.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="PickerDateHeader" parent="@android:style/TextAppearance.Material.Title">
+        <item name="android:textColor">@color/picker_date_header_text_color</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+</resources>
diff --git a/src/com/android/providers/media/photopicker/DateTimeUtils.java b/src/com/android/providers/media/photopicker/DateTimeUtils.java
new file mode 100644
index 0000000..f7e0f00
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/DateTimeUtils.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import static android.icu.text.DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE;
+import static android.icu.text.RelativeDateTimeFormatter.Style.LONG;
+
+import android.content.Context;
+import android.icu.text.RelativeDateTimeFormatter;
+import android.icu.text.RelativeDateTimeFormatter.Direction;
+import android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit;
+import android.icu.util.ULocale;
+import android.text.format.DateUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.TextStyle;
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+
+/**
+ * Provide the utility methods to handle date time.
+ */
+public class DateTimeUtils {
+
+    /**
+     * Formats a time according to the local conventions.
+     *
+     * If the difference of the date between the time and now is zero, show
+     * "Today".
+     * If the difference is 1, show "Yesterday".
+     * If the difference is less than 7, show the weekday. E.g. "Sunday".
+     * Otherwise, show the weekday and the date. E.g. "Sat, Jun 5".
+     * If they have different years, show the weekday, the date and the year.
+     * E.g. "Sat, Jun 5, 2021"
+     *
+     * @param context the context
+     * @param when    the time to be formatted. The unit is in milliseconds
+     *                since January 1, 1970 00:00:00.0 UTC.
+     * @return the formatted string
+     */
+    public static String getDateTimeString(Context context, long when) {
+        // Get the system time zone
+        final ZoneId zoneId = ZoneId.systemDefault();
+        final LocalDate nowDate = LocalDate.now(zoneId);
+
+        return getDateTimeString(context, when, nowDate);
+    }
+
+    @VisibleForTesting
+    static String getDateTimeString(Context context, long when, LocalDate nowDate) {
+        // Get the system time zone
+        final ZoneId zoneId = ZoneId.systemDefault();
+        final LocalDate whenDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(when),
+                zoneId).toLocalDate();
+
+        final long dayDiff = ChronoUnit.DAYS.between(whenDate, nowDate);
+        if (dayDiff == 0) {
+            return getTodayString();
+        } else if (dayDiff == 1) {
+            return getYesterdayString();
+        } else if (dayDiff > 0 && dayDiff < 7) {
+            return whenDate.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault());
+        } else {
+            int flags = DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
+                    | DateUtils.FORMAT_ABBREV_ALL;
+            if (whenDate.getYear() == nowDate.getYear()) {
+                flags |= DateUtils.FORMAT_NO_YEAR;
+            } else {
+                flags |= DateUtils.FORMAT_SHOW_YEAR;
+            }
+            return DateUtils.formatDateTime(context, when, flags);
+        }
+    }
+
+    /**
+     * It is borrowed from {@link DateUtils} since it is no official API yet.
+     *
+     * @param oneMillis the first time. The unit is in milliseconds since
+     *                  January 1, 1970 00:00:00.0 UTC.
+     * @param twoMillis the second time. The unit is in milliseconds since
+     *                  January 1, 1970 00:00:00.0 UTC.
+     * @return True, the date is the same. Otherwise, return false.
+     */
+    public static boolean isSameDate(long oneMillis, long twoMillis) {
+        // Get the system time zone
+        final ZoneId zoneId = ZoneId.systemDefault();
+
+        final Instant oneInstant = Instant.ofEpochMilli(oneMillis);
+        final LocalDateTime oneLocalDateTime = LocalDateTime.ofInstant(oneInstant, zoneId);
+
+        final Instant twoInstant = Instant.ofEpochMilli(twoMillis);
+        final LocalDateTime twoLocalDateTime = LocalDateTime.ofInstant(twoInstant, zoneId);
+
+        return (oneLocalDateTime.getYear() == twoLocalDateTime.getYear())
+                && (oneLocalDateTime.getMonthValue() == twoLocalDateTime.getMonthValue())
+                && (oneLocalDateTime.getDayOfMonth() == twoLocalDateTime.getDayOfMonth());
+    }
+
+    @VisibleForTesting
+    static String getTodayString() {
+        final RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(
+                ULocale.getDefault(), null, LONG, CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE);
+        return fmt.format(Direction.THIS, AbsoluteUnit.DAY);
+    }
+
+    @VisibleForTesting
+    static String getYesterdayString() {
+        final RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(
+                ULocale.getDefault(), null, LONG, CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE);
+        return fmt.format(Direction.LAST, AbsoluteUnit.DAY);
+    }
+}
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 be2910d..b91efe6 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -67,6 +67,7 @@
     private boolean mIsImage;
     private boolean mIsVideo;
     private boolean mIsGif;
+    private boolean mIsDate;
 
     private Item() {}
 
@@ -90,6 +91,10 @@
         return mIsGif;
     }
 
+    public boolean isDate() {
+        return mIsDate;
+    }
+
     public Uri getContentUri() {
         return mUri;
     }
@@ -116,8 +121,21 @@
 
     public static Item fromCursor(Cursor cursor, UserId userId) {
         assert(cursor != null);
-        final Item info = new Item(cursor, userId);
-        return info;
+        final Item item = new Item(cursor, userId);
+        return item;
+    }
+
+    /**
+     * Return the date item. If dateTaken is 0, it is a recent item.
+     * @param dateTaken the time of date taken. The unit is in milliseconds since
+     *                  January 1, 1970 00:00:00.0 UTC.
+     * @return the item with date type
+     */
+    public static Item createDateItem(long dateTaken) {
+        final Item item = new Item();
+        item.mIsDate = true;
+        item.mDateTaken = dateTaken;
+        return item;
     }
 
     /**
diff --git a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
new file mode 100644
index 0000000..b5122a1
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
@@ -0,0 +1,44 @@
+/*
+ * 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.content.Context;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.providers.media.R;
+import com.android.providers.media.photopicker.DateTimeUtils;
+import com.android.providers.media.photopicker.data.model.Item;
+
+/**
+ * ViewHolder of a date header within a RecyclerView.
+ */
+public class DateHeaderHolder extends BaseItemHolder {
+    private TextView mTitle;
+    public DateHeaderHolder(Context context, ViewGroup parent) {
+        super(context, parent, R.layout.item_date_header);
+        mTitle = itemView.findViewById(R.id.date_header_title);
+    }
+
+    @Override
+    public void bind() {
+        final Item item = (Item) itemView.getTag();
+        final long dateTaken = item.getDateTaken();
+        if (dateTaken == 0) {
+            mTitle.setText(R.string.recent);
+        } else {
+            mTitle.setText(DateTimeUtils.getDateTimeString(itemView.getContext(), dateTaken));
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index 9538a84..b239d52 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -20,6 +20,7 @@
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.providers.media.photopicker.data.model.Item;
@@ -33,6 +34,7 @@
  */
 public class PhotosTabAdapter extends RecyclerView.Adapter<BaseItemHolder> {
 
+    private static final int ITEM_TYPE_DATE_HEADER = 0;
     private static final int ITEM_TYPE_PHOTO = 1;
 
     public static final int COLUMN_COUNT = 3;
@@ -52,20 +54,26 @@
     @NonNull
     @Override
     public BaseItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+        if (viewType == ITEM_TYPE_DATE_HEADER) {
+            return new DateHeaderHolder(viewGroup.getContext(), viewGroup);
+        }
         return new PhotoGridHolder(viewGroup.getContext(), viewGroup, mImageLoader,
                 mPickerViewModel.canSelectMultiple());
     }
 
     @Override
-    public void onBindViewHolder(@NonNull BaseItemHolder photoHolder, int position) {
+    public void onBindViewHolder(@NonNull BaseItemHolder itemHolder, int position) {
         final Item item = getItem(position);
-        photoHolder.itemView.setTag(item);
-        photoHolder.itemView.setOnClickListener(mOnClickListener);
-        final boolean isItemSelected =
-                mPickerViewModel.getSelectedItems().getValue().containsKey(
-                        item.getContentUri());
-        photoHolder.itemView.setSelected(isItemSelected);
-        photoHolder.bind();
+        itemHolder.itemView.setTag(item);
+
+        if (getItemViewType(position) == ITEM_TYPE_PHOTO) {
+            itemHolder.itemView.setOnClickListener(mOnClickListener);
+            final boolean isItemSelected =
+                    mPickerViewModel.getSelectedItems().getValue().containsKey(
+                            item.getContentUri());
+            itemHolder.itemView.setSelected(isItemSelected);
+        }
+        itemHolder.bind();
     }
 
     @Override
@@ -75,6 +83,9 @@
 
     @Override
     public int getItemViewType(int position) {
+        if (getItem(position).isDate()) {
+            return ITEM_TYPE_DATE_HEADER;
+        }
         return ITEM_TYPE_PHOTO;
     }
 
@@ -86,4 +97,19 @@
         mItemList = itemList;
         notifyDataSetChanged();
     }
+
+    public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
+        return new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                // Make layout whitespace span the grid. This has the effect of breaking
+                // grid rows whenever layout whitespace is encountered.
+                if (getItemViewType(position) == ITEM_TYPE_DATE_HEADER) {
+                    return COLUMN_COUNT;
+                } else {
+                    return 1;
+                }
+            }
+        };
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index 30a8cd5..7c344a1 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -92,6 +92,10 @@
             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);
     }
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index c0195e4..93b34a6 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -27,6 +27,7 @@
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
+import com.android.providers.media.photopicker.DateTimeUtils;
 import com.android.providers.media.photopicker.data.ItemsProvider;
 import com.android.providers.media.photopicker.data.UserIdManager;
 import com.android.providers.media.photopicker.data.model.Item;
@@ -43,6 +44,8 @@
 public class PickerViewModel extends AndroidViewModel {
     public static final String TAG = "PhotoPicker";
 
+    private static final int RECENT_MINIMUM_COUNT = 12;
+
     private MutableLiveData<List<Item>> mItemList;
     private MutableLiveData<Map<Uri, Item>> mSelectedItemList = new MutableLiveData<>();
     private final ItemsProvider mItemsProvider;
@@ -116,10 +119,28 @@
             return items;
         }
 
+        int recentSize = 0;
+        long currentDateTaken = 0;
+        // add Recent date header
+        items.add(Item.createDateItem(0));
         while (cursor.moveToNext()) {
             // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it
             //  here again.
-            items.add(Item.fromCursor(cursor, userId));
+            final Item item = Item.fromCursor(cursor, userId);
+            final long dateTaken = item.getDateTaken();
+            // the minimum count of items in recent is not reached
+            if (recentSize < RECENT_MINIMUM_COUNT) {
+                recentSize++;
+                currentDateTaken = dateTaken;
+            }
+
+            // The date taken of these two images are not on the
+            // same day, add the new date header.
+            if (!DateTimeUtils.isSameDate(currentDateTaken, dateTaken)) {
+                items.add(Item.createDateItem(dateTaken));
+                currentDateTaken = dateTaken;
+            }
+            items.add(item);
         }
 
         Log.d(TAG, "Loaded " + items.size() + " items for user " + userId.toString());
diff --git a/tests/src/com/android/providers/media/photopicker/DateTimeUtilsTest.java b/tests/src/com/android/providers/media/photopicker/DateTimeUtilsTest.java
new file mode 100644
index 0000000..062d3d4
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/DateTimeUtilsTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+
+@RunWith(AndroidJUnit4.class)
+public class DateTimeUtilsTest {
+
+    private Context mContext;
+    private static LocalDate FAKE_DATE =
+            LocalDate.of(2020 /* year */, 7 /* month */, 7 /* dayOfMonth */);
+    private static long FAKE_TIME =
+            FAKE_DATE.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void testGetDateTimeString_today() throws Exception {
+        final String result = DateTimeUtils.getDateTimeString(mContext, FAKE_TIME, FAKE_DATE);
+
+        assertThat(result).isEqualTo(DateTimeUtils.getTodayString());
+    }
+
+    @Test
+    public void testGetDateTimeString_yesterday() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusDays(1);
+        final long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+
+        assertThat(result).isEqualTo(DateTimeUtils.getYesterdayString());
+    }
+
+    @Test
+    public void testGetDateTimeString_weekday() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusDays(3);
+        final long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+
+        assertThat(result).isEqualTo("Saturday");
+    }
+
+    @Test
+    public void testGetDateTimeString_weekdayAndDate() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusMonths(1);
+        final long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+
+        assertThat(result).isEqualTo("Sun, Jun 7");
+    }
+
+    @Test
+    public void testGetDateTimeString_weekdayDateAndYear() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusYears(1);
+        long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+
+        assertThat(result).isEqualTo("Sun, Jul 7, 2019");
+    }
+
+    @Test
+    public void testIsSameDay_differentYear_false() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusYears(1);
+        long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        assertThat(DateTimeUtils.isSameDate(when, FAKE_TIME)).isFalse();
+    }
+
+    @Test
+    public void testIsSameDay_differentMonth_false() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusMonths(1);
+        final long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        assertThat(DateTimeUtils.isSameDate(when, FAKE_TIME)).isFalse();
+    }
+
+    @Test
+    public void testIsSameDay_differentDay_false() throws Exception {
+        final LocalDate whenDate = FAKE_DATE.minusDays(1);
+        final long when = generateDateTimeMillis(whenDate.getYear(), whenDate.getMonthValue(),
+                whenDate.getDayOfMonth());
+
+        assertThat(DateTimeUtils.isSameDate(when, FAKE_TIME)).isFalse();
+    }
+
+    @Test
+    public void testIsSameDay_true() throws Exception {
+        assertThat(DateTimeUtils.isSameDate(FAKE_TIME, FAKE_TIME)).isTrue();
+    }
+
+    private static long generateDateTimeMillis(int year, int month, int dayOfMonth) {
+        final LocalDate result = LocalDate.of(year, month, dayOfMonth);
+        return result.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
+    }
+}
+
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 6c74140..0048327 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
@@ -39,7 +39,6 @@
         final String displayName = "123.png";
         final String volumeName = "primary";
         final long duration = 1000;
-
         final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
                 dateTaken, duration);
         cursor.moveToFirst();
@@ -54,6 +53,67 @@
         assertThat(item.getDuration()).isEqualTo(duration);
     }
 
+    @Test
+    public void testIsImage() {
+        final long id = 1;
+        final long dateTaken = 12345678l;
+        final String mimeType = "image/png";
+        final String displayName = "123.png";
+        final String volumeName = "primary";
+        final long duration = 1000;
+        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+                dateTaken, duration);
+        cursor.moveToFirst();
+
+        final Item item = new Item(cursor, UserId.CURRENT_USER);
+
+        assertThat(item.isImage()).isTrue();
+    }
+
+    @Test
+    public void testIsVideo() {
+        final long id = 1;
+        final long dateTaken = 12345678l;
+        final String mimeType = "video/mpeg";
+        final String displayName = "123.png";
+        final String volumeName = "primary";
+        final long duration = 1000;
+        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+                dateTaken, duration);
+        cursor.moveToFirst();
+
+        final Item item = new Item(cursor, UserId.CURRENT_USER);
+
+        assertThat(item.isVideo()).isTrue();
+    }
+
+    @Test
+    public void testIsGif() {
+        final long id = 1;
+        final long dateTaken = 12345678l;
+        final String mimeType = "image/gif";
+        final String displayName = "123.png";
+        final String volumeName = "primary";
+        final long duration = 1000;
+        final Cursor cursor = generateCursorForItem(id, mimeType, displayName, volumeName,
+                dateTaken, duration);
+        cursor.moveToFirst();
+
+        final Item item = new Item(cursor, UserId.CURRENT_USER);
+
+        assertThat(item.isGif()).isTrue();
+    }
+
+    @Test
+    public void testCreateDateItem() {
+        final long dateTaken = 12345678l;
+
+        final Item item = Item.createDateItem(dateTaken);
+
+        assertThat(item.getDateTaken()).isEqualTo(dateTaken);
+        assertThat(item.isDate()).isTrue();
+    }
+
     private static Cursor generateCursorForItem(long id, String mimeType,
             String displayName, String volumeName, long dateTaken, long duration) {
         final MatrixCursor cursor = new MatrixCursor(