Search code examples
androidloadercalllog

Call list view lag while scrolling


I am working on sample of calls log application. In this application my fragment displays Dialed Type calls in a list. here in each list item it shows photo from contacts, Number, name and time. It is working fine but it lags while scrolling.

fragment code:

package com.example.vl.calllogs;


import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog;
import android.provider.ContactsContract;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.View;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;

import org.w3c.dom.Text;

import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Created by vl on 12/29/2015.
 */
public class TabDialedFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {

    CursorAdapter mAdapter;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        setEmptyText("No Dialed Numbers");
        mAdapter = new MyCursorAdapter(getActivity(), R.layout.fragment_tab_dialed, null, 0);
        setListAdapter(mAdapter);
        getLoaderManager().initLoader(0, null, this);
    }

    private static final String[] PROJECTION = {
            CallLog.Calls._ID,
            CallLog.Calls.DATE,
            CallLog.Calls.CACHED_NAME,
            CallLog.Calls.CACHED_PHOTO_ID,
            CallLog.Calls.NUMBER,
            CallLog.Calls.DURATION
    };

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri baseUri  = CallLog.Calls.CONTENT_URI;
        String selection = CallLog.Calls.TYPE + "= 2";
        return new CursorLoader(getActivity(), baseUri, PROJECTION,selection, null, CallLog.Calls.DATE + " DESC" );
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
       mAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
       mAdapter.swapCursor(null);
    }

    class MyCursorAdapter extends ResourceCursorAdapter{

        MyCursorAdapter(Context context, int layout, Cursor cursor, int flags ){
            super(context, layout,cursor,flags);
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            TextView name = (TextView) view.findViewById(R.id.name);
            String nameString = cursor.getString(cursor.getColumnIndex(CallLog.Calls.CACHED_NAME));
            if(nameString == null || "".equals(nameString.trim())){
                name.setText("Unknown");
            }else{
                name.setText(nameString);
            }

            TextView time = (TextView) view.findViewById(R.id.time);
            String timeS = cursor.getString(cursor.getColumnIndex(CallLog.Calls.DATE));
            Date callDayTime = new Date(Long.valueOf(timeS));
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm");
            String str = simpleDateFormat.format(callDayTime);

            String durationS = cursor.getString(cursor.getColumnIndex(CallLog.Calls.DURATION));
            time.setText(String.format(getActivity().getResources().getString(R.string.thirdLine), str, durationS));
            TextView number = (TextView) view.findViewById(R.id.number);
            String numberS = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
            number.setText(numberS);

            int contactID = getContactIDFromNumber(numberS);
            ImageView imageView = (ImageView) view.findViewById(R.id.imageView);
            imageView.setImageBitmap(getPhoto(contactID+""));
         }

        public int getContactIDFromNumber(String contactNumber)
        {
            contactNumber = Uri.encode(contactNumber);
            int phoneContactID = -1;
            Cursor contactLookupCursor = getActivity().getContentResolver().query(Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,contactNumber),new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME, ContactsContract.PhoneLookup._ID}, null, null, null);
            while(contactLookupCursor.moveToNext()){
                phoneContactID = contactLookupCursor.getInt(contactLookupCursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup._ID));
            }
            contactLookupCursor.close();

            return phoneContactID;
        }

        private Bitmap getPhoto(String id){
            Bitmap photo = null;
            try{
                InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(
                        getActivity().getContentResolver(),
                        ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, new Long(id).longValue()));
                if(inputStream != null)
                    photo= BitmapFactory.decodeStream(inputStream);
            }catch (Exception e){

            }
            return photo;
        }

    }

}

I feel it might be the problem of in efficient way of getting photo from Contacts. here I first get the contact_id and then I queried for the photo using contact ID. Is this the correct way?

Last queries are not working asynchronously. To make async what should I do?


Solution

  • There're three main performance issues on your bindView you should fix.

    1. Most serious issue:

    The call to private Bitmap getPhoto that is a SQLite operation followed by a disk loading operation. that takes several milliseconds to happen and is definitely lagging the UI.

    Bad news is that background thread image loading and caching is a very complex topic and very difficult to code properly.

    Good news is that nowadays we have loads of great libraries that does the work for you. Below is the code to load it using Picasso library

    Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
    Picasso.with(getActivity()).load(uri).into(imageView);
    
    1. Pretty bad issue:

    The call public int getContactIDFromNumber is also doing an SQLite query and that's also pretty slow.

    Unfortunately I don't have any major suggestion on how you should fix it. It's a slow operation and you should do in a background thread. It will be a major refactor to make it work from inside the adapter.

    My suggestion is to extend CursorLoader and make it after it finishes load the cursor (but still on the background thread) to perform all those queries and keep it in some HashMap

    1. Minor issues:

      • Use a Holder Pattern to avoid all those calls to findViewById(int).
      • Also avoid creating new objects inside bindView. For example, you should have only 1 private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm"); for the whole class and use this same instance all the time. Also have just 1 Date object for the Adapter and just call callDayTime.setTime(Long.valueOf(timeS));

    Hope it helps.