SmartListFragment : add recyclerView

Replace the actual listView with a recyclerView

Tuleap: #1374
Change-Id: I293336c3c221d683887e8068c8b05ea8d4c24fb3
Reviewed-by: Aline Bonnet <aline.bonnet@savoirfairelinux.com>
diff --git a/ring-android/app/build.gradle b/ring-android/app/build.gradle
index 4e79ce9..154f1c4 100644
--- a/ring-android/app/build.gradle
+++ b/ring-android/app/build.gradle
@@ -40,6 +40,9 @@
     apt 'com.google.dagger:dagger-compiler:2.7'
     compile 'com.google.dagger:dagger:2.7'
     provided 'javax.annotation:jsr250-api:1.0'
+
+    // Glide
+    compile 'com.github.bumptech.glide:glide:3.7.0'
 }
 
 android {
diff --git a/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java
index 6d38ff3..848644d 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java
@@ -1,8 +1,7 @@
 /*
- *  Copyright (C) 2004-2016 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
  *
- *  Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *           Romain Bertozzi <romain.bertozzi@savoirfairelinux.com>
+ *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -16,226 +15,65 @@
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
- *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  */
-
 package cx.ring.adapters;
 
 import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
 import android.graphics.Typeface;
-import android.text.TextUtils;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.LruCache;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.animation.AnimationUtils;
-import android.widget.BaseAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
 
-import java.lang.ref.WeakReference;
-import java.text.Normalizer;
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
 
-import butterknife.BindView;
-import butterknife.ButterKnife;
 import cx.ring.R;
 import cx.ring.model.HistoryCall;
 import cx.ring.model.HistoryEntry;
-import cx.ring.utils.Tuple;
-import cx.ring.model.CallContact;
-import cx.ring.model.Conversation;
-import cx.ring.model.Phone;
 import cx.ring.model.TextMessage;
+import cx.ring.smartlist.SmartListViewModel;
+import cx.ring.utils.CircleTransform;
+import cx.ring.viewholders.SmartListViewHolder;
 
-public class SmartListAdapter extends BaseAdapter {
+public class SmartListAdapter extends RecyclerView.Adapter<SmartListViewHolder> {
 
-    private static String TAG = SmartListAdapter.class.getSimpleName();
+    private ArrayList<SmartListViewModel> mSmartListViewModels;
+    private SmartListViewHolder.SmartListListeners listener;
 
-    private final ArrayList<Conversation> mConversations = new ArrayList<>();
-    private final ExecutorService mInfosFetcher;
-    private final LruCache<Long, Bitmap> mMemoryCache;
-    private final HashMap<Long, WeakReference<ContactDetailsTask>> mRunningTasks = new HashMap<>();
-
-    private final Context mContext;
-
-    public interface SmartListAdapterCallback {
-        void pictureTapped(Conversation conversation);
-    }
-
-    private static SmartListAdapterCallback sDummyCallbacks = new SmartListAdapterCallback() {
-        @Override
-        public void pictureTapped(Conversation conversation) {
-            // Stub
-        }
-    };
-
-    private SmartListAdapterCallback mCallbacks = sDummyCallbacks;
-
-    public SmartListAdapter(Context act, LruCache<Long, Bitmap> cache, ExecutorService pool) {
-        super();
-        mContext = act;
-        mMemoryCache = cache;
-        mInfosFetcher = pool;
-    }
-
-    private String stringFormatting(String query) {
-        return Normalizer.normalize(query.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\u0300-\u036F]", "");
-    }
-
-    public void emptyDataset () {
-        mConversations.clear();
-        notifyDataSetChanged();
-    }
-
-    public void updateDataset(final Collection<Conversation> list, String query) {
-        Log.d(TAG, "updateDataset " + list.size() + " with query: " + query);
-
-        if (list.isEmpty() && mConversations.isEmpty()) {
-            return;
-        }
-
-        List<Conversation> newConversations = new ArrayList<>();
-
-        for (Conversation c : list) {
-            if (!c.getContact().isUnknown()
-                    || !c.getAccountsUsed().isEmpty()
-                    || c.getCurrentCall() != null) {
-                if (TextUtils.isEmpty(query) || c.getCurrentCall() != null) {
-                    newConversations.add(c);
-                } else if (c.getContact() != null) {
-                    CallContact contact = c.getContact();
-                    if (!TextUtils.isEmpty(contact.getDisplayName()) &&
-                            stringFormatting(contact.getDisplayName()).contains(stringFormatting(query))) {
-                        newConversations.add(c);
-                    } else if (contact.getPhones() != null && !contact.getPhones().isEmpty()) {
-                        ArrayList<Phone> phones = contact.getPhones();
-                        for (Phone phone : phones) {
-                            if (phone.getNumber() != null) {
-                                String rawUriString = phone.getNumber().getRawUriString();
-                                if (!TextUtils.isEmpty(rawUriString) &&
-                                        stringFormatting(rawUriString.toLowerCase()).contains(stringFormatting(query))) {
-                                    newConversations.add(c);
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        mConversations.clear();
-        mConversations.addAll(newConversations);
-        notifyDataSetChanged();
-    }
-
-    /**
-     *
-     * @return true if list are different
-     */
-    private boolean areConversationListDifferent(List<Conversation> leftList, List<Conversation> rightList) {
-        if (leftList == null || rightList == null) {
-            return true;
-        }
-
-        if (leftList.size() != rightList.size()) {
-            return true;
-        }
-
-        for (Conversation rightConversation: rightList) {
-            if (rightConversation.hasCurrentCall() || rightConversation.hasUnreadTextMessages()) {
-                return true;
-            }
-
-            int rightId = rightConversation.getUuid();
-            CallContact rightContact = rightConversation.getContact();
-            boolean found = false;
-            for (Conversation leftConversation: leftList) {
-
-                if (leftConversation.hasCurrentCall() || leftConversation.hasUnreadTextMessages()) {
-                    return true;
-                }
-
-                int leftId = leftConversation.getUuid();
-                CallContact leftContact = leftConversation.getContact();
-                if (leftId == rightId
-                        && leftContact!=null
-                        && leftContact.equals(rightContact)) {
-                    found = true;
-                    break;
-                }
-            }
-
-            if (!found) {
-                return true;
-            }
-        }
-
-        return false;
+    public SmartListAdapter(ArrayList<SmartListViewModel> smartListViewModels, SmartListViewHolder.SmartListListeners listener) {
+        this.mSmartListViewModels = new ArrayList<>(smartListViewModels);
+        this.listener = listener;
     }
 
     @Override
-    public int getCount() {
-        return mConversations.size();
+    public SmartListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_smartlist, parent, false);
+
+        return new SmartListViewHolder(v);
     }
 
     @Override
-    public Conversation getItem(int position) {
-        return mConversations.get(position);
-    }
+    public void onBindViewHolder(SmartListViewHolder holder, int position) {
+        final SmartListViewModel smartListViewModel = mSmartListViewModels.get(position);
 
-    @Override
-    public long getItemId(int position) {
-        return 0;
-    }
-
-    public class ViewHolder {
-        @BindView(R.id.conv_participant)
-        TextView convParticipants;
-        @BindView(R.id.conv_last_item)
-        TextView convStatus;
-        @BindView(R.id.conv_last_time)
-        TextView convTime;
-        @BindView(R.id.photo)
-        ImageView photo;
-        int position;
-        public Conversation conv;
-
-        public ViewHolder(View view) {
-            ButterKnife.bind(this, view);
-        }
-    }
-
-    @Override
-    public View getView(final int position, View convertView, ViewGroup parent) {
-        if (convertView == null) {
-            convertView = LayoutInflater.from(mContext).inflate(cx.ring.R.layout.item_smartlist, null);
-        }
-
-        final ViewHolder holder = convertView.getTag() != null
-                ? (ViewHolder) convertView.getTag()
-                : new ViewHolder(convertView);
-
-        holder.position = -1;
-        convertView.setTag(holder);
-
-        holder.conv = mConversations.get(position);
-        holder.position = position;
-        holder.convParticipants.setText(holder.conv.getContact().getDisplayName());
-        long lastInteraction = holder.conv.getLastInteraction().getTime();
+        holder.convParticipants.setText(smartListViewModel.getContactName());
+        long lastInteraction = smartListViewModel.getLastInteractionTime();
         holder.convTime.setText(lastInteraction == 0 ? "" :
                 DateUtils.getRelativeTimeSpanString(lastInteraction, System.currentTimeMillis(), 0L, DateUtils.FORMAT_ABBREV_ALL));
-        holder.convStatus.setText(getLastInteractionSummary(holder.conv, mContext.getResources()));
-        if (holder.conv.hasUnreadTextMessages()) {
+        if (smartListViewModel.hasOngoingCall()) {
+            holder.convStatus.setText(holder.itemView.getContext().getString(R.string.ongoing_call));
+        } else if (smartListViewModel.getLastInteraction() != null) {
+            holder.convStatus.setText(getLastInteractionSummary(smartListViewModel.getLastInteraction(), holder.itemView.getContext()));
+        } else {
+            holder.convStatus.setText("");
+        }
+        if (smartListViewModel.hasUnreadTextMessage()) {
             holder.convParticipants.setTypeface(null, Typeface.BOLD);
             holder.convTime.setTypeface(null, Typeface.BOLD);
             holder.convStatus.setTypeface(null, Typeface.BOLD);
@@ -245,93 +83,50 @@
             holder.convStatus.setTypeface(null, Typeface.NORMAL);
         }
 
-        holder.photo.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mCallbacks.pictureTapped(holder.conv);
-            }
-        });
+        String photoUri = smartListViewModel.getPhotoUri();
 
-        final Long cid = holder.conv.getContact().getId();
-        final Bitmap bmp = mMemoryCache.get(cid);
-        if (bmp != null && cid != -1L) {
-            holder.photo.setImageBitmap(bmp);
+        if (photoUri != null) {
+            if (isContentUri(photoUri)) {
+                Glide.with(holder.itemView.getContext())
+                        .load(Uri.withAppendedPath(Uri.parse(photoUri), ContactsContract.Contacts.Photo.DISPLAY_PHOTO))
+                        .placeholder(R.drawable.ic_contact_picture)
+                        .crossFade()
+                        .transform(new CircleTransform(holder.itemView.getContext()))
+                        .error(R.drawable.ic_contact_picture)
+                        .into(holder.photo);
+            }
+        } else if (smartListViewModel.getPhotoData() != null) {
+            Glide.with(holder.itemView.getContext())
+                    .load(smartListViewModel.getPhotoData())
+                    .placeholder(R.drawable.ic_contact_picture)
+                    .crossFade()
+                    .transform(new CircleTransform(holder.itemView.getContext()))
+                    .error(R.drawable.ic_contact_picture)
+                    .into(holder.photo);
         } else {
-            final WeakReference<ViewHolder> holderWeakRef = new WeakReference<>(holder);
-            final ContactDetailsTask.DetailsLoadedCallback cb = new ContactDetailsTask.DetailsLoadedCallback() {
-                @Override
-                public void onDetailsLoaded(final Bitmap bmp, final String name) {
-                    final ViewHolder holder = holderWeakRef.get();
-                    if (holder == null || holder.photo.getParent() == null)
-                        return;
-                    if (holder.conv.getContact().getId() == cid) {
-                        holder.photo.post(new Runnable() {
-                            @Override
-                            public void run() {
-                                holder.photo.setImageBitmap(bmp);
-                                holder.photo.startAnimation(AnimationUtils.loadAnimation(holder.photo.getContext(), R.anim.contact_fadein));
-                            }
-                        });
-
-                        holder.convParticipants.post(new Runnable() {
-                            @Override
-                            public void run() {
-                                holder.convParticipants.setText(name);
-                                holder.photo.startAnimation(AnimationUtils.loadAnimation(holder.convParticipants.getContext(), R.anim.contact_fadein));
-                            }
-                        });
-                    }
-                }
-            };
-            WeakReference<ContactDetailsTask> wtask = mRunningTasks.get(cid);
-            ContactDetailsTask task = wtask == null ? null : wtask.get();
-            if (task != null && cid != -1L) {
-                task.addCallback(cb);
-            } else {
-                task = new ContactDetailsTask(mContext, holder.photo, holder.convParticipants, holder.conv.getContact(), new ContactDetailsTask.DetailsLoadedCallback() {
-                    @Override
-                    public void onDetailsLoaded(Bitmap bmp, final String name) {
-                        mMemoryCache.put(cid, bmp);
-                        mRunningTasks.remove(cid);
-                    }
-                });
-                task.addCallback(cb);
-                mRunningTasks.put(cid, new WeakReference<>(task));
-                mInfosFetcher.execute(task);
-            }
+            Glide.with(holder.itemView.getContext())
+                    .load(R.drawable.ic_contact_picture)
+                    .into(holder.photo);
         }
-        return convertView;
+        holder.bind(listener, smartListViewModel);
     }
 
-    public void setCallback(SmartListAdapterCallback callback) {
-        if (callback == null) {
-            this.mCallbacks = sDummyCallbacks;
-            return;
-        }
-        this.mCallbacks = callback;
+    @Override
+    public int getItemCount() {
+        return mSmartListViewModels.size();
     }
 
-    private String getLastInteractionSummary(Conversation conversation, Resources resources) {
-        if (conversation.hasCurrentCall()) {
-            return resources.getString(R.string.ongoing_call);
-        }
-        Tuple<Date, String> d = new Tuple<>(new Date(0), null);
-
-        for (HistoryEntry e : conversation.getHistory().values()) {
-            Date entryDate = e.getLastInteractionDate();
-            String entrySummary = getLastInteractionSummary(e, resources);
-            if (entryDate == null || entrySummary == null) {
-                continue;
-            }
-            Tuple<Date, String> tmp = new Tuple<>(entryDate, entrySummary);
-            if (d.first.compareTo(entryDate) < 0) {
-                d = tmp;
-            }
-        }
-        return d.second;
+    public void replaceAll(ArrayList<SmartListViewModel> list) {
+        mSmartListViewModels.clear();
+        mSmartListViewModels.addAll(list);
+        notifyDataSetChanged();
     }
 
-    private String getLastInteractionSummary(HistoryEntry e, Resources resources) {
+    private boolean isContentUri(String uri) {
+        return uri.contains("content://");
+    }
+
+    private String getLastInteractionSummary(HistoryEntry e, Context context) {
         long lastTextTimestamp = e.getTextMessages().isEmpty() ? 0 : e.getTextMessages().lastEntry().getKey();
         long lastCallTimestamp = e.getCalls().isEmpty() ? 0 : e.getCalls().lastEntry().getKey();
         if (lastTextTimestamp > 0 && lastTextTimestamp > lastCallTimestamp) {
@@ -343,14 +138,14 @@
                     msgString = msgString.substring(msgString.lastIndexOf("\n") + 1);
                 }
             }
-            return (msg.isIncoming() ? "" : resources.getText(R.string.you_txt_prefix) + " ") + msgString;
+            return (msg.isIncoming() ? "" : context.getText(R.string.you_txt_prefix) + " ") + msgString;
         }
         if (lastCallTimestamp > 0) {
             HistoryCall lastCall = e.getCalls().lastEntry().getValue();
-            return String.format(resources.getString(lastCall.isIncoming()
+            return String.format(context.getString(lastCall.isIncoming()
                     ? R.string.hist_in_call
                     : R.string.hist_out_call), lastCall.getDurationString());
         }
         return null;
     }
-}
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java
index 4310dec..882d188 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java
@@ -23,7 +23,6 @@
 
 import android.app.Activity;
 import android.app.Fragment;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.Uri;
@@ -34,6 +33,8 @@
 import android.support.design.widget.Snackbar;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.SearchView;
 import android.support.v7.widget.Toolbar;
 import android.text.InputType;
@@ -45,17 +46,14 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.EditorInfo;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ImageView;
-import android.widget.ListView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import com.google.zxing.integration.android.IntentIntegrator;
 import com.google.zxing.integration.android.IntentResult;
 
-import java.util.Collection;
+import java.util.ArrayList;
 
 import javax.inject.Inject;
 
@@ -75,14 +73,16 @@
 import cx.ring.service.LocalService;
 import cx.ring.smartlist.SmartListPresenter;
 import cx.ring.smartlist.SmartListView;
+import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.ClipboardHelper;
 import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.NetworkUtils;
+import cx.ring.viewholders.SmartListViewHolder;
 
 public class SmartListFragment extends BaseFragment<SmartListPresenter> implements SearchView.OnQueryTextListener,
         HomeActivity.Refreshable,
-        SmartListAdapter.SmartListAdapterCallback,
+        SmartListViewHolder.SmartListListeners,
         Conversation.ConversationActionCallback,
         ClipboardHelper.ClipboardHelperCallback,
         SmartListView {
@@ -90,7 +90,6 @@
 
     private static final String TAG = SmartListFragment.class.getSimpleName();
     private static final String STATE_LOADING = TAG + ".STATE_LOADING";
-    private static final int USER_INPUT_DELAY = 300;
 
     @Inject
     protected SmartListPresenter mSmartListPresenter;
@@ -99,13 +98,13 @@
     protected FloatingActionButton mFloatingActionButton;
 
     @BindView(R.id.confs_list)
-    protected ListView mList;
+    protected RecyclerView mRecyclerView;
 
     @BindView(R.id.loading_indicator)
     protected ProgressBar mLoader;
 
-    @BindView(R.id.emptyTextView)
-    protected TextView mEmptyTextView = null;
+    @BindView(R.id.empty_text_view)
+    protected TextView mEmptyTextView;
 
     @BindView(R.id.newcontact_element)
     protected ViewGroup mNewContact;
@@ -119,7 +118,6 @@
     @BindView(R.id.error_image_view)
     protected ImageView mErrorImageView;
 
-    private LocalService.Callbacks mCallbacks = LocalService.DUMMY_CALLBACKS;
     private SmartListAdapter mSmartListAdapter;
 
     private SearchView mSearchView = null;
@@ -138,33 +136,14 @@
             throw new IllegalStateException("Activity must implement fragment's callbacks.");
         }
 
-        mCallbacks = (LocalService.Callbacks) activity;
     }
 
     public void refresh() {
-
-        LocalService service = mCallbacks.getService();
-        if (service == null) {
-            Log.e(TAG, "refresh: null service");
-            return;
-        }
-
-        if (mSmartListAdapter == null) {
-            bindService(getActivity(), service);
-        }
-
         mSmartListPresenter.refresh(NetworkUtils.isConnectedWifi(getActivity()),
                 NetworkUtils.isConnectedMobile(getActivity()));
     }
 
     @Override
-    public void onDetach() {
-        super.onDetach();
-        Log.d(TAG, "onDetach");
-        mCallbacks = LocalService.DUMMY_CALLBACKS;
-    }
-
-    @Override
     public void onResume() {
         super.onResume();
 
@@ -290,20 +269,12 @@
         // dependency injection
         ((RingApplication) getActivity().getApplication()).getRingInjectionComponent().inject(this);
 
-        mList.setOnItemClickListener(conversationClickListener);
-        mList.setOnItemLongClickListener(conversationLongClickListener);
-
         if (savedInstanceState != null) {
             this.setLoading(savedInstanceState.getBoolean(STATE_LOADING, false));
         }
 
         mNewContact.setVisibility(View.GONE);
 
-        LocalService service = mCallbacks.getService();
-        if (service != null) {
-            bindService(inflater.getContext(), service);
-        }
-
         if (ConversationFragment.isTabletMode(getActivity())) {
             isTabletMode = true;
         }
@@ -329,17 +300,6 @@
         }
     }
 
-    public void bindService(final Context ctx, final LocalService service) {
-        mSmartListAdapter = new SmartListAdapter(ctx,
-                service.get40dpContactCache(),
-                service.getThreadPool());
-
-        mSmartListAdapter.setCallback(this);
-        if (mList != null) {
-            mList.setAdapter(mSmartListAdapter);
-        }
-    }
-
     public void startConversationTablet(Bundle bundle) {
         mConversationFragment = new ConversationFragment();
         mConversationFragment.setArguments(bundle);
@@ -349,57 +309,6 @@
                 .commit();
     }
 
-    private final OnItemClickListener conversationClickListener = new OnItemClickListener() {
-        @Override
-        public void onItemClick(AdapterView<?> arg0, View v, int arg2, long arg3) {
-            presenter.conversationClicked(((SmartListAdapter.ViewHolder) v.getTag()).conv.getContact());
-        }
-    };
-
-    private final AdapterView.OnItemLongClickListener conversationLongClickListener =
-            new AdapterView.OnItemLongClickListener() {
-                @Override
-                public boolean onItemLongClick(AdapterView<?> parent, View v, int position, long id) {
-                    presentActions(getActivity(),
-                            ((SmartListAdapter.ViewHolder) v.getTag()).conv,
-                            SmartListFragment.this);
-                    return true;
-                }
-            };
-
-    public static void presentActions(final Activity activity,
-                                      final Conversation conversation,
-                                      final Conversation.ConversationActionCallback callback) {
-        if (activity == null) {
-            cx.ring.utils.Log.d(TAG, "presentActions: activity is null");
-            return;
-        }
-
-        if (conversation == null) {
-            cx.ring.utils.Log.d(TAG, "presentActions: conversation is null");
-            return;
-        }
-
-        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        builder.setItems(R.array.conversation_actions, new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface dialog, int which) {
-                switch (which) {
-                    case 0:
-                        ActionHelper.launchCopyNumberToClipboardFromContact(activity,
-                                conversation.getContact(),
-                                callback);
-                        break;
-                    case 1:
-                        ActionHelper.launchDeleteAction(activity, conversation, callback);
-                        break;
-                }
-            }
-        });
-        AlertDialog dialog = builder.create();
-        dialog.show();
-    }
-
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
@@ -412,22 +321,17 @@
         }
     }
 
-    private void setLoading(boolean loading) {
-        if (this.mLoader == null) {
-            return;
-        }
-        int loaderVisibility = (loading) ? View.VISIBLE : View.GONE;
-        this.mLoader.setVisibility(loaderVisibility);
-        if (loading) {
-            this.mEmptyTextView.setText("");
-        } else {
-            String emptyText = getResources().getQuantityString(R.plurals.home_conferences_title, 0, 0);
-            this.mEmptyTextView.setText(emptyText);
-        }
-
-        if (mList != null) {
-            mList.setEmptyView(this.mEmptyTextView);
-        }
+    @Override
+    public void setLoading(final boolean loading) {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (mLoader == null) {
+                    return;
+                }
+                mLoader.setVisibility(loading ? View.VISIBLE : View.GONE);
+            }
+        });
     }
 
     /**
@@ -465,20 +369,6 @@
     }
 
     @Override
-    public void pictureTapped(Conversation conversation) {
-        if (conversation == null) {
-            Log.d(TAG, "pictureTapped, conversation is null");
-            return;
-        }
-        if (conversation.getContact() != null) {
-            Activity activity = getActivity();
-            if (activity != null) {
-                ActionHelper.displayContact(getActivity(), conversation.getContact());
-            }
-        }
-    }
-
-    @Override
     public void deleteConversation(Conversation conversation) {
         mSmartListPresenter.deleteConversation(conversation);
     }
@@ -567,43 +457,84 @@
     }
 
     @Override
+    public void displayNoConversationMessage() {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                String emptyText = getResources().getQuantityString(R.plurals.home_conferences_title, 0, 0);
+                mEmptyTextView.setText(emptyText);
+                mEmptyTextView.setVisibility(View.VISIBLE);
+            }
+        });
+    }
+
+    @Override
+    public void displayConversationDialog(final Conversation conversation) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setItems(R.array.conversation_actions, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                switch (which) {
+                    case 0:
+                        ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(),
+                                conversation.getContact(),
+                                SmartListFragment.this);
+                        break;
+                    case 1:
+                        ActionHelper.launchDeleteAction(getActivity(), conversation, SmartListFragment.this);
+                        break;
+                }
+            }
+        });
+        AlertDialog dialog = builder.create();
+        dialog.show();
+    }
+
+    @Override
     public void hideSearchRow() {
-        mNewContact.setVisibility(View.GONE);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mNewContact.setVisibility(View.GONE);
+            }
+        });
     }
 
     @Override
     public void hideErrorPanel() {
-        if (mErrorMessagePane != null) {
-            mErrorMessagePane.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void updateViewWithQuery(final Collection<Conversation> list, final String query) {
-        Handler mUserInputHandler = new Handler();
-        mUserInputHandler.removeCallbacksAndMessages(null);
-        mUserInputHandler.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                if (mCallbacks.getService() == null) {
-                    Log.d(TAG, "onQueryTextChange: null service");
-                } else {
-                    mSmartListAdapter.updateDataset(
-                            list,
-                            query
-                    );
-                }
-            }
-        }, USER_INPUT_DELAY);
-    }
-
-    @Override
-    public void updateView(final Collection<Conversation> list) {
         getActivity().runOnUiThread(new Runnable() {
             @Override
             public void run() {
-                mSmartListAdapter.updateDataset(list, null);
-                setLoading(false);
+                mErrorMessagePane.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    @Override
+    public void hideNoConversationMessage() {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mEmptyTextView.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    @Override
+    public void updateView(final ArrayList<SmartListViewModel> list) {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (mRecyclerView.getAdapter() != null) {
+                    mSmartListAdapter.replaceAll(list);
+                } else {
+                    mSmartListAdapter = new SmartListAdapter(list, SmartListFragment.this);
+                    mRecyclerView.setAdapter(mSmartListAdapter);
+                    mRecyclerView.setHasFixedSize(true);
+                    LinearLayoutManager llm = new LinearLayoutManager(getActivity());
+                    llm.setOrientation(LinearLayoutManager.VERTICAL);
+                    mRecyclerView.setLayoutManager(llm);
+                }
             }
         });
     }
@@ -641,8 +572,30 @@
     }
 
     @Override
+    public void goToContact(CallContact callContact) {
+        Activity activity = getActivity();
+        if (activity != null) {
+            ActionHelper.displayContact(getActivity(), callContact);
+        }
+    }
+
+    @Override
     protected SmartListPresenter createPresenter() {
         return mSmartListPresenter;
     }
 
+    @Override
+    public void onItemClick(SmartListViewModel smartListViewModel) {
+        presenter.conversationClicked(smartListViewModel);
+    }
+
+    @Override
+    public void onItemLongClick(SmartListViewModel smartListViewModel) {
+        presenter.conversationLongClicked(smartListViewModel);
+    }
+
+    @Override
+    public void onPhotoClick(SmartListViewModel smartListViewModel) {
+        presenter.photoClicked(smartListViewModel);
+    }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
index a10fe2c..3aa2538 100644
--- a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
@@ -25,9 +25,11 @@
 import android.database.Cursor;
 import android.provider.ContactsContract;
 import android.support.annotation.NonNull;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -35,6 +37,9 @@
 
 import cx.ring.model.CallContact;
 import cx.ring.model.Uri;
+import cx.ring.utils.Tuple;
+import cx.ring.utils.VCardUtils;
+import ezvcard.VCard;
 
 public class ContactServiceImpl extends ContactService {
 
@@ -131,6 +136,7 @@
                 if (contact == null) {
                     contact = new CallContact(contactId);
                     isNewContact = true;
+                    contact.setFromSystem(true);
                 }
 
                 String contactNumber = contactCursor.getString(indexNumber);
@@ -378,4 +384,45 @@
 
         return callContact;
     }
+
+    @Override
+    public Tuple<String, String> loadContactDataFromSystem(CallContact callContact) {
+
+        String contactName = callContact.getDisplayName();
+        String photoURI = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, callContact.getId()).toString();
+
+        return new Tuple<>(contactName, photoURI);
+    }
+
+    @Override
+    public Tuple<String, byte[]> loadContactData(CallContact callContact) {
+
+        String contactName = null;
+        byte[] photoURI = null;
+        VCard vcard = null;
+
+        if (!callContact.getPhones().isEmpty()) {
+            String username = callContact.getPhones().get(0).getNumber().getHost();
+            vcard = VCardUtils.loadPeerProfileFromDisk(mContext.getFilesDir(), username + ".vcf");
+
+            if (vcard != null && vcard.getFormattedName() != null) {
+                if (!TextUtils.isEmpty(vcard.getFormattedName().getValue())) {
+                    contactName = vcard.getFormattedName().getValue();
+                }
+            }
+        }
+        if (contactName == null) {
+            contactName = callContact.getDisplayName();
+        }
+
+        if (vcard != null && !vcard.getPhotos().isEmpty()) {
+            try {
+                photoURI = vcard.getPhotos().get(0).getData();
+            } catch (Exception e) {
+                photoURI = null;
+            }
+        }
+
+        return new Tuple<>(contactName, photoURI);
+    }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/BitmapUtils.java b/ring-android/app/src/main/java/cx/ring/utils/BitmapUtils.java
index c421506..a51c10c 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/BitmapUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/BitmapUtils.java
@@ -31,7 +31,6 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
-import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
 
 /**
diff --git a/ring-android/app/src/main/java/cx/ring/utils/CircleTransform.java b/ring-android/app/src/main/java/cx/ring/utils/CircleTransform.java
new file mode 100644
index 0000000..21be7bb
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/utils/CircleTransform.java
@@ -0,0 +1,72 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package cx.ring.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+public class CircleTransform extends BitmapTransformation {
+
+    public CircleTransform(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
+        return circleCrop(pool, toTransform);
+    }
+
+    private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
+        if (source == null) {
+            return null;
+        }
+
+        int size = Math.min(source.getWidth(), source.getHeight());
+        int x = (source.getWidth() - size) / 2;
+        int y = (source.getHeight() - size) / 2;
+
+        Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
+
+        Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
+        if (result == null) {
+            result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+        }
+
+        Canvas canvas = new Canvas(result);
+        Paint paint = new Paint();
+        paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
+        paint.setAntiAlias(true);
+        float radius = size / 2f;
+        canvas.drawCircle(radius, radius, radius, paint);
+        return result;
+    }
+
+    @Override
+    public String getId() {
+        return getClass().getName();
+    }
+}
+
diff --git a/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java
new file mode 100644
index 0000000..18b9036
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java
@@ -0,0 +1,79 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package cx.ring.viewholders;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import cx.ring.R;
+import cx.ring.smartlist.SmartListViewModel;
+
+public class SmartListViewHolder extends RecyclerView.ViewHolder {
+
+    @BindView(R.id.conv_participant)
+    public TextView convParticipants;
+    @BindView(R.id.conv_last_item)
+    public TextView convStatus;
+    @BindView(R.id.conv_last_time)
+    public TextView convTime;
+    @BindView(R.id.photo)
+    public ImageView photo;
+
+    public SmartListViewHolder(View itemView) {
+        super(itemView);
+        ButterKnife.bind(this, itemView);
+    }
+
+    public void bind(final SmartListListeners clickListener, final SmartListViewModel smartListViewModel) {
+        itemView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                clickListener.onItemClick(smartListViewModel);
+
+            }
+        });
+        itemView.setOnLongClickListener(new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                clickListener.onItemLongClick(smartListViewModel);
+                return true;
+            }
+        });
+        photo.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                clickListener.onPhotoClick(smartListViewModel);
+            }
+        });
+    }
+
+    public interface SmartListListeners {
+        void onItemClick(SmartListViewModel smartListViewModel);
+
+        void onItemLongClick(SmartListViewModel smartListViewModel);
+
+        void onPhotoClick(SmartListViewModel smartListViewModel);
+    }
+
+}
diff --git a/ring-android/app/src/main/res/layout-w960dp-land/frag_smartlist.xml b/ring-android/app/src/main/res/layout-w960dp-land/frag_smartlist.xml
index 9eb2960..c6e5d80 100644
--- a/ring-android/app/src/main/res/layout-w960dp-land/frag_smartlist.xml
+++ b/ring-android/app/src/main/res/layout-w960dp-land/frag_smartlist.xml
@@ -1,114 +1,106 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:animateLayoutChanges="true"
-    android:orientation="horizontal"
     tools:context=".client.HomeActivity">
 
     <RelativeLayout
+        android:id="@+id/error_msg_pane"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"
+        android:background="@color/error_pane"
+        android:padding="16dp"
+        android:visibility="gone">
+
+        <ImageView
+            android:id="@+id/error_image_view"
+            android:layout_width="28dp"
+            android:layout_height="28dp"
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentRight="true"
+            android:layout_gravity="right"
+            android:scaleType="fitCenter"
+            android:src="@drawable/ic_settings_white" />
+
+        <TextView
+            android:id="@+id/error_msg_txt"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:layout_marginLeft="26dp"
+            android:layout_marginRight="26dp"
+            android:gravity="center_vertical|center_horizontal"
+            android:textColor="@color/white"
+            android:textSize="14sp" />
+    </RelativeLayout>
+
+
+    <LinearLayout
+        android:id="@+id/newcontact_element"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/error_msg_pane"
+        android:background="?android:attr/selectableItemBackground"
+        android:orientation="vertical"
+        android:visibility="gone">
+
+        <include
+            layout="@layout/item_contact"
+            android:layout_width="match_parent"
+            android:layout_height="72dp" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?android:attr/listDivider" />
+
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/smartlist_layout"
         android:layout_width="320dp"
-        android:layout_height="match_parent">
+        android:layout_height="match_parent"
+        android:layout_below="@+id/newcontact_element">
 
-        <RelativeLayout
-            android:id="@+id/error_msg_pane"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
-            android:layout_alignParentStart="true"
-            android:layout_alignParentTop="true"
-            android:background="@color/error_pane"
-            android:padding="16dp"
-            android:visibility="gone">
-
-            <ImageView
-                android:id="@+id/error_image_view"
-                android:layout_width="28dp"
-                android:layout_height="28dp"
-                android:layout_alignParentEnd="true"
-                android:layout_alignParentRight="true"
-                android:layout_gravity="right"
-                android:scaleType="fitCenter"
-                android:src="@drawable/ic_settings_white" />
-
-            <TextView
-                android:id="@+id/error_msg_txt"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerHorizontal="true"
-                android:layout_centerVertical="true"
-                android:layout_marginLeft="26dp"
-                android:layout_marginRight="26dp"
-                android:gravity="center_vertical|center_horizontal"
-                android:textColor="@color/white"
-                android:textSize="14sp" />
-        </RelativeLayout>
-
-
-        <LinearLayout
-            android:id="@+id/newcontact_element"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_below="@+id/error_msg_pane"
-            android:background="?android:attr/selectableItemBackground"
-            android:orientation="vertical"
-            android:visibility="gone">
-
-            <include
-                layout="@layout/item_contact"
-                android:layout_width="match_parent"
-                android:layout_height="72dp" />
-
-            <View
-                android:layout_width="match_parent"
-                android:layout_height="1dp"
-                android:background="?android:attr/listDivider" />
-
-        </LinearLayout>
-
-        <FrameLayout
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/confs_list"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_below="@+id/newcontact_element">
+            android:layout_marginLeft="0dp"
+            android:layout_marginStart="0dp"
+            android:clipToPadding="false"
+            android:divider="@null"
+            android:elevation="2dp"
+            android:paddingBottom="8dp"
+            tools:listitem="@layout/item_smartlist" />
 
-            <ListView
-                android:id="@+id/confs_list"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_marginLeft="0dp"
-                android:layout_marginStart="0dp"
-                android:clipToPadding="false"
-                android:divider="@null"
-                android:elevation="2dp"
-                android:paddingBottom="8dp"
-                tools:listitem="@layout/item_smartlist" />
+        <TextView
+            android:id="@+id/empty_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:gravity="center"
+            android:visibility="gone"
+            tools:text="0 conversations"
+            tools:visibility="visible" />
 
-            <TextView
-                android:id="@+id/emptyTextView"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:gravity="center"
-                tools:text="0 conversations" />
-
-            <ProgressBar
-                android:id="@+id/loading_indicator"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center" />
-        </FrameLayout>
+        <ProgressBar
+            android:id="@+id/loading_indicator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center" />
 
         <android.support.design.widget.FloatingActionButton
             android:id="@+id/newconv_fab"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true"
-            android:layout_alignParentEnd="true"
-            android:layout_alignParentRight="true"
-            android:layout_alignWithParentIfMissing="false"
             android:layout_gravity="bottom|end"
             android:layout_margin="@dimen/fab_compat_margin"
             android:elevation="6dp"
@@ -118,16 +110,21 @@
             app:pressedTranslationZ="12dp"
             app:rippleColor="@color/color_primary_dark" />
 
-    </RelativeLayout>
+    </FrameLayout>
 
     <View
+        android:id="@+id/separator"
         android:layout_width="1dp"
         android:layout_height="match_parent"
-        android:background="@color/darker_gray" />
+        android:background="@color/darker_gray"
+        android:layout_toRightOf="@+id/smartlist_layout"
+        android:layout_toEndOf="@+id/smartlist_layout"/>
 
     <FrameLayout
         android:id="@+id/conversation_container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:layout_toRightOf="@+id/separator"
+        android:layout_toEndOf="@+id/separator"/>
 
-</LinearLayout>
\ No newline at end of file
+</RelativeLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout/frag_smartlist.xml b/ring-android/app/src/main/res/layout/frag_smartlist.xml
index f614f6f..6ba58ab 100644
--- a/ring-android/app/src/main/res/layout/frag_smartlist.xml
+++ b/ring-android/app/src/main/res/layout/frag_smartlist.xml
@@ -18,7 +18,6 @@
 along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 -->
-
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
@@ -39,25 +38,26 @@
         android:visibility="gone">
 
         <ImageView
+            android:id="@+id/error_image_view"
             android:layout_width="28dp"
             android:layout_height="28dp"
-            android:id="@+id/error_image_view"
-            android:src="@drawable/ic_settings_white"
-            android:scaleType="fitCenter"
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentRight="true"
             android:layout_gravity="right"
-            android:layout_alignParentEnd="true" />
+            android:scaleType="fitCenter"
+            android:src="@drawable/ic_settings_white" />
 
         <TextView
             android:id="@+id/error_msg_txt"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:textColor="@color/white"
-            android:textSize="14sp"
-            android:gravity="center_vertical|center_horizontal"
-            android:layout_centerVertical="true"
             android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
             android:layout_marginLeft="26dp"
-            android:layout_marginRight="26dp" />
+            android:layout_marginRight="26dp"
+            android:gravity="center_vertical|center_horizontal"
+            android:textColor="@color/white"
+            android:textSize="14sp" />
     </RelativeLayout>
 
 
@@ -65,9 +65,9 @@
         android:id="@+id/newcontact_element"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_below="@+id/error_msg_pane"
         android:background="?android:attr/selectableItemBackground"
         android:orientation="vertical"
-        android:layout_below="@+id/error_msg_pane"
         android:visibility="gone">
 
         <include
@@ -83,11 +83,12 @@
     </LinearLayout>
 
     <FrameLayout
+        android:id="@+id/smartlist_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_below="@+id/newcontact_element">
 
-        <ListView
+        <android.support.v7.widget.RecyclerView
             android:id="@+id/confs_list"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
@@ -100,12 +101,14 @@
             tools:listitem="@layout/item_smartlist" />
 
         <TextView
-            android:id="@+id/emptyTextView"
+            android:id="@+id/empty_text_view"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            tools:text="0 conversations"
+            android:layout_gravity="center"
             android:gravity="center"
-            android:layout_gravity="center" />
+            android:visibility="gone"
+            tools:text="0 conversations"
+            tools:visibility="visible" />
 
         <ProgressBar
             android:id="@+id/loading_indicator"
diff --git a/ring-android/app/src/main/res/layout/item_smartlist.xml b/ring-android/app/src/main/res/layout/item_smartlist.xml
index d788264..cf48783 100644
--- a/ring-android/app/src/main/res/layout/item_smartlist.xml
+++ b/ring-android/app/src/main/res/layout/item_smartlist.xml
@@ -19,11 +19,14 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/call_entry"
     android:layout_width="match_parent"
     android:layout_height="72dp"
     android:descendantFocusability="blocksDescendants"
-    android:padding="16dp">
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp"
+    android:background="?android:attr/selectableItemBackground">
 
     <ImageView
         android:id="@+id/photo"
@@ -35,7 +38,7 @@
         android:layout_marginEnd="16dp"
         android:layout_marginRight="16dp"
         android:background="@null"
-        android:scaleType="centerCrop"
+        android:scaleType="fitCenter"
         android:src="@drawable/ic_contact_picture" />
 
     <TextView
@@ -43,7 +46,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentTop="true"
-        android:layout_marginTop="2dp"
+        android:layout_marginTop="14dp"
         android:layout_toEndOf="@+id/photo"
         android:layout_toLeftOf="@+id/conv_last_time"
         android:layout_toRightOf="@+id/photo"
@@ -55,7 +58,8 @@
         android:singleLine="true"
         android:textAlignment="viewStart"
         android:textColor="@color/text_color_primary"
-        android:textSize="16sp" />
+        android:textSize="16sp"
+        tools:text="Test test"/>
 
     <TextView
         android:id="@+id/conv_last_item"
@@ -70,7 +74,8 @@
         android:maxLines="2"
         android:textAlignment="viewStart"
         android:textColor="@color/text_color_secondary"
-        android:textSize="14sp" />
+        android:textSize="14sp"
+        tools:text="Ongoing call of 56 secs"/>
 
     <TextView
         android:id="@+id/conv_last_time"
@@ -80,6 +85,7 @@
         android:layout_alignParentEnd="true"
         android:layout_alignParentRight="true"
         android:textColor="@color/text_color_secondary"
-        android:textSize="12sp" />
+        android:textSize="12sp"
+        tools:text="2days ago"/>
 
 </RelativeLayout>
\ No newline at end of file
diff --git a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
index d2205a5..3fea2dc 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
@@ -19,7 +19,6 @@
  */
 package cx.ring.facades;
 
-import java.rmi.RemoteException;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -522,6 +521,7 @@
                     notifyObservers(mEvent);
                     break;
                 case HISTORY_LOADED:
+
                     List<HistoryCall> historyCalls = (List<HistoryCall>) event.getEventInput(ServiceEvent.EventInput.HISTORY_CALLS, ArrayList.class);
                     parseHistoryCalls(historyCalls);
 
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
index 2581d89..7306acb 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
@@ -38,6 +38,7 @@
     private boolean isUser;
     private WeakReference<byte[]> mContactPhoto = new WeakReference<>(null);
     private boolean stared = false;
+    private boolean isFromSystem = false;
 
     public CallContact(long cID) {
         this(cID, null, null, UNKNOWN_ID);
@@ -202,6 +203,14 @@
         mContactPhoto = new WeakReference<>(externalArray);
     }
 
+    public boolean isFromSystem() {
+        return isFromSystem;
+    }
+
+    public void setFromSystem(boolean fromSystem) {
+        isFromSystem = fromSystem;
+    }
+
     /**
      * A contact is Unknown when his name == his phone number
      *
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
index 613e8c3..e8ad435 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
@@ -48,12 +48,12 @@
 
     // runtime flag set to true if the user is currently viewing this conversation
     private boolean mVisible = false;
-    private int uuid;
+    private String uuid;
 
-    public Conversation(CallContact c) {
-        setContact(c);
+    public Conversation(CallContact contact) {
+        setContact(contact);
+        this.uuid = contact.getIds().get(0);
         mCurrentCalls = new ArrayList<>();
-        setUuid(new Random().nextInt());
     }
 
     public class ConversationElement {
@@ -138,11 +138,11 @@
         this.mVisible = mVisible;
     }
 
-    public int getUuid() {
+    public String getUuid() {
         return uuid;
     }
 
-    public void setUuid(int uuid) {
+    public void setUuid(String uuid) {
         this.uuid = uuid;
     }
 
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
index 3d63be7..899fdf1 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
@@ -38,6 +38,7 @@
 import cx.ring.utils.FutureUtils;
 import cx.ring.utils.Log;
 import cx.ring.utils.Observable;
+import cx.ring.utils.Tuple;
 
 /**
  * This service handles the contacts
@@ -52,6 +53,9 @@
  */
 public abstract class ContactService extends Observable {
 
+    public static final String CONTACT_NAME_KEY = "CONTACT_NAME";
+    public static final String CONTACT_PHOTO_KEY = "CONTACT_PHOTO";
+
     private final static String TAG = ContactService.class.getName();
 
     @Inject
@@ -79,6 +83,10 @@
 
     protected abstract CallContact findContactByNumberFromSystem(String number);
 
+    public abstract Tuple<String, String> loadContactDataFromSystem(CallContact callContact);
+
+    public abstract Tuple<String, byte[]> loadContactData(CallContact callContact);
+
     public ContactService() {
         mContactList = new HashMap<>();
         mCallbackHandler = new ConfigurationCallbackHandler();
diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
index 4cfbee8..0200bd8 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
@@ -20,6 +20,9 @@
 package cx.ring.smartlist;
 
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -39,6 +42,7 @@
 import cx.ring.utils.BlockchainInputHandler;
 import cx.ring.utils.Observable;
 import cx.ring.utils.Observer;
+import cx.ring.utils.Tuple;
 
 public class SmartListPresenter extends RootPresenter<SmartListView> implements Observer<ServiceEvent> {
 
@@ -55,6 +59,9 @@
     private BlockchainInputHandler mBlockchainInputHandler;
     private String mLastBlockchainQuery = null;
 
+    private ArrayList<Conversation> mConversations;
+    private ArrayList<SmartListViewModel> mSmartListViewModels;
+
     @Inject
     public SmartListPresenter(AccountService accountService, ContactService contactService,
                               HistoryService historyService, ConversationFacade conversationFacade,
@@ -89,7 +96,8 @@
         boolean isConnected = isConnectedWifi
                 || (isConnectedMobile && mSettingsService.getUserSettings().isAllowMobileData());
 
-        boolean isMobileAndNotAllowed = isConnectedMobile && !mSettingsService.getUserSettings().isAllowMobileData();
+        boolean isMobileAndNotAllowed = isConnectedMobile
+                && !mSettingsService.getUserSettings().isAllowMobileData();
 
         if (isConnected) {
             getView().hideErrorPanel();
@@ -103,6 +111,7 @@
 
         mConversationFacade.refreshConversations();
         searchForRingIdInBlockchain();
+        getView().hideSearchRow();
     }
 
     public void queryTextChanged(String query) {
@@ -141,7 +150,8 @@
             }
         }
 
-        getView().updateViewWithQuery(mConversationFacade.getConversationsList(), query);
+        getView().updateView(filter(mSmartListViewModels, query));
+        getView().setLoading(false);
     }
 
     public void newContactClicked(CallContact callContact) {
@@ -151,11 +161,25 @@
         startConversation(callContact);
     }
 
-    public void conversationClicked(CallContact callContact) {
-        if (callContact == null) {
-            return;
+    public void conversationClicked(SmartListViewModel smartListViewModel) {
+        Conversation conversation = getConversationByUuid(mConversations, smartListViewModel.getUuid());
+        if (conversation != null && conversation.getContact() != null) {
+            startConversation(conversation.getContact());
         }
-        startConversation(callContact);
+    }
+
+    public void conversationLongClicked(SmartListViewModel smartListViewModel) {
+        Conversation conversation = getConversationByUuid(mConversations, smartListViewModel.getUuid());
+        if (conversation != null) {
+            getView().displayConversationDialog(conversation);
+        }
+    }
+
+    public void photoClicked(SmartListViewModel smartListViewModel) {
+        Conversation conversation = getConversationByUuid(mConversations, smartListViewModel.getUuid());
+        if (conversation != null && conversation.getContact() != null) {
+            getView().goToContact(conversation.getContact());
+        }
     }
 
     public void quickCallClicked(CallContact callContact) {
@@ -211,28 +235,111 @@
         }
     }
 
+    private synchronized void displayConversations() {
+        if (mConversations == null) {
+            mConversations = new ArrayList<>();
+        }
+        mConversations.clear();
+        mConversations.addAll(mConversationFacade.getConversationsList());
+        if (mConversations != null && mConversations.size() > 0) {
+            if (mSmartListViewModels == null) {
+                mSmartListViewModels = new ArrayList<>();
+            }
+            for (int i = 0; i < mConversations.size(); i++) {
+                Conversation conversation = mConversations.get(i);
+                SmartListViewModel smartListViewModel = getSmartListViewModelByUuid(mSmartListViewModels, conversation.getUuid());
+
+                if (smartListViewModel == null || i >= mSmartListViewModels.size()) {
+
+                    if(conversation.getContact().isFromSystem()) {
+                        Tuple<String, String> tuple = mContactService.loadContactDataFromSystem(conversation.getContact());
+                        smartListViewModel = new SmartListViewModel(conversation, tuple.first, tuple.second, null);
+                    } else {
+                        Tuple<String, byte[]> tuple = mContactService.loadContactData(conversation.getContact());
+                        smartListViewModel = new SmartListViewModel(conversation, tuple.first, null, tuple.second);
+                    }
+
+                    mSmartListViewModels.add(smartListViewModel);
+                } else {
+                    if(conversation.getContact().isFromSystem()) {
+                        Tuple<String, String> tuple = mContactService.loadContactDataFromSystem(conversation.getContact());
+                        smartListViewModel.update(conversation, tuple.first, tuple.second, null);
+                    } else {
+                        Tuple<String, byte[]> tuple = mContactService.loadContactData(conversation.getContact());
+                        smartListViewModel.update(conversation, tuple.first, null, tuple.second);
+                    }
+                }
+            }
+
+            Collections.sort(mSmartListViewModels, new Comparator<SmartListViewModel>() {
+                @Override
+                public int compare(SmartListViewModel lhs, SmartListViewModel rhs) {
+                    return (int) ((rhs.getLastInteractionTime() - lhs.getLastInteractionTime()) / 1000l);
+                }
+            });
+
+            getView().updateView(mSmartListViewModels);
+            getView().hideNoConversationMessage();
+            getView().setLoading(false);
+        } else {
+            getView().displayNoConversationMessage();
+            getView().setLoading(false);
+        }
+    }
+
+    private ArrayList<SmartListViewModel> filter(ArrayList<SmartListViewModel> list, String query) {
+        ArrayList<SmartListViewModel> filteredList = new ArrayList<>();
+        if(list == null || list.size() == 0) {
+            return filteredList;
+        }
+        for(SmartListViewModel smartListViewModel : list) {
+            if(smartListViewModel.getContactName().toLowerCase().contains(query.toLowerCase())) {
+                filteredList.add(smartListViewModel);
+            }
+        }
+        return filteredList;
+    }
+
+    private Conversation getConversationByUuid(ArrayList<Conversation> conversations, String uuid) {
+        for (Conversation conversation : conversations) {
+            if (conversation.getUuid().equals(uuid)) {
+                return conversation;
+            }
+        }
+        return null;
+    }
+
+    private SmartListViewModel getSmartListViewModelByUuid(ArrayList<SmartListViewModel> smartListViewModels, String uuid) {
+        for (SmartListViewModel smartListViewModel : smartListViewModels) {
+            if (smartListViewModel.getUuid().equals(uuid)) {
+                return smartListViewModel;
+            }
+        }
+        return null;
+    }
+
     private void parseEventState(String name, String address, int state) {
         switch (state) {
             case 0:
                 // on found
-                if (mLastBlockchainQuery.equals(name)) {
+                if (mLastBlockchainQuery != null && mLastBlockchainQuery.equals(name)) {
                     getView().displayNewContactRowWithName(name, address);
                     mLastBlockchainQuery = null;
                 } else {
+                    if (name.equals("") || address.equals("")) {
+                        return;
+                    }
                     getView().hideSearchRow();
+                    mConversationFacade.updateConversationContactWithRingId(name, address);
+                    displayConversations();
                 }
-
-                if (name.equals("") || address.equals("")) {
-                    return;
-                }
-
-                mConversationFacade.updateConversationContactWithRingId(name, address);
-                getView().updateView(mConversationFacade.getConversationsList());
                 break;
             case 1:
                 // invalid name
                 Uri uriName = new Uri(name);
-                if (uriName.isRingId()) {
+                if (uriName.isRingId()
+                        && mLastBlockchainQuery != null
+                        && mLastBlockchainQuery.equals(name)) {
                     getView().displayNewContactRowWithName(name, null);
                 } else {
                     getView().hideSearchRow();
@@ -241,8 +348,10 @@
             default:
                 // on error
                 Uri uriAddress = new Uri(address);
-                if (uriAddress.isRingId()) {
-                    getView().displayNewContactRowWithName(name, address);
+                if (uriAddress.isRingId()
+                        && mLastBlockchainQuery != null
+                        && mLastBlockchainQuery.equals(name)) {
+                    getView().displayNewContactRowWithName(address, null);
                 } else {
                     getView().hideSearchRow();
                 }
@@ -260,7 +369,8 @@
         switch (event.getEventType()) {
             case REGISTERED_NAME_FOUND:
                 String name = event.getEventInput(ServiceEvent.EventInput.NAME, String.class);
-                if (mLastBlockchainQuery.equals("") || !mLastBlockchainQuery.equals(name)) {
+                if (mLastBlockchainQuery != null
+                        && (mLastBlockchainQuery.equals("") || !mLastBlockchainQuery.equals(name))) {
                     return;
                 }
                 String address = event.getEventInput(ServiceEvent.EventInput.ADDRESS, String.class);
@@ -269,10 +379,8 @@
                 break;
             case INCOMING_MESSAGE:
             case HISTORY_LOADED:
-                getView().updateView(mConversationFacade.getConversationsList());
-                break;
             case CONVERSATIONS_CHANGED:
-                getView().updateView(mConversationFacade.getConversationsList());
+                displayConversations();
                 break;
         }
     }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
index 7bc9d0f..67db8f8 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
@@ -19,6 +19,7 @@
  */
 package cx.ring.smartlist;
 
+import java.util.ArrayList;
 import java.util.Collection;
 
 import cx.ring.model.CallContact;
@@ -34,17 +35,25 @@
 
     void displayChooseNumberDialog(CharSequence numbers[]);
 
+    void displayNoConversationMessage();
+
+    void displayConversationDialog(Conversation conversation);
+
+    void setLoading(boolean display);
+
     void hideSearchRow();
 
     void hideErrorPanel();
 
-    void updateView(final Collection<Conversation> list);
+    void hideNoConversationMessage();
 
-    void updateViewWithQuery(final Collection<Conversation> list, String query);
+    void updateView(ArrayList<SmartListViewModel> list);
 
     void goToConversation(CallContact callContact);
 
     void goToCallActivity(String rawUriNumber);
 
     void goToQRActivity();
+
+    void goToContact(CallContact callContact);
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java
new file mode 100644
index 0000000..679bdc3
--- /dev/null
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java
@@ -0,0 +1,103 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package cx.ring.smartlist;
+
+import java.util.Map;
+
+import cx.ring.model.Conversation;
+import cx.ring.model.HistoryEntry;
+import cx.ring.services.ContactService;
+import cx.ring.utils.Tuple;
+
+public class SmartListViewModel {
+
+    private String uuid;
+    private String contactName;
+    private HistoryEntry lastInteraction;
+    private String photoUri;
+    private byte[] photoData;
+    private long lastInteractionTime;
+    private boolean hasUnreadTextMessage;
+    private boolean hasOngoingCall;
+
+    public SmartListViewModel(Conversation conversation, String contactName, String photoUri, byte[] photoData) {
+        this.uuid = conversation.getUuid();
+        setData(conversation, contactName, photoUri, photoData);
+    }
+
+    public void update(Conversation conversation, String contactName, String photoUri, byte[] photoData) {
+        setData(conversation, contactName, photoUri, photoData);
+    }
+
+    private void setData(Conversation conversation, String contactName, String photoUri, byte[] photoData) {
+        this.contactName = contactName;
+        this.photoUri = photoUri;
+        this.photoData = photoData;
+
+        for (HistoryEntry historyEntry : conversation.getHistory().values()) {
+            long lastTextTimestamp = historyEntry.getTextMessages().isEmpty() ? 0 : historyEntry.getTextMessages().lastEntry().getKey();
+            long lastCallTimestamp = historyEntry.getCalls().isEmpty() ? 0 : historyEntry.getCalls().lastEntry().getKey();
+            if (lastTextTimestamp > 0 && lastTextTimestamp > lastCallTimestamp) {
+                this.lastInteraction = historyEntry;
+                break;
+            }
+            if (lastCallTimestamp > 0) {
+                this.lastInteraction = historyEntry;
+                break;
+            }
+        }
+
+        this.lastInteractionTime = conversation.getLastInteraction().getTime();
+        this.hasUnreadTextMessage = conversation.hasUnreadTextMessages();
+        this.hasOngoingCall = conversation.hasCurrentCall();
+    }
+
+    public String getContactName() {
+        return contactName;
+    }
+
+    public HistoryEntry getLastInteraction() {
+        return lastInteraction;
+    }
+
+    public String getPhotoUri() {
+        return photoUri;
+    }
+
+    public long getLastInteractionTime() {
+        return lastInteractionTime;
+    }
+
+    public boolean hasUnreadTextMessage() {
+        return hasUnreadTextMessage;
+    }
+
+    public boolean hasOngoingCall() {
+        return hasOngoingCall;
+    }
+
+    public String getUuid() {
+        return uuid;
+    }
+
+    public byte[] getPhotoData() {
+        return photoData;
+    }
+}