Separate call history list by time periods

Bug: 136092811
Test: Manually + unit tests
Change-Id: I0d812d7ff229026861f249d6b82bac20e5e32b39
diff --git a/res/layout/header_item.xml b/res/layout/header_item.xml
new file mode 100644
index 0000000..e7343f8
--- /dev/null
+++ b/res/layout/header_item.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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/title"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"/>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4b48392..0726f27 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -65,6 +65,16 @@
     <!-- Toolbar title for tabbed pages -->
     <string name="default_toolbar_title" translatable="false"></string>
 
+    <!-- Headers for call history -->
+    <!-- Header in call log to group calls from the current day. [CHAR LIMIT=30] -->
+    <string name="call_log_header_today">Today</string>
+
+    <!-- Header in call log to group calls from the previous day. [CHAR LIMIT=30] -->
+    <string name="call_log_header_yesterday">Yesterday</string>
+
+    <!-- Header in call log to group calls from before yesterday. [CHAR LIMIT=30] -->
+    <string name="call_log_header_older">Older</string>
+
     <!-- Button to add start choosing a contact to add as a new favorite [CHAR_LIMIT=50] -->
     <string name="add_favorite_button">Add a favorite</string>
     <!-- Error message shown when on the favorites page without any favorites added [CHAR_LIMIT=80] -->
diff --git a/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java
index 5dc4dc4..bef716b 100644
--- a/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java
+++ b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java
@@ -18,13 +18,14 @@
 
 import android.app.Application;
 import android.text.format.DateUtils;
+
 import androidx.annotation.NonNull;
 import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
+
 import com.android.car.dialer.livedata.CallHistoryLiveData;
 import com.android.car.dialer.livedata.HeartBeatLiveData;
 import com.android.car.dialer.ui.common.UiCallLogLiveData;
-import com.android.car.dialer.ui.common.entity.UiCallLog;
 import com.android.car.telephony.common.InMemoryPhoneBook;
 
 import java.util.List;
@@ -46,7 +47,7 @@
     /**
      * Returns the live data for call history list.
      */
-    public LiveData<List<UiCallLog>> getCallHistory() {
+    public LiveData<List<Object>> getCallHistory() {
         return mUiCallLogLiveData;
     }
 }
diff --git a/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java b/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java
index 41e7e41..71427c5 100644
--- a/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java
+++ b/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java
@@ -20,11 +20,13 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.dialer.R;
 import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.entity.HeaderViewHolder;
 import com.android.car.dialer.ui.common.entity.UiCallLog;
 import com.android.car.telephony.common.Contact;
 
@@ -32,15 +34,28 @@
 import java.util.List;
 
 /** Adapter for call history list. */
-public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> {
+public class CallLogAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 
     private static final String TAG = "CD.CallLogAdapter";
 
+    /** IntDef for the different groups of calllog lists separated by time periods. */
+    @IntDef({
+            EntryType.TYPE_HEADER,
+            EntryType.TYPE_CALLLOG,
+    })
+    private @interface EntryType {
+        /** Entry typre is header. */
+        int TYPE_HEADER = 1;
+
+        /** Entry type is calllog. */
+        int TYPE_CALLLOG = 2;
+    }
+
     public interface OnShowContactDetailListener {
         void onShowContactDetail(Contact contact);
     }
 
-    private List<UiCallLog> mUiCallLogs = new ArrayList<>();
+    private List<Object> mUiCallLogs = new ArrayList<>();
     private Context mContext;
     private CallLogAdapter.OnShowContactDetailListener mOnShowContactDetailListener;
 
@@ -50,7 +65,10 @@
         mOnShowContactDetailListener = onShowContactDetailListener;
     }
 
-    public void setUiCallLogs(@NonNull List<UiCallLog> uiCallLogs) {
+    /**
+     * Sets calllogs.
+     */
+    public void setUiCallLogs(@NonNull List<Object> uiCallLogs) {
         L.d(TAG, "setUiCallLogs: %d", uiCallLogs.size());
         mUiCallLogs.clear();
         mUiCallLogs.addAll(uiCallLogs);
@@ -59,20 +77,42 @@
 
     @NonNull
     @Override
-    public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        if (viewType == EntryType.TYPE_CALLLOG) {
+            View rootView = LayoutInflater.from(mContext)
+                    .inflate(R.layout.call_history_list_item, parent, false);
+            return new CallLogViewHolder(rootView, mOnShowContactDetailListener);
+        }
+
         View rootView = LayoutInflater.from(mContext)
-                .inflate(R.layout.call_history_list_item, parent, false);
-        return new CallLogViewHolder(rootView, mOnShowContactDetailListener);
+                .inflate(R.layout.header_item, parent, false);
+        return new HeaderViewHolder(rootView);
     }
 
     @Override
-    public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
-        holder.onBind(mUiCallLogs.get(position));
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        if (holder instanceof  CallLogViewHolder) {
+            ((CallLogViewHolder) holder).onBind((UiCallLog) mUiCallLogs.get(position));
+        } else {
+            ((HeaderViewHolder) holder).setHeaderTitle((String) mUiCallLogs.get(position));
+        }
     }
 
     @Override
