Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 492cd737 authored by Andrew Lee's avatar Andrew Lee
Browse files

Replace ListView with RecyclerView in call log.

Yay, finally!

+ Replace ListView with RecyclerView in layout and fragment files.
+ Change GroupingListAdapter to extend RecyclerView.Adapter instead
of BaseAdapter.
+ Change CallLogListItemViews to extend RecyclerView.ViewHolder.
+ Adapt onBindViewHolder and onCreateViewHolder methods in the
CallLogAdapter.
+ Update/rework tests for related classes.
+ Fix a bug in the GroupingListAdapter, where childCount was not
updated for standalone views, and the previously cached group size
was used instead. Set childCount to 1 for standalone views.
- Removed the idea of creating different views for standalone vs
group vs group headers from the adapters. This logic has not been
used for quite some time and all these functions funneled into
createView/bindView methods anyways, so there is no logical
difference. If we need to create custom views in the future, we can
leverage onCreateViewHolder's viewType parameter.

Bug: 19372817
Change-Id: I1b7289340600609669db22d8bc89265240d0b561
parent 69748953
Loading
Loading
Loading
Loading
+3 −10
Original line number Diff line number Diff line
@@ -61,18 +61,11 @@
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!-- clipChildren=false is required to ensure shadows drawn
            within list items aren't clipped by the list item bounds. -->
        <ListView android:id="@android:id/list"

        <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fadingEdge="none"
            android:scrollbarStyle="outsideOverlay"
            android:background="@color/background_dialer_list_items"
            android:divider="@null"
            android:nestedScrollingEnabled="true"
            android:clipChildren="false"
        />
            android:background="@color/background_dialer_list_items" />

        <include
            android:id="@+id/empty_list_view"
+16 −40
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.PhoneLookup;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
@@ -225,13 +226,12 @@ public class CallLogAdapter extends GroupingListAdapter
        mLoading = loading;
    }

    @Override
    public boolean isEmpty() {
        if (mLoading) {
            // We don't want the empty state to show when loading.
            return false;
        } else {
            return super.isEmpty();
            return getItemCount() == 0;
        }
    }

@@ -262,48 +262,19 @@ public class CallLogAdapter extends GroupingListAdapter
    }

    @Override
    protected View newStandAloneView(Context context, ViewGroup parent) {
        return newChildView(context, parent);
    }

    @Override
    protected View newGroupView(Context context, ViewGroup parent) {
        return newChildView(context, parent);
    }

    @Override
    protected View newChildView(Context context, ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(context);
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);

        // Get the views to bind to and cache them.
        CallLogListItemViews views = CallLogListItemViews.fromView(context, view);
        CallLogListItemViews views = CallLogListItemViews.fromView(mContext, view);
        view.setTag(views);

        // Set text height to false on the TextViews so they don't have extra padding.
        views.phoneCallDetailsViews.nameView.setElegantTextHeight(false);
        views.phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);

        return view;
    }

    @Override
    protected void bindStandAloneView(View view, Context context, Cursor cursor) {
        bindView(view, cursor, 1);
    }

    @Override
    protected void bindChildView(View view, Context context, Cursor cursor) {
        bindView(view, cursor, 1);
    }

    @Override
    protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
            boolean expanded) {
        bindView(view, cursor, groupSize);
    }

    private void findAndCacheViews(View view) {
        return (CallLogListItemViews) view.getTag();
    }

    /**
@@ -312,12 +283,17 @@ public class CallLogAdapter extends GroupingListAdapter
     * should not. It invokes cross-process methods and the repeat execution can get costly.
     *
     * @param callLogItemView the view corresponding to this entry
     * @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
     */
    public void bindView(View callLogItemView, Cursor c, int count) {
        callLogItemView.setAccessibilityDelegate(mAccessibilityDelegate);
        final CallLogListItemViews views = (CallLogListItemViews) callLogItemView.getTag();
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        Cursor c = (Cursor) getItem(position);
        if (c == null) {
            return;
        }
        int count = getGroupSize(position);

        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
        views.rootView.setAccessibilityDelegate(mAccessibilityDelegate);

        // Default case: an item in the call log.
        views.primaryActionView.setVisibility(View.VISIBLE);
@@ -435,7 +411,7 @@ public class CallLogAdapter extends GroupingListAdapter

        // Listen for the first draw
        if (mViewTreeObserver == null) {
            mViewTreeObserver = callLogItemView.getViewTreeObserver();
            mViewTreeObserver = views.rootView.getViewTreeObserver();
            mViewTreeObserver.addOnPreDrawListener(this);
        }
    }
+22 −12
Original line number Diff line number Diff line
@@ -21,8 +21,8 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.KeyguardManager;
import android.app.ListFragment;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
@@ -34,10 +34,11 @@ import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.provider.VoicemailContract.Status;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.ListView;
@@ -60,7 +61,7 @@ import java.util.List;
 * Displays a list of call log entries. To filter for a particular kind of call
 * (all, missed or voicemails), specify it in the constructor.
 */
public class CallLogFragment extends ListFragment
public class CallLogFragment extends Fragment
        implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
        CallLogAdapter.CallFetcher {
    private static final String TAG = "CallLogFragment";
@@ -76,6 +77,8 @@ public class CallLogFragment extends ListFragment
    private static final String KEY_LOG_LIMIT = "log_limit";
    private static final String KEY_DATE_LIMIT = "date_limit";

    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLayoutManager;
    private CallLogAdapter mAdapter;
    private CallLogQueryHandler mCallLogQueryHandler;
    private boolean mScrollToTop;
@@ -172,9 +175,6 @@ public class CallLogFragment extends ListFragment
        }

        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
                new ContactInfoHelper(getActivity(), currentCountryIso), this);
        setListAdapter(mAdapter);
        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
                this, mLogLimit);
        mKeyguardManager =
