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();