diff options
-rw-r--r-- | Android.mk | 10 | ||||
-rw-r--r-- | res/drawable/recent_lists_footer_background.xml | 24 | ||||
-rw-r--r-- | res/layout/recents_list_footer.xml | 33 | ||||
-rw-r--r-- | res/values/animation_constants.xml | 12 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/CallLogAdapter.java | 78 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/CallLogFragment.java | 231 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/CallLogGroupBuilder.java | 1 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/GroupingListAdapter.java | 490 | ||||
-rw-r--r-- | src/com/android/dialer/list/ListsFragment.java | 1 | ||||
-rw-r--r-- | src/com/android/dialerbind/ObjectFactory.java | 12 | ||||
-rw-r--r-- | tests/src/com/android/dialer/calllog/CallLogAdapterTest.java | 2 | ||||
-rw-r--r-- | tests/src/com/android/dialer/calllog/CallLogFragmentTest.java | 41 | ||||
-rw-r--r-- | tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java | 311 |
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); + } +} |