@@ -201,9 +201,8 @@ public class CallLogFragment extends ListFragment
        // 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);
        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);

        if (mScrollToTop) {
@@ -213,8 +212,9 @@ public class CallLogFragment extends ListFragment
            // will not experience the illusion of downward motion.  Instead,
            // if we're not already near the top of the list, we instantly jump
            // near the top, and animate from there.
            if (listView.getFirstVisiblePosition() > 5) {
                listView.setSelection(5);
            if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
                // TODO: Jump to near the top, then begin smooth scroll.
                mRecyclerView.smoothScrollToPosition(0);
            }
            // Workaround for framework issue: the smooth-scroll doesn't
            // occur if setSelection() is called immediately before.
@@ -224,7 +224,7 @@ public class CallLogFragment extends ListFragment
                   if (getActivity() == null || getActivity().isFinishing()) {
                       return;
                   }
                   listView.smoothScrollToPosition(0);
                   mRecyclerView.smoothScrollToPosition(0);
               }
            });

@@ -269,6 +269,17 @@ public class CallLogFragment extends ListFragment
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
        View view = inflater.inflate(R.layout.call_log_fragment, container, false);

        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLayoutManager);

        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
                new ContactInfoHelper(getActivity(), currentCountryIso), this);
        mRecyclerView.setAdapter(mAdapter);

        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
        mStatusMessageView = view.findViewById(R.id.voicemail_status);
        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
@@ -280,7 +291,6 @@ public class CallLogFragment extends ListFragment
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mEmptyListView = view.findViewById(R.id.empty_list_view);
        getListView().setItemsCanFocus(true);

        updateEmptyMessage(mCallTypeFilter);
    }
+3 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.support.v7.widget.RecyclerView;
import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
import android.view.View;
@@ -44,7 +45,7 @@ import com.android.dialer.R;
 * is a way of isolating view logic from the CallLogAdapter. We should consider moving that logic
 * if the call log list item is eventually represented as a UI component.
 */
public final class CallLogListItemViews {
public final class CallLogListItemViews extends RecyclerView.ViewHolder {
    /** The root view of the call log list item */
    public final View rootView;
    /** The quick contact badge for the contact. */
@@ -147,6 +148,7 @@ public final class CallLogListItemViews {
            PhoneCallDetailsViews phoneCallDetailsViews,
            View callLogEntryView,
            TextView dayGroupHeader) {
        super(rootView);
        mContext = context;

        this.rootView = rootView;
+19 −60
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
@@ -34,7 +36,7 @@ import com.android.contacts.common.testing.NeededForTesting;
 * 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 {
abstract class GroupingListAdapter extends RecyclerView.Adapter {

    private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
    private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
@@ -109,11 +111,6 @@ abstract class GroupingListAdapter extends BaseAdapter {
        public void onChanged() {
            notifyDataSetChanged();
        }

        @Override
        public void onInvalidated() {
            notifyDataSetInvalidated();
        }
    };

    public GroupingListAdapter(Context context) {
@@ -127,15 +124,7 @@ abstract class GroupingListAdapter extends BaseAdapter {
     */
    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);
    protected abstract void onContentChanged();

    /**
     * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
@@ -149,9 +138,6 @@ abstract class GroupingListAdapter extends BaseAdapter {
        mPositionCache.clear();
    }

    protected void onContentChanged() {
    }

    public void changeCursor(Cursor cursor) {
        if (cursor == mCursor) {
            return;
@@ -171,13 +157,10 @@ abstract class GroupingListAdapter extends BaseAdapter {
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
            notifyDataSetChanged();
        } else {
            // notify the observers about the lack of a data set
            notifyDataSetInvalidated();
        }

    }

    @NeededForTesting
    public Cursor getCursor() {
        return mCursor;
    }
@@ -231,7 +214,8 @@ abstract class GroupingListAdapter extends BaseAdapter {
        return need;
    }

    public int getCount() {
    @Override
    public int getItemCount() {
        if (mCursor == null) {
            return 0;
        }
@@ -343,6 +327,7 @@ abstract class GroupingListAdapter extends BaseAdapter {
            if (position < listPosition) {
                metadata.itemType = ITEM_TYPE_STANDALONE;
                metadata.cursorPosition = cursorPosition - (listPosition - position);
                metadata.childCount = 1;
                return;
            }

@@ -382,6 +367,7 @@ abstract class GroupingListAdapter extends BaseAdapter {
        // The required item is past the last group
        metadata.itemType = ITEM_TYPE_STANDALONE;
        metadata.cursorPosition = cursorPosition + (position - listPosition);
        metadata.childCount = 1;
    }

    /**
@@ -421,12 +407,6 @@ abstract class GroupingListAdapter extends BaseAdapter {
        notifyDataSetChanged();
    }

    @Override
    public int getViewTypeCount() {
        return 3;
    }

    @Override
    public int getItemViewType(int position) {
        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.itemType;
@@ -454,37 +434,16 @@ abstract class GroupingListAdapter extends BaseAdapter {
        }
    }

    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;
    /**
     * Used for setting the cursor without triggering a UI thread update.
     */
    @NeededForTesting
    public void setCursorForTesting(Cursor cursor) {
        if (cursor != null) {
            mCursor = cursor;
            cursor.registerContentObserver(mChangeObserver);
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
        }
    }
}
Loading