-    public void onViewRecycled(@NonNull CallLogViewHolder holder) {
-        holder.onRecycle();
+    @EntryType
+    public int getItemViewType(int position) {
+        if (mUiCallLogs.get(position) instanceof UiCallLog) {
+            return EntryType.TYPE_CALLLOG;
+        } else {
+            return EntryType.TYPE_HEADER;
+        }
+    }
+
+    @Override
+    public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
+        if (holder instanceof CallLogViewHolder) {
+            ((CallLogViewHolder) holder).onRecycle();
+        }
     }
 
     @Override
@@ -80,4 +120,3 @@
         return mUiCallLogs.size();
     }
 }
-
diff --git a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
index 428af4b..9a5e846 100644
--- a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
+++ b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
@@ -39,14 +39,16 @@
 import com.google.common.base.Splitter;
 
 import java.util.ArrayList;
+import java.util.Calendar;
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Represents a list of call logs for UI representation. This live data get data source from both
- * call log and contact list. It also refresh itself on the relative time in the body text.
+ * Represents a list of {@link UiCallLog}s and label {@link String}s for UI representation.
+ * This live data gets data source from both call log and contact list. It also refresh
+ * itself on the relative time in the body text.
  */
-public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> {
+public class UiCallLogLiveData extends MediatorLiveData<List<Object>> {
     private static final String TAG = "CD.UiCallLogLiveData";
 
     private static final String TYPE_AND_RELATIVE_TIME_JOINER = ", ";
@@ -76,32 +78,35 @@
 
     private void updateRelativeTime() {
         boolean hasChanged = false;
-        List<UiCallLog> uiCallLogs = getValue();
+        List<Object> uiCallLogs = getValue();
         if (uiCallLogs == null) {
             return;
         }
-        for (UiCallLog uiCallLog : uiCallLogs) {
-            String secondaryText = uiCallLog.getText();
-            List<String> splittedSecondaryText = Splitter.on(
-                    TYPE_AND_RELATIVE_TIME_JOINER).splitToList(secondaryText);
+        for (Object object : uiCallLogs) {
+            if (object instanceof UiCallLog) {
+                UiCallLog uiCallLog = (UiCallLog) object;
+                String secondaryText = uiCallLog.getText();
+                List<String> splittedSecondaryText = Splitter.on(
+                        TYPE_AND_RELATIVE_TIME_JOINER).splitToList(secondaryText);
 
-            String oldRelativeTime;
-            String type = "";
-            if (splittedSecondaryText.size() == 1) {
-                oldRelativeTime = splittedSecondaryText.get(0);
-            } else if (splittedSecondaryText.size() == 2) {
-                type = splittedSecondaryText.get(0);
-                oldRelativeTime = splittedSecondaryText.get(1);
-            } else {
-                L.w(TAG, "secondary text format is incorrect: %s", secondaryText);
-                return;
-            }
+                String oldRelativeTime;
+                String type = "";
+                if (splittedSecondaryText.size() == 1) {
+                    oldRelativeTime = splittedSecondaryText.get(0);
+                } else if (splittedSecondaryText.size() == 2) {
+                    type = splittedSecondaryText.get(0);
+                    oldRelativeTime = splittedSecondaryText.get(1);
+                } else {
+                    L.w(TAG, "secondary text format is incorrect: %s", secondaryText);
+                    return;
+                }
 
-            String newRelativeTime = getRelativeTime(uiCallLog.getMostRecentCallEndTimestamp());
-            if (!oldRelativeTime.equals(newRelativeTime)) {
-                String newSecondaryText = getSecondaryText(type, newRelativeTime);
-                uiCallLog.setText(newSecondaryText);
-                hasChanged = true;
+                String newRelativeTime = getRelativeTime(uiCallLog.getMostRecentCallEndTimestamp());
+                if (!oldRelativeTime.equals(newRelativeTime)) {
+                    String newSecondaryText = getSecondaryText(type, newRelativeTime);
+                    uiCallLog.setText(newSecondaryText);
+                    hasChanged = true;
+                }
             }
         }
 
@@ -110,14 +115,21 @@
         }
     }
 
-    private List<UiCallLog> convert(List<PhoneCallLog> phoneCallLogs) {
+    private List<Object> convert(List<PhoneCallLog> phoneCallLogs) {
         if (phoneCallLogs == null) {
             return Collections.emptyList();
         }
-        List<UiCallLog> uiCallLogs = new ArrayList<>();
+        List<Object> uiCallLogs = new ArrayList<>();
+        String preHeader = null;
 
         InMemoryPhoneBook inMemoryPhoneBook = InMemoryPhoneBook.get();
         for (PhoneCallLog phoneCallLog : phoneCallLogs) {
+            String header = getHeader(phoneCallLog.getLastCallEndTimestamp());
+            if (preHeader == null || (!header.equals(preHeader))) {
+                uiCallLogs.add(header);
+            }
+            preHeader = header;
+
             String number = phoneCallLog.getPhoneNumberString();
             String relativeTime = getRelativeTime(phoneCallLog.getLastCallEndTimestamp());
             if (TelecomUtils.isVoicemailNumber(mContext, number)) {
@@ -148,6 +160,9 @@
 
             uiCallLogs.add(uiCallLog);
         }
+        L.i(TAG, "phoneCallLog size: %d, uiCallLog size: %d",
+                phoneCallLogs.size(), uiCallLogs.size());
+
         return uiCallLogs;
     }
 
@@ -170,4 +185,22 @@
     private CharSequence getType(@Nullable PhoneNumber phoneNumber) {
         return phoneNumber != null ? phoneNumber.getReadableLabel(mContext.getResources()) : "";
     }
+
+    private String getHeader(long calllogTime) {
+        // Calllog times are acquired before getting currentTime, so calllogTime is always
+        // less than currentTime
+        if (DateUtils.isToday(calllogTime)) {
+            return mContext.getResources().getString(R.string.call_log_header_today);
+        }
+
+        Calendar callLogCalender = Calendar.getInstance();
+        callLogCalender.setTimeInMillis(calllogTime);
+        callLogCalender.add(Calendar.DAY_OF_YEAR, 1);
+
+        if (DateUtils.isToday(callLogCalender.getTimeInMillis())) {
+            return mContext.getResources().getString(R.string.call_log_header_yesterday);
+        }
+
+        return mContext.getResources().getString(R.string.call_log_header_older);
+    }
 }
diff --git a/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java b/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java
new file mode 100644
index 0000000..1c5bed1
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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.car.dialer.ui.common.entity;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+
+/**
+ * {@link RecyclerView.ViewHolder} for headers to display divider.
+ */
+public class HeaderViewHolder extends RecyclerView.ViewHolder {
+
+    private TextView mHeaderTitle;
+
+    public HeaderViewHolder(@NonNull View itemView) {
+        super(itemView);
+        mHeaderTitle = itemView.findViewById(R.id.title);
+    }
+
+    /**
+     * Sets header title.
+     */
+    public void setHeaderTitle(String headerTitle) {
+        mHeaderTitle.setText(headerTitle);
+    }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java
index 6338362..73553cd 100644
--- a/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java
+++ b/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java
@@ -26,6 +26,7 @@
 import android.widget.TextView;
 
 import androidx.lifecycle.MutableLiveData;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.apps.common.widget.PagedRecyclerView;
 import com.android.car.dialer.CarDialerRobolectricTestRunner;
@@ -34,6 +35,7 @@
 import com.android.car.dialer.livedata.CallHistoryLiveData;
 import com.android.car.dialer.telecom.UiCallManager;
 import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.dialer.ui.common.entity.HeaderViewHolder;
 import com.android.car.dialer.ui.common.entity.UiCallLog;
 import com.android.car.dialer.widget.CallTypeIconsView;
 import com.android.car.telephony.common.InMemoryPhoneBook;
@@ -56,13 +58,16 @@
 @Config(shadows = {ShadowAndroidViewModelFactory.class})
 @RunWith(CarDialerRobolectricTestRunner.class)
 public class CallHistoryFragmentTest {
+    private static final String HEADER = "TODAY";
     private static final String PHONE_NUMBER = "6502530000";
     private static final String UI_CALLOG_TITLE = "TITLE";
     private static final String UI_CALLOG_TEXT = "TEXT";
-    private static final long TIME_STAMP_1 = 5000;
-    private static final long TIME_STAMP_2 = 10000;
+    private static final long TIME_STAMP_1 = System.currentTimeMillis();
+    private static final long TIME_STAMP_2 = System.currentTimeMillis() - 10000;
 
-    private CallLogViewHolder mViewHolder;
+    private CallHistoryFragment mCallHistoryFragment;
+    private RecyclerView.ViewHolder mCalllogViewHolder;
+    private RecyclerView.ViewHolder mHeaderViewHolder;
     @Mock
     private UiCallManager mMockUiCallManager;
     @Mock
@@ -84,20 +89,22 @@
         UiCallLog uiCallLog = new UiCallLog(UI_CALLOG_TITLE, UI_CALLOG_TEXT, PHONE_NUMBER, mMockUri,
                 Arrays.asList(record1, record2));
 
-        MutableLiveData<List<UiCallLog>> callLog = new MutableLiveData<>();
-        callLog.setValue(Arrays.asList(uiCallLog));
+        MutableLiveData<List<Object>> callLog = new MutableLiveData<>();
+        callLog.setValue(Arrays.asList(HEADER, uiCallLog));
         ShadowAndroidViewModelFactory.add(CallHistoryViewModel.class, mMockCallHistoryViewModel);
         when(mMockCallHistoryViewModel.getCallHistory()).thenReturn(callLog);
 
-        CallHistoryFragment callHistoryFragment = CallHistoryFragment.newInstance();
+        mCallHistoryFragment = CallHistoryFragment.newInstance();
         FragmentTestActivity mFragmentTestActivity = Robolectric.buildActivity(
                 FragmentTestActivity.class).create().resume().get();
-        mFragmentTestActivity.setFragment(callHistoryFragment);
+        mFragmentTestActivity.setFragment(mCallHistoryFragment);
 
-        PagedRecyclerView recyclerView = callHistoryFragment.getView().findViewById(R.id.list_view);
+        PagedRecyclerView recyclerView = mCallHistoryFragment.getView()
+                .findViewById(R.id.list_view);
         // set up layout for recyclerView
         recyclerView.layoutBothForTesting(0, 0, 100, 1000);
-        mViewHolder = (CallLogViewHolder) recyclerView.findViewHolderForLayoutPosition(0);
+        mHeaderViewHolder = recyclerView.findViewHolderForLayoutPosition(0);
+        mCalllogViewHolder = recyclerView.findViewHolderForLayoutPosition(1);
     }
 
     @After
@@ -106,10 +113,21 @@
     }
 
     @Test
-    public void testUI() {
-        TextView titleView = mViewHolder.itemView.findViewById(R.id.title);
-        TextView textView = mViewHolder.itemView.findViewById(R.id.text);
-        CallTypeIconsView callTypeIconsView = mViewHolder.itemView.findViewById(
+    public void testHeaderViewHolder() {
+        assertThat(mHeaderViewHolder instanceof HeaderViewHolder).isTrue();
+
+        TextView title = ((HeaderViewHolder) mHeaderViewHolder).itemView.findViewById(R.id.title);
+        assertThat(title.getText()).isEqualTo(HEADER);
+    }
+
+    @Test
+    public void testCalllogViewHolder() {
+        assertThat(mCalllogViewHolder instanceof CallLogViewHolder).isTrue();
+
+        CallLogViewHolder viewHolder = (CallLogViewHolder) mCalllogViewHolder;
+        TextView titleView = viewHolder.itemView.findViewById(R.id.title);
+        TextView textView = viewHolder.itemView.findViewById(R.id.text);
+        CallTypeIconsView callTypeIconsView = viewHolder.itemView.findViewById(
                 R.id.call_type_icons);
 
         assertThat(titleView.getText()).isEqualTo(UI_CALLOG_TITLE);
@@ -122,7 +140,8 @@
 
     @Test
     public void testClick_placeCall() {
-        View callButton = mViewHolder.itemView.findViewById(R.id.call_action_id);
+        View callButton = ((CallLogViewHolder) mCalllogViewHolder).itemView
+                .findViewById(R.id.call_action_id);
         assertThat(callButton.hasOnClickListeners()).isTrue();
 
         callButton.performClick();