summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk10
-rw-r--r--res/drawable/recent_lists_footer_background.xml24
-rw-r--r--res/layout/recents_list_footer.xml33
-rw-r--r--res/values/animation_constants.xml12
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapter.java78
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java231
-rw-r--r--src/com/android/dialer/calllog/CallLogGroupBuilder.java1
-rw-r--r--src/com/android/dialer/calllog/GroupingListAdapter.java490
-rw-r--r--src/com/android/dialer/list/ListsFragment.java1
-rw-r--r--src/com/android/dialerbind/ObjectFactory.java12
-rw-r--r--tests/src/com/android/dialer/calllog/CallLogAdapterTest.java2
-rw-r--r--tests/src/com/android/dialer/calllog/CallLogFragmentTest.java41
-rw-r--r--tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java311
13 files changed, 868 insertions, 378 deletions
diff --git a/Android.mk b/Android.mk
index 696caf133..b029189e8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -22,19 +22,21 @@ LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
LOCAL_AAPT_FLAGS := \
--auto-add-overlay \
+ --extra-packages android.support.v7.recyclerview \
--extra-packages com.android.incallui \
--extra-packages com.android.contacts.common \
--extra-packages com.android.phone.common
LOCAL_JAVA_LIBRARIES := telephony-common
LOCAL_STATIC_JAVA_LIBRARIES := \
- com.android.services.telephony.common \
- com.android.vcard \
android-common \
- guava \
+ android-ex-variablespeed \
android-support-v13 \
android-support-v4 \
- android-ex-variablespeed \
+ android-support-v7-recyclerview \
+ com.android.services.telephony.common \
+ com.android.vcard \
+ guava \
libphonenumber
LOCAL_REQUIRED_MODULES := libvariablespeed
diff --git a/res/drawable/recent_lists_footer_background.xml b/res/drawable/recent_lists_footer_background.xml
deleted file mode 100644
index b5029afcb..000000000
--- a/res/drawable/recent_lists_footer_background.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
- ~ Copyright (C) 2014 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
- -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="?android:attr/colorControlHighlight">
- <!-- Mask to constrain the ripple to the bounds of the view. -->
- <item android:id="@android:id/mask">
- <color android:color="@android:color/white" />
- </item>
-</ripple>
diff --git a/res/layout/recents_list_footer.xml b/res/layout/recents_list_footer.xml
deleted file mode 100644
index 3a56cbe16..000000000
--- a/res/layout/recents_list_footer.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 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.
--->
-
-<!-- Text field and possibly soft menu button above the keypad where
- the digits are displayed. -->
-
-<TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/recents_list_footer"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="20dp"
- android:paddingBottom="20dp"
- android:gravity="center"
- android:fontFamily="@string/view_full_call_history_font_family"
- android:textStyle="bold"
- android:textColor="@color/dialtacts_secondary_text_color"
- android:textSize="14sp"
- android:text="@string/recents_footer_text"
- android:background="@drawable/recent_lists_footer_background" />
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index b8b2a59f4..4e4bc36e1 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -27,16 +27,4 @@
<dimen name="min_swipe">0dip</dimen>
<dimen name="min_vert">10dip</dimen>
<dimen name="min_lock">20dip</dimen>
-
- <!-- Expand/collapse of call log entry duration. -->
- <integer name="call_log_expand_collapse_duration">200</integer>
-
- <!-- Start delay for the fade in of the call log actions. -->
- <integer name="call_log_actions_fade_start">150</integer>
-
- <!-- Duration of the fade in of the call log actions. -->
- <integer name="call_log_actions_fade_in_duration">50</integer>
-
- <!-- Duration of the fade out of the call log actions. -->
- <integer name="call_log_actions_fade_out_duration">20</integer>
</resources>
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index f5a3f62ed..fd6b37b8c 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -36,7 +36,6 @@ import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.common.widget.GroupingListAdapter;
import com.android.contacts.common.util.UriUtils;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.PhoneCallDetailsHelper;
@@ -96,12 +95,6 @@ public class CallLogAdapter extends GroupingListAdapter
protected ContactInfoCache mContactInfoCache;
/**
- * Tracks the call log row which was previously expanded. Used so that the closure of a
- * previously expanded call log entry can be animated on rebind.
- */
- private long mPreviouslyExpanded = NONE_EXPANDED;
-
- /**
* Tracks the currently expanded call log row.
*/
private long mCurrentlyExpanded = NONE_EXPANDED;
@@ -160,7 +153,7 @@ public class CallLogAdapter extends GroupingListAdapter
@Override
public void onClick(View v) {
final View callLogItem = (View) v.getParent().getParent();
- handleRowExpanded(callLogItem, true /* animate */, false /* forceExpand */);
+ handleRowExpanded(callLogItem, false /* forceExpand */);
}
};
@@ -177,8 +170,7 @@ public class CallLogAdapter extends GroupingListAdapter
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
- handleRowExpanded(host, false /* animate */,
- true /* forceExpand */);
+ handleRowExpanded(host, true /* forceExpand */);
}
return super.onRequestSendAccessibilityEvent(host, child, event);
}
@@ -193,15 +185,16 @@ public class CallLogAdapter extends GroupingListAdapter
return true;
}
- public CallLogAdapter(Context context, CallFetcher callFetcher,
- ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener,
+ public CallLogAdapter(
+ Context context,
+ CallFetcher callFetcher,
+ ContactInfoHelper contactInfoHelper,
OnReportButtonClickListener onReportButtonClickListener) {
super(context);
mContext = context;
mCallFetcher = callFetcher;
mContactInfoHelper = contactInfoHelper;
- mCallItemExpandedListener = callItemExpandedListener;
mOnReportButtonClickListener = onReportButtonClickListener;
@@ -322,7 +315,7 @@ public class CallLogAdapter extends GroupingListAdapter
* @param c the cursor pointing to the entry in the call log
* @param count the number of entries in the current item, greater than 1 if it is a group
*/
- private void bindView(View callLogItemView, Cursor c, int count) {
+ public void bindView(View callLogItemView, Cursor c, int count) {
callLogItemView.setAccessibilityDelegate(mAccessibilityDelegate);
final CallLogListItemViews views = (CallLogListItemViews) callLogItemView.getTag();
@@ -500,14 +493,10 @@ public class CallLogAdapter extends GroupingListAdapter
private boolean toggleExpansion(long rowId) {
if (rowId == mCurrentlyExpanded) {
// Collapsing currently expanded row.
- mPreviouslyExpanded = NONE_EXPANDED;
mCurrentlyExpanded = NONE_EXPANDED;
-
return false;
} else {
// Expanding a row (collapsing current expanded one).
-
- mPreviouslyExpanded = mCurrentlyExpanded;
mCurrentlyExpanded = rowId;
return true;
}
@@ -551,22 +540,6 @@ public class CallLogAdapter extends GroupingListAdapter
}
/**
- * Bind a call log entry view for testing purposes. Also inflates the action view stub so
- * unit tests can access the buttons contained within.
- *
- * @param view The current call log row.
- * @param context The current context.
- * @param cursor The cursor to bind from.
- */
- @VisibleForTesting
- void bindViewForTest(View view, Context context, Cursor cursor) {
- bindStandAloneView(view, context, cursor);
- CallLogListItemViews views = CallLogListItemViews.fromView(context, view);
- views.inflateActionViewStub(mOnReportButtonClickListener, mActionListener,
- mPhoneNumberUtilsWrapper, mCallLogViewsHelper);
- }
-
- /**
* Sets whether processing of requests for contact details should be enabled.
*
* This method should be called in tests to disable such processing of requests when not
@@ -650,11 +623,10 @@ public class CallLogAdapter extends GroupingListAdapter
* Manages the state changes for the UI interaction where a call log row is expanded.
*
* @param view The view that was tapped
- * @param animate Whether or not to animate the expansion/collapse
* @param forceExpand Whether or not to force the call log row into an expanded state regardless
* of its previous state
*/
- private void handleRowExpanded(View view, boolean animate, boolean forceExpand) {
+ private void handleRowExpanded(View view, boolean forceExpand) {
final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
if (forceExpand && isExpanded(views.rowId)) {
@@ -663,38 +635,20 @@ public class CallLogAdapter extends GroupingListAdapter
// Hide or show the actions view.
boolean expanded = toggleExpansion(views.rowId);
+ expandItem(views, expanded);
+ }
+ /**
+ * @param views The view holder for the item to expand or collapse.
+ * @param expand {@code true} to expand the item, {@code false} otherwise.
+ */
+ public void expandItem(CallLogListItemViews views, boolean expand) {
// Trigger loading of the viewstub and visual expand or collapse.
views.expandOrCollapseActions(
- expanded,
+ expand,
mOnReportButtonClickListener,
mActionListener,
mPhoneNumberUtilsWrapper,
mCallLogViewsHelper);
-
- // Animate the expansion or collapse.
- if (mCallItemExpandedListener != null) {
- if (animate) {
- mCallItemExpandedListener.onItemExpanded(view);
- }
-
- // Animate the collapse of the previous item if it is still visible on screen.
- if (mPreviouslyExpanded != NONE_EXPANDED) {
- View previousItem = mCallItemExpandedListener.getViewForCallId(mPreviouslyExpanded);
-
- if (previousItem != null) {
- ((CallLogListItemViews) previousItem.getTag()).expandOrCollapseActions(
- false /* isExpanded */,
- mOnReportButtonClickListener,
- mActionListener,
- mPhoneNumberUtilsWrapper,
- mCallLogViewsHelper);
- if (animate) {
- mCallItemExpandedListener.onItemExpanded(previousItem);
- }
- }
- mPreviouslyExpanded = NONE_EXPANDED;
- }
- }
}
}
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 77565766e..d69c2ed7e 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -62,13 +62,10 @@ import java.util.List;
*/
public class CallLogFragment extends ListFragment
implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
- CallLogAdapter.CallFetcher,
- CallLogAdapter.CallItemExpandedListener {
+ CallLogAdapter.CallFetcher {
private static final String TAG = "CallLogFragment";
private static final String REPORT_DIALOG_TAG = "report_dialog";
- private String mReportDialogNumber;
- private boolean mIsReportDialogShowing;
/**
* ID of the empty loader to defer other fragments.
@@ -78,9 +75,6 @@ public class CallLogFragment extends ListFragment
private static final String KEY_FILTER_TYPE = "filter_type";
private static final String KEY_LOG_LIMIT = "log_limit";
private static final String KEY_DATE_LIMIT = "date_limit";
- private static final String KEY_SHOW_FOOTER = "show_footer";
- private static final String KEY_IS_REPORT_DIALOG_SHOWING = "is_report_dialog_showing";
- private static final String KEY_REPORT_DIALOG_NUMBER = "report_dialog_number";
private CallLogAdapter mAdapter;
private CallLogQueryHandler mCallLogQueryHandler;
@@ -91,21 +85,15 @@ public class CallLogFragment extends ListFragment
private VoicemailStatusHelper mVoicemailStatusHelper;
private View mStatusMessageView;
+ private View mEmptyListView;
private TextView mStatusMessageText;
private TextView mStatusMessageAction;
private KeyguardManager mKeyguardManager;
- private View mFooterView;
private boolean mEmptyLoaderRunning;
private boolean mCallLogFetched;
private boolean mVoicemailStatusFetched;
- private float mExpandedItemTranslationZ;
- private int mFadeInDuration;
- private int mFadeInStartDelay;
- private int mFadeOutDuration;
- private int mExpandCollapseDuration;
-
private final Handler mHandler = new Handler();
private class CustomContentObserver extends ContentObserver {
@@ -138,9 +126,6 @@ public class CallLogFragment extends ListFragment
// the date filter are included. If zero, no date-based filtering occurs.
private long mDateLimit = 0;
- // Whether or not to show the Show call history footer view
- private boolean mHasFooterView = false;
-
public CallLogFragment() {
this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
}
@@ -184,15 +169,11 @@ public class CallLogFragment extends ListFragment
mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
- mHasFooterView = state.getBoolean(KEY_SHOW_FOOTER, mHasFooterView);
- mIsReportDialogShowing = state.getBoolean(KEY_IS_REPORT_DIALOG_SHOWING,
- mIsReportDialogShowing);
- mReportDialogNumber = state.getString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber);
}
String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
- new ContactInfoHelper(getActivity(), currentCountryIso), this, this);
+ new ContactInfoHelper(getActivity(), currentCountryIso), this);
setListAdapter(mAdapter);
mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
this, mLogLimit);
@@ -206,22 +187,6 @@ public class CallLogFragment extends ListFragment
Status.CONTENT_URI, true, mVoicemailStatusObserver);
setHasOptionsMenu(true);
fetchCalls();
-
- mExpandedItemTranslationZ =
- getResources().getDimension(R.dimen.call_log_expanded_translation_z);
- mFadeInDuration = getResources().getInteger(R.integer.call_log_actions_fade_in_duration);
- mFadeInStartDelay = getResources().getInteger(R.integer.call_log_actions_fade_start);
- mFadeOutDuration = getResources().getInteger(R.integer.call_log_actions_fade_out_duration);
- mExpandCollapseDuration = getResources().getInteger(
- R.integer.call_log_expand_collapse_duration);
-
- if (mIsReportDialogShowing) {
- DialogFragment df = ObjectFactory.getReportDialogFragment(mReportDialogNumber);
- if (df != null) {
- df.setTargetFragment(this, 0);
- df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
- }
- }
}
/** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@@ -235,8 +200,13 @@ public class CallLogFragment extends ListFragment
mAdapter.changeCursor(cursor);
// This will update the state of the "Clear call log" menu item.
getActivity().invalidateOptionsMenu();
+
+ final ListView listView = getListView();
+ boolean showListView = cursor.getCount() > 0;
+ listView.setVisibility(showListView ? View.VISIBLE : View.GONE);
+ mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
+
if (mScrollToTop) {
- final ListView listView = getListView();
// The smooth-scroll animation happens over a fixed time period.
// As a result, if it scrolls through a large portion of the list,
// each frame will jump so far from the previous one that the user
@@ -309,9 +279,8 @@ public class CallLogFragment extends ListFragment
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- getListView().setEmptyView(view.findViewById(R.id.empty_list_view));
+ mEmptyListView = view.findViewById(R.id.empty_list_view);
getListView().setItemsCanFocus(true);
- maybeAddFooterView();
updateEmptyMessage(mCallTypeFilter);
}
@@ -404,9 +373,6 @@ public class CallLogFragment extends ListFragment
outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
outState.putInt(KEY_LOG_LIMIT, mLogLimit);
outState.putLong(KEY_DATE_LIMIT, mDateLimit);
- outState.putBoolean(KEY_SHOW_FOOTER, mHasFooterView);
- outState.putBoolean(KEY_IS_REPORT_DIALOG_SHOWING, mIsReportDialogShowing);
- outState.putString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber);
}
@Override
@@ -431,7 +397,7 @@ public class CallLogFragment extends ListFragment
+ filterType);
}
DialerUtils.configureEmptyListView(
- getListView().getEmptyView(), R.drawable.empty_call_log, messageId, getResources());
+ mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
}
CallLogAdapter getAdapter() {
@@ -494,180 +460,7 @@ public class CallLogFragment extends ListFragment
}
}
- /**
- * Enables/disables the showing of the view full call history footer
- *
- * @param hasFooterView Whether or not to show the footer
- */
- public void setHasFooterView(boolean hasFooterView) {
- mHasFooterView = hasFooterView;
- maybeAddFooterView();
- }
-
- /**
- * Determine whether or not the footer view should be added to the listview. If getView()
- * is null, which means onCreateView hasn't been called yet, defer the addition of the footer
- * until onViewCreated has been called.
- */
- private void maybeAddFooterView() {
- if (!mHasFooterView || getView() == null) {
- return;
- }
-
- if (mFooterView == null) {
- mFooterView = getActivity().getLayoutInflater().inflate(
- R.layout.recents_list_footer, getListView(), false);
- mFooterView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- ((HostInterface) getActivity()).showCallHistory();
- }
- });
- }
-
- final ListView listView = getListView();
- listView.removeFooterView(mFooterView);
- listView.addFooterView(mFooterView);
-
- ViewUtil.addBottomPaddingToListViewForFab(listView, getResources());
- }
-
- @Override
- public void onItemExpanded(final View view) {
- final int startingHeight = view.getHeight();
- final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
- final ViewTreeObserver observer = getListView().getViewTreeObserver();
- observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- // We don't want to continue getting called for every draw.
- if (observer.isAlive()) {
- observer.removeOnPreDrawListener(this);
- }
- // Calculate some values to help with the animation.
- final int endingHeight = view.getHeight();
- final int distance = Math.abs(endingHeight - startingHeight);
- final int baseHeight = Math.min(endingHeight, startingHeight);
- final boolean isExpand = endingHeight > startingHeight;
-
- // Set the views back to the start state of the animation
- view.getLayoutParams().height = startingHeight;
- if (!isExpand) {
- viewHolder.actionsView.setVisibility(View.VISIBLE);
- }
- viewHolder.expandVoicemailTranscriptionView(!isExpand);
-
- // Set up the fade effect for the action buttons.
- if (isExpand) {
- // Start the fade in after the expansion has partly completed, otherwise it
- // will be mostly over before the expansion completes.
- viewHolder.actionsView.setAlpha(0f);
- viewHolder.actionsView.animate()
- .alpha(1f)
- .setStartDelay(mFadeInStartDelay)
- .setDuration(mFadeInDuration)
- .start();
- } else {
- viewHolder.actionsView.setAlpha(1f);
- viewHolder.actionsView.animate()
- .alpha(0f)
- .setDuration(mFadeOutDuration)
- .start();
- }
- view.requestLayout();
-
- // Set up the animator to animate the expansion and shadow depth.
- ValueAnimator animator = isExpand ? ValueAnimator.ofFloat(0f, 1f)
- : ValueAnimator.ofFloat(1f, 0f);
-
- // Figure out how much scrolling is needed to make the view fully visible.
- final Rect localVisibleRect = new Rect();
- view.getLocalVisibleRect(localVisibleRect);
- final int scrollingNeeded = localVisibleRect.top > 0 ? -localVisibleRect.top
- : view.getMeasuredHeight() - localVisibleRect.height();
- final ListView listView = getListView();
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-
- private int mCurrentScroll = 0;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- Float value = (Float) animator.getAnimatedValue();
-
- // For each value from 0 to 1, animate the various parts of the layout.
- view.getLayoutParams().height = (int) (value * distance + baseHeight);
- float z = mExpandedItemTranslationZ * value;
- viewHolder.callLogEntryView.setTranslationZ(z);
- view.setTranslationZ(z); // WAR
- view.requestLayout();
-
- if (isExpand) {
- if (listView != null) {
- int scrollBy = (int) (value * scrollingNeeded) - mCurrentScroll;
- listView.smoothScrollBy(scrollBy, /* duration = */ 0);
- mCurrentScroll += scrollBy;
- }
- }
- }
- });
- // Set everything to their final values when the animation's done.
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
-
- if (!isExpand) {
- viewHolder.actionsView.setVisibility(View.GONE);
- } else {
- // This seems like it should be unnecessary, but without this, after
- // navigating out of the activity and then back, the action view alpha
- // is defaulting to the value (0) at the start of the expand animation.
- viewHolder.actionsView.setAlpha(1);
- }
- viewHolder.expandVoicemailTranscriptionView(isExpand);
- }
- });
-
- animator.setDuration(mExpandCollapseDuration);
- animator.start();
-
- // Return false so this draw does not occur to prevent the final frame from
- // being drawn for the single frame before the animations start.
- return false;
- }
- });
- }
-
- /**
- * Retrieves the call log view for the specified call Id. If the view is not currently
- * visible, returns null.
- *
- * @param callId The call Id.
- * @return The call log view.
- */
- @Override
- public View getViewForCallId(long callId) {
- ListView listView = getListView();
-
- int firstPosition = listView.getFirstVisiblePosition();
- int lastPosition = listView.getLastVisiblePosition();
-
- for (int position = 0; position <= lastPosition - firstPosition; position++) {
- View view = listView.getChildAt(position);
-
- if (view != null) {
- final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
- if (viewHolder != null && viewHolder.rowId == callId) {
- return view;
- }
- }
- }
-
- return null;
- }
-
public void onBadDataReported(String number) {
- mIsReportDialogShowing = false;
if (number == null) {
return;
}
@@ -680,8 +473,6 @@ public class CallLogFragment extends ListFragment
if (df != null) {
df.setTargetFragment(this, 0);
df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
- mReportDialogNumber = number;
- mIsReportDialogShowing = true;
}
}
}
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 1f11e1e60..0826aeb4a 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -21,7 +21,6 @@ import android.provider.CallLog.Calls;
import android.telephony.PhoneNumberUtils;
import android.text.format.Time;
-import com.android.common.widget.GroupingListAdapter;
import com.android.contacts.common.util.DateUtils;
import com.android.contacts.common.util.PhoneNumberHelper;
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
new file mode 100644
index 000000000..78955492e
--- /dev/null
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2015 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.dialer.calllog;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.android.contacts.common.testing.NeededForTesting;
+
+/**
+ * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
+ *
+ * The list has three types of elements: stand-alone, group header and group child. Groups are
+ * collapsible and collapsed by default. This is used by the call log to group related entries.
+ */
+abstract class GroupingListAdapter extends BaseAdapter {
+
+ private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
+ private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
+ private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL;
+ private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L;
+ private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
+
+ public static final int ITEM_TYPE_STANDALONE = 0;
+ public static final int ITEM_TYPE_GROUP_HEADER = 1;
+ public static final int ITEM_TYPE_IN_GROUP = 2;
+
+ /**
+ * Information about a specific list item: is it a group, if so is it expanded.
+ * Otherwise, is it a stand-alone item or a group member.
+ */
+ protected static class PositionMetadata {
+ int itemType;
+ boolean isExpanded;
+ int cursorPosition;
+ int childCount;
+ private int groupPosition;
+ private int listPosition = -1;
+ }
+
+ private Context mContext;
+ private Cursor mCursor;
+
+ /**
+ * Count of list items.
+ */
+ private int mCount;
+
+ private int mRowIdColumnIndex;
+
+ /**
+ * Count of groups in the list.
+ */
+ private int mGroupCount;
+
+ /**
+ * Information about where these groups are located in the list, how large they are
+ * and whether they are expanded.
+ */
+ private long[] mGroupMetadata;
+
+ private SparseIntArray mPositionCache = new SparseIntArray();
+ private int mLastCachedListPosition;
+ private int mLastCachedCursorPosition;
+ private int mLastCachedGroup;
+
+ /**
+ * A reusable temporary instance of PositionMetadata
+ */
+ private PositionMetadata mPositionMetadata = new PositionMetadata();
+
+ protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onContentChanged();
+ }
+ };
+
+ protected DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ };
+
+ public GroupingListAdapter(Context context) {
+ mContext = context;
+ resetCache();
+ }
+
+ /**
+ * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
+ * each of them.
+ */
+ protected abstract void addGroups(Cursor cursor);
+
+ protected abstract View newStandAloneView(Context context, ViewGroup parent);
+ protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
+
+ protected abstract View newGroupView(Context context, ViewGroup parent);
+ protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded);
+
+ protected abstract View newChildView(Context context, ViewGroup parent);
+ protected abstract void bindChildView(View view, Context context, Cursor cursor);
+
+ /**
+ * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
+ */
+ private void resetCache() {
+ mCount = -1;
+ mLastCachedListPosition = -1;
+ mLastCachedCursorPosition = -1;
+ mLastCachedGroup = -1;
+ mPositionMetadata.listPosition = -1;
+ mPositionCache.clear();
+ }
+
+ protected void onContentChanged() {
+ }
+
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mChangeObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.close();
+ }
+ mCursor = cursor;
+ resetCache();
+ findGroups();
+
+ if (cursor != null) {
+ cursor.registerContentObserver(mChangeObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
+ notifyDataSetChanged();
+ } else {
+ // notify the observers about the lack of a data set
+ notifyDataSetInvalidated();
+ }
+
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * Scans over the entire cursor looking for duplicate phone numbers that need
+ * to be collapsed.
+ */
+ private void findGroups() {
+ mGroupCount = 0;
+ mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
+
+ if (mCursor == null) {
+ return;
+ }
+
+ addGroups(mCursor);
+ }
+
+ /**
+ * Records information about grouping in the list. Should be called by the overridden
+ * {@link #addGroups} method.
+ */
+ protected void addGroup(int cursorPosition, int size, boolean expanded) {
+ if (mGroupCount >= mGroupMetadata.length) {
+ int newSize = idealLongArraySize(
+ mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
+ long[] array = new long[newSize];
+ System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
+ mGroupMetadata = array;
+ }
+
+ long metadata = ((long)size << 32) | cursorPosition;
+ if (expanded) {
+ metadata |= EXPANDED_GROUP_MASK;
+ }
+ mGroupMetadata[mGroupCount++] = metadata;
+ }
+
+ // Copy/paste from ArrayUtils
+ private int idealLongArraySize(int need) {
+ return idealByteArraySize(need * 8) / 8;
+ }
+
+ // Copy/paste from ArrayUtils
+ private int idealByteArraySize(int need) {
+ for (int i = 4; i < 32; i++)
+ if (need <= (1 << i) - 12)
+ return (1 << i) - 12;
+
+ return need;
+ }
+
+ public int getCount() {
+ if (mCursor == null) {
+ return 0;
+ }
+
+ if (mCount != -1) {
+ return mCount;
+ }
+
+ int cursorPosition = 0;
+ int count = 0;
+ for (int i = 0; i < mGroupCount; i++) {
+ long metadata = mGroupMetadata[i];
+ int offset = (int)(metadata & GROUP_OFFSET_MASK);
+ boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
+ int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
+
+ count += (offset - cursorPosition);
+
+ if (expanded) {
+ count += size + 1;
+ } else {
+ count++;
+ }
+
+ cursorPosition = offset + size;
+ }
+
+ mCount = count + mCursor.getCount() - cursorPosition;
+ return mCount;
+ }
+
+ /**
+ * Figures out whether the item at the specified position represents a
+ * stand-alone element, a group or a group child. Also computes the
+ * corresponding cursor position.
+ */
+ public void obtainPositionMetadata(PositionMetadata metadata, int position) {
+
+ // If the description object already contains requested information, just return
+ if (metadata.listPosition == position) {
+ return;
+ }
+
+ int listPosition = 0;
+ int cursorPosition = 0;
+ int firstGroupToCheck = 0;
+
+ // Check cache for the supplied position. What we are looking for is
+ // the group descriptor immediately preceding the supplied position.
+ // Once we have that, we will be able to tell whether the position
+ // is the header of the group, a member of the group or a standalone item.
+ if (mLastCachedListPosition != -1) {
+ if (position <= mLastCachedListPosition) {
+
+ // Have SparceIntArray do a binary search for us.
+ int index = mPositionCache.indexOfKey(position);
+
+ // If we get back a positive number, the position corresponds to
+ // a group header.
+ if (index < 0) {
+
+ // We had a cache miss, but we did obtain valuable information anyway.
+ // The negative number will allow us to compute the location of
+ // the group header immediately preceding the supplied position.
+ index = ~index - 1;
+
+ if (index >= mPositionCache.size()) {
+ index--;
+ }
+ }
+
+ // A non-negative index gives us the position of the group header
+ // corresponding or preceding the position, so we can
+ // search for the group information at the supplied position
+ // starting with the cached group we just found
+ if (index >= 0) {
+ listPosition = mPositionCache.keyAt(index);
+ firstGroupToCheck = mPositionCache.valueAt(index);
+ long descriptor = mGroupMetadata[firstGroupToCheck];
+ cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
+ }
+ } else {
+
+ // If we haven't examined groups beyond the supplied position,
+ // we will start where we left off previously
+ firstGroupToCheck = mLastCachedGroup;
+ listPosition = mLastCachedListPosition;
+ cursorPosition = mLastCachedCursorPosition;
+ }
+ }
+
+ for (int i = firstGroupToCheck; i < mGroupCount; i++) {
+ long group = mGroupMetadata[i];
+ int offset = (int)(group & GROUP_OFFSET_MASK);
+
+ // Move pointers to the beginning of the group
+ listPosition += (offset - cursorPosition);
+ cursorPosition = offset;
+
+ if (i > mLastCachedGroup) {
+ mPositionCache.append(listPosition, i);
+ mLastCachedListPosition = listPosition;
+ mLastCachedCursorPosition = cursorPosition;
+ mLastCachedGroup = i;
+ }
+
+ // Now we have several possibilities:
+ // A) The requested position precedes the group
+ if (position < listPosition) {
+ metadata.itemType = ITEM_TYPE_STANDALONE;
+ metadata.cursorPosition = cursorPosition - (listPosition - position);
+ return;
+ }
+
+ boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
+ int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
+
+ // B) The requested position is a group header
+ if (position == listPosition) {
+ metadata.itemType = ITEM_TYPE_GROUP_HEADER;
+ metadata.groupPosition = i;
+ metadata.isExpanded = expanded;
+ metadata.childCount = size;
+ metadata.cursorPosition = offset;
+ return;
+ }
+
+ if (expanded) {
+ // C) The requested position is an element in the expanded group
+ if (position < listPosition + size + 1) {
+ metadata.itemType = ITEM_TYPE_IN_GROUP;
+ metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
+ return;
+ }
+
+ // D) The element is past the expanded group
+ listPosition += size + 1;
+ } else {
+
+ // E) The element is past the collapsed group
+ listPosition++;
+ }
+
+ // Move cursor past the group
+ cursorPosition += size;
+ }
+
+ // The required item is past the last group
+ metadata.itemType = ITEM_TYPE_STANDALONE;
+ metadata.cursorPosition = cursorPosition + (position - listPosition);
+ }
+
+ /**
+ * Returns true if the specified position in the list corresponds to a
+ * group header.
+ */
+ public boolean isGroupHeader(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
+ }
+
+ /**
+ * Given a position of a groups header in the list, returns the size of
+ * the corresponding group.
+ */
+ public int getGroupSize(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.childCount;
+ }
+
+ /**
+ * Mark group as expanded if it is collapsed and vice versa.
+ */
+ @NeededForTesting
+ public void toggleGroup(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
+ throw new IllegalArgumentException("Not a group at position " + position);
+ }
+
+ if (mPositionMetadata.isExpanded) {
+ mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
+ } else {
+ mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
+ }
+ resetCache();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 3;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.itemType;
+ }
+
+ public Object getItem(int position) {
+ if (mCursor == null) {
+ return null;
+ }
+
+ obtainPositionMetadata(mPositionMetadata, position);
+ if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ public long getItemId(int position) {
+ Object item = getItem(position);
+ if (item != null) {
+ return mCursor.getLong(mRowIdColumnIndex);
+ } else {
+ return -1;
+ }
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ View view = convertView;
+ if (view == null) {
+ switch (mPositionMetadata.itemType) {
+ case ITEM_TYPE_STANDALONE:
+ view = newStandAloneView(mContext, parent);
+ break;
+ case ITEM_TYPE_GROUP_HEADER:
+ view = newGroupView(mContext, parent);
+ break;
+ case ITEM_TYPE_IN_GROUP:
+ view = newChildView(mContext, parent);
+ break;
+ }
+ }
+
+ mCursor.moveToPosition(mPositionMetadata.cursorPosition);
+ switch (mPositionMetadata.itemType) {
+ case ITEM_TYPE_STANDALONE:
+ bindStandAloneView(view, mContext, mCursor);
+ break;
+ case ITEM_TYPE_GROUP_HEADER:
+ bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount,
+ mPositionMetadata.isExpanded);
+ break;
+ case ITEM_TYPE_IN_GROUP:
+ bindChildView(view, mContext, mCursor);
+ break;
+
+ }
+ return view;
+ }
+}
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index 0e558bfa4..f22a5d19c 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -109,7 +109,6 @@ public class ListsFragment extends Fragment implements ViewPager.OnPageChangeLis
case TAB_INDEX_RECENTS:
mRecentsFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL,
MAX_RECENTS_ENTRIES, System.currentTimeMillis() - OLDEST_RECENTS_DATE);
- mRecentsFragment.setHasFooterView(true);
return mRecentsFragment;
case TAB_INDEX_ALL_CONTACTS:
mAllContactsFragment = new AllContactsFragment();
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index e5c39d078..dfacd3f6d 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -42,15 +42,15 @@ public class ObjectFactory {
* @param context The context to use.
* @param callFetcher Instance of call fetcher to use.
* @param contactInfoHelper Instance of contact info helper class to use.
- * @param isCallLog Is this call log adapter being used on the call log?
* @return Instance of CallLogAdapter.
*/
- public static CallLogAdapter newCallLogAdapter(Context context,
- CallFetcher callFetcher, ContactInfoHelper contactInfoHelper,
- CallItemExpandedListener callItemExpandedListener,
+ public static CallLogAdapter newCallLogAdapter(
+ Context context,
+ CallFetcher callFetcher,
+ ContactInfoHelper contactInfoHelper,
OnReportButtonClickListener onReportButtonClickListener) {
- return new CallLogAdapter(context, callFetcher, contactInfoHelper,
- callItemExpandedListener, onReportButtonClickListener);
+ return new CallLogAdapter(
+ context, callFetcher, contactInfoHelper, onReportButtonClickListener);
}
public static DialogFragment getReportDialogFragment(String number) {
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index dbdde6875..845e279c9 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -196,7 +196,7 @@ public class CallLogAdapterTest extends AndroidTestCase {
private static final class TestCallLogAdapter extends CallLogAdapter {
public TestCallLogAdapter(Context context, CallFetcher callFetcher,
ContactInfoHelper contactInfoHelper) {
- super(context, callFetcher, contactInfoHelper, null, null);
+ super(context, callFetcher, contactInfoHelper, null);
mContactInfoCache = new TestContactInfoCache(
contactInfoHelper, mOnContactInfoChangedListener);
}
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
index 055342250..b57489d55 100644
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -57,7 +57,7 @@ import java.util.Random;
* runtest contacts
* or
* adb shell am instrument \
- * -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ * -w com.android.dialer.tests/android.test.InstrumentationTestRunner
*/
@LargeTest
public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<FragmentTestActivity> {
@@ -177,7 +177,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
mCursor.moveToFirst();
insertPrivate(NOW, 0);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
}
@MediumTest
@@ -193,7 +193,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
mCursor.moveToFirst();
insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, TEST_NUMBER);
@@ -207,7 +207,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
values[CallLogQuery.CACHED_FORMATTED_NUMBER] = TEST_FORMATTED_NUMBER;
insertValues(values);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, TEST_FORMATTED_NUMBER);
@@ -221,7 +221,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, "John Doe");
@@ -234,7 +234,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues("sip:johndoe@gmail.com", NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, "John Doe");
@@ -249,7 +249,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, "John Doe");
@@ -264,7 +264,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_WORK, TEST_DEFAULT_CUSTOM_LABEL);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, "John Doe");
@@ -278,7 +278,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_CUSTOM, numberLabel);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertNameIs(views, "John Doe");
@@ -291,7 +291,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
"John Doe", Phone.TYPE_HOME, "");
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertTrue(views.quickContactView.isEnabled());
@@ -302,7 +302,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
mCursor.moveToFirst();
insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
assertFalse(views.quickContactView.isEnabled());
@@ -313,7 +313,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
mCursor.moveToFirst();
insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
@@ -334,7 +334,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
mCursor.moveToFirst();
insertVoicemail(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0);
View view = mAdapter.newStandAloneView(getActivity(), mParentView);
- mAdapter.bindViewForTest(view, getActivity(), mCursor);
+ bindViewForTest(view, mCursor);
CallLogListItemViews views = (CallLogListItemViews) view.getTag();
IntentProvider intentProvider = (IntentProvider) views.voicemailButtonView.getTag();
@@ -424,7 +424,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
if (null == mList[i]) {
mList[i] = mAdapter.newStandAloneView(mActivity, mParentView);
}
- mAdapter.bindViewForTest(mList[i], mActivity, mCursor);
+ bindViewForTest(mList[i], mCursor);
mCursor.moveToPrevious();
i++;
}
@@ -442,6 +442,19 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<Fragme
//
/**
+ * Bind a call log entry view for testing purposes. Also inflates the action view stub so
+ * unit tests can access the buttons contained within.
+ *
+ * @param view The current call log row.
+ * @param cursor The cursor to bind from.
+ */
+ private void bindViewForTest(View view, MatrixCursor cursor) {
+ mAdapter.bindView(view, cursor, /* count */ 1);
+ CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+ mAdapter.expandItem(views, /* expand */ true);
+ }
+
+ /**
* Insert a certain number of random numbers in the DB. Makes sure
* there is at least one private and one unknown number in the DB.
* @param num Of entries to be inserted.
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
new file mode 100644
index 000000000..3eb5f06b1
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2015 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.dialer.calllog;
+
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_GROUP_HEADER;
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_IN_GROUP;
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_STANDALONE;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Tests for {@link GroupingListAdapter}.
+ *
+ * Running all tests:
+ *
+ * adb shell am instrument -e class com.android.dialer.calllog.GroupingListAdapterTests \
+ * -w com.google.android.dialer.tests/android.test.InstrumentationTestRunner
+ */
+public class GroupingListAdapterTests extends AndroidTestCase {
+
+ static private final String[] PROJECTION = new String[] {
+ "_id",
+ "group",
+ };
+
+ private static final int GROUPING_COLUMN_INDEX = 1;
+
+ private MatrixCursor mCursor;
+ private long mNextId;
+
+ private GroupingListAdapter mAdapter = new GroupingListAdapter(null) {
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+ int count = cursor.getCount();
+ int groupItemCount = 1;
+ cursor.moveToFirst();
+ String currentValue = cursor.getString(GROUPING_COLUMN_INDEX);
+ for (int i = 1; i < count; i++) {
+ cursor.moveToNext();
+ String value = cursor.getString(GROUPING_COLUMN_INDEX);
+ if (TextUtils.equals(value, currentValue)) {
+ groupItemCount++;
+ } else {
+ if (groupItemCount > 1) {
+ addGroup(i - groupItemCount, groupItemCount, false);
+ }
+
+ groupItemCount = 1;
+ currentValue = value;
+ }
+ }
+ if (groupItemCount > 1) {
+ addGroup(count - groupItemCount, groupItemCount, false);
+ }
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor) {
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ }
+
+ @Override
+ protected void bindStandAloneView(View view, Context context, Cursor cursor) {
+ }
+
+ @Override
+ protected View newChildView(Context context, ViewGroup parent) {
+ return null;
+ }
+
+ @Override
+ protected View newGroupView(Context context, ViewGroup parent) {
+ return null;
+ }
+
+ @Override
+ protected View newStandAloneView(Context context, ViewGroup parent) {
+ return null;
+ }
+ };
+
+ private void buildCursor(String... numbers) {
+ mCursor = new MatrixCursor(PROJECTION);
+ mNextId = 1;
+ for (String number : numbers) {
+ mCursor.addRow(new Object[]{mNextId, number});
+ mNextId++;
+ }
+ }
+
+ public void testGroupingWithoutGroups() {
+ buildCursor("1", "2", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithCollapsedGroupAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(2, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithExpandedGroupAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(0);
+
+ assertEquals(4, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, true, 0);
+ assertPositionMetadata(1, ITEM_TYPE_IN_GROUP, false, 0);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithExpandCollapseCycleAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(0);
+ mAdapter.toggleGroup(0);
+
+ assertEquals(2, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithCollapsedGroupInTheMiddle() {
+ buildCursor("1", "2", "2", "2", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 4);
+ }
+
+ public void testGroupingWithExpandedGroupInTheMiddle() {
+ buildCursor("1", "2", "2", "2", "3");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(1);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 4);
+ }
+
+ public void testGroupingWithCollapsedGroupAtTheEnd() {
+ buildCursor("1", "2", "3", "3", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, false, 2);
+ }
+
+ public void testGroupingWithExpandedGroupAtTheEnd() {
+ buildCursor("1", "2", "3", "3", "3");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(2);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, true, 2);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_IN_GROUP, false, 4);
+ }
+
+ public void testGroupingWithMultipleCollapsedGroups() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testGroupingWithMultipleExpandedGroups() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(1);
+
+ // Note that expanding the group of 2's shifted the group of 5's down from the
+ // 4th to the 6th position
+ mAdapter.toggleGroup(6);
+
+ assertEquals(10, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, true, 6);
+ assertPositionMetadata(7, ITEM_TYPE_IN_GROUP, false, 6);
+ assertPositionMetadata(8, ITEM_TYPE_IN_GROUP, false, 7);
+ assertPositionMetadata(9, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testPositionCache() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+
+ // First pass - building up cache
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Second pass - using cache
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Invalidate cache by expanding a group
+ mAdapter.toggleGroup(1);
+
+ // First pass - building up cache
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Second pass - using cache
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testGroupDescriptorArrayGrowth() {
+ String[] numbers = new String[500];
+ for (int i = 0; i < numbers.length; i++) {
+
+ // Make groups of 2
+ numbers[i] = String.valueOf((i / 2) * 2);
+ }
+
+ buildCursor(numbers);
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(250, mAdapter.getCount());
+ }
+
+ private void assertPositionMetadata(int position, int itemType, boolean isExpanded,
+ int cursorPosition) {
+ GroupingListAdapter.PositionMetadata metadata = new GroupingListAdapter.PositionMetadata();
+ mAdapter.obtainPositionMetadata(metadata, position);
+ assertEquals(itemType, metadata.itemType);
+ if (metadata.itemType == ITEM_TYPE_GROUP_HEADER) {
+ assertEquals(isExpanded, metadata.isExpanded);
+ }
+ assertEquals(cursorPosition, metadata.cursorPosition);
+ }
